/* Copyright (c) 2014, The Linux Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of The Linux Foundation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.android.incallui; import android.telecom.VideoProfile; import com.android.incallui.Call.State; import com.android.incallui.InCallPresenter.InCallState; import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.incallui.InCallPresenter.IncomingCallListener; import com.android.incallui.InCallVideoCallCallbackNotifier.SessionModificationListener; import com.google.common.base.Preconditions; /** * This class is responsible for generating video pause/resume requests when the InCall UI is sent * to the background and subsequently brought back to the foreground. */ class VideoPauseController implements InCallStateListener, IncomingCallListener, SessionModificationListener { private static final String TAG = "VideoPauseController:"; /** * Keeps track of the current active/foreground call. */ private class CallContext { public CallContext(Call call) { Preconditions.checkNotNull(call); update(call); } public void update(Call call) { mCall = Preconditions.checkNotNull(call); mState = call.getState(); mVideoState = call.getVideoState(); } public int getState() { return mState; } public int getVideoState() { return mVideoState; } public String toString() { return String.format("CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState); } public Call getCall() { return mCall; } private int mState = State.INVALID; private int mVideoState; private Call mCall; } private InCallPresenter mInCallPresenter; private static VideoPauseController sVideoPauseController; /** * The current call context, if applicable. */ private CallContext mPrimaryCallContext = null; /** * Tracks whether the application is in the background. {@code True} if the application is in * the background, {@code false} otherwise. */ private boolean mIsInBackground = false; /** * Singleton accessor for the {@link VideoPauseController}. * @return Singleton instance of the {@link VideoPauseController}. */ /*package*/ static synchronized VideoPauseController getInstance() { if (sVideoPauseController == null) { sVideoPauseController = new VideoPauseController(); } return sVideoPauseController; } /** * Configures the {@link VideoPauseController} to listen to call events. Configured via the * {@link com.android.incallui.InCallPresenter}. * * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}. */ public void setUp(InCallPresenter inCallPresenter) { log("setUp"); mInCallPresenter = Preconditions.checkNotNull(inCallPresenter); mInCallPresenter.addListener(this); mInCallPresenter.addIncomingCallListener(this); InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this); } /** * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its * internal state. Called from {@link com.android.incallui.InCallPresenter}. */ public void tearDown() { log("tearDown..."); InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this); mInCallPresenter.removeListener(this); mInCallPresenter.removeIncomingCallListener(this); clear(); } /** * Clears the internal state for the {@link VideoPauseController}. */ private void clear() { mInCallPresenter = null; mPrimaryCallContext = null; mIsInBackground = false; } /** * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the * current foreground call. * * @param oldState The previous {@link InCallState}. * @param newState The current {@link InCallState}. * @param callList List of current call. */ @Override public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { log("onStateChange, OldState=" + oldState + " NewState=" + newState); Call call = null; if (newState == InCallState.INCOMING) { call = callList.getIncomingCall(); } else if (newState == InCallState.WAITING_FOR_ACCOUNT) { call = callList.getWaitingForAccountCall(); } else if (newState == InCallState.PENDING_OUTGOING) { call = callList.getPendingOutgoingCall(); } else if (newState == InCallState.OUTGOING) { call = callList.getOutgoingCall(); } else { call = callList.getActiveCall(); } boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext); boolean canVideoPause = CallUtils.canVideoPause(call); log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged); log("onStateChange, canVideoPause=" + canVideoPause); log("onStateChange, IsInBackground=" + mIsInBackground); if (hasPrimaryCallChanged) { onPrimaryCallChanged(call); return; } if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) { // Bring UI to foreground if outgoing request becomes active while UI is in // background. bringToForeground(); } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) { // Bring UI to foreground if VoLTE call becomes active while UI is in // background. bringToForeground(); } updatePrimaryCallContext(call); } /** * Handles a change to the primary call. *
* Reject incoming or hangup dialing call: Where the previous call was an incoming call or a * call in dialing state, resume the new primary call. * Call swap: Where the new primary call is incoming, pause video on the previous primary call. * * @param call The new primary call. */ private void onPrimaryCallChanged(Call call) { log("onPrimaryCallChanged: New call = " + call); log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext); log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground); Preconditions.checkState(!areSame(call, mPrimaryCallContext)); final boolean canVideoPause = CallUtils.canVideoPause(call); if ((isIncomingCall(mPrimaryCallContext) || isDialing(mPrimaryCallContext)) && canVideoPause && !mIsInBackground) { // Send resume request for the active call, if user rejects incoming call or ends // dialing call and UI is in the foreground. sendRequest(call, true); } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) { // Send pause request if there is an active video call, and we just received a new // incoming call. sendRequest(mPrimaryCallContext.getCall(), false); } updatePrimaryCallContext(call); } /** * Handles new incoming calls by triggering a change in the primary call. * * @param oldState the old {@link InCallState}. * @param newState the new {@link InCallState}. * @param call the incoming call. */ @Override public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " Call=" + call); if (areSame(call, mPrimaryCallContext)) { return; } onPrimaryCallChanged(call); } /** * Caches a reference to the primary call and stores its previous state. * * @param call The new primary call. */ private void updatePrimaryCallContext(Call call) { if (call == null) { mPrimaryCallContext = null; } else if (mPrimaryCallContext != null) { mPrimaryCallContext.update(call); } else { mPrimaryCallContext = new CallContext(call); } } /** * Called when UI goes in/out of the foreground. * @param showing true if UI is in the foreground, false otherwise. */ public void onUiShowing(boolean showing) { // Only send pause/unpause requests if we are in the INCALL state. if (mInCallPresenter == null || mInCallPresenter.getInCallState() != InCallState.INCALL) { return; } if (showing) { onResume(); } else { onPause(); } } /** * Handles requests to upgrade to video. * * @param call The call the request was received for. * @param videoState The video state that the request wants to upgrade to. */ @Override public void onUpgradeToVideoRequest(Call call, int videoState) { // Not used. } /** * Handles successful upgrades to video. * @param call The call the request was successful for. */ @Override public void onUpgradeToVideoSuccess(Call call) { // Not used. } /** * Handles a failure to upgrade a call to video. * * @param status The failure status. * @param call The call the request was successful for. */ @Override public void onUpgradeToVideoFail(int status, Call call) { // TODO (ims-vt) Automatically bring in call ui to foreground. } /** * Handles a downgrade of a call to audio-only. * * @param call The call which was downgraded to audio-only. */ @Override public void onDowngradeToAudio(Call call) { } /** * Called when UI is brought to the foreground. Sends a session modification request to resume * the outgoing video. */ private void onResume() { log("onResume"); mIsInBackground = false; if (canVideoPause(mPrimaryCallContext)) { sendRequest(mPrimaryCallContext.getCall(), true); } else { log("onResume. Ignoring..."); } } /** * Called when UI is sent to the background. Sends a session modification request to pause the * outgoing video. */ private void onPause() { log("onPause"); mIsInBackground = true; if (canVideoPause(mPrimaryCallContext)) { sendRequest(mPrimaryCallContext.getCall(), false); } else { log("onPause, Ignoring..."); } } private void bringToForeground() { if (mInCallPresenter != null) { log("Bringing UI to foreground"); mInCallPresenter.bringToForeground(false); } else { loge("InCallPresenter is null. Cannot bring UI to foreground"); } } /** * Sends Pause/Resume request. * * @param call Call to be paused/resumed. * @param resume If true resume request will be sent, otherwise pause request. */ private void sendRequest(Call call, boolean resume) { // Check if this call supports pause/un-pause. if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) { return; } if (resume) { log("sending resume request, call=" + call); call.getVideoCall() .sendSessionModifyRequest(CallUtils.makeVideoUnPauseProfile(call)); } else { log("sending pause request, call=" + call); call.getVideoCall().sendSessionModifyRequest(CallUtils.makeVideoPauseProfile(call)); } } /** * Determines if a given call is the same one stored in a {@link CallContext}. * * @param call The call. * @param callContext The call context. * @return {@code true} if the {@link Call} is the same as the one referenced in the * {@link CallContext}. */ private static boolean areSame(Call call, CallContext callContext) { if (call == null && callContext == null) { return true; } else if (call == null || callContext == null) { return false; } return call.equals(callContext.getCall()); } /** * Determines if a video call can be paused. Only a video call which is active can be paused. * * @param callContext The call context to check. * @return {@code true} if the call is an active video call. */ private static boolean canVideoPause(CallContext callContext) { return isVideoCall(callContext) && callContext.getState() == Call.State.ACTIVE; } /** * Determines if a call referenced by a {@link CallContext} is a video call. * * @param callContext The call context. * @return {@code true} if the call is a video call, {@code false} otherwise. */ private static boolean isVideoCall(CallContext callContext) { return callContext != null && CallUtils.isVideoCall(callContext.getVideoState()); } /** * Determines if call is in incoming/waiting state. * * @param call The call context. * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. */ private static boolean isIncomingCall(CallContext call) { return call != null && isIncomingCall(call.getCall()); } /** * Determines if a call is in incoming/waiting state. * * @param call The call. * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. */ private static boolean isIncomingCall(Call call) { return call != null && (call.getState() == Call.State.CALL_WAITING || call.getState() == Call.State.INCOMING); } /** * Determines if a call is dialing. * * @param call The call context. * @return {@code true} if the call is dialing, {@code false} otherwise. */ private static boolean isDialing(CallContext call) { return call != null && Call.State.isDialing(call.getState()); } /** * Determines if a call is holding. * * @param call The call context. * @return {@code true} if the call is holding, {@code false} otherwise. */ private static boolean isHolding(CallContext call) { return call != null && call.getState() == Call.State.ONHOLD; } /** * Logs a debug message. * * @param msg The message. */ private void log(String msg) { Log.d(this, TAG + msg); } /** * Logs an error message. * * @param msg The message. */ private void loge(String msg) { Log.e(this, TAG + msg); } }