1 /*
2  * Copyright (C) 2015 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;
18 
19 import com.android.incallui.Call.State;
20 import com.android.incallui.InCallPresenter.InCallState;
21 import com.android.incallui.InCallPresenter.InCallStateListener;
22 import com.android.incallui.InCallPresenter.IncomingCallListener;
23 import com.android.incallui.InCallVideoCallCallbackNotifier.SessionModificationListener;
24 import com.google.common.base.Preconditions;
25 
26 import android.telecom.VideoProfile;
27 
28 /**
29  * This class is responsible for generating video pause/resume requests when the InCall UI is sent
30  * to the background and subsequently brought back to the foreground.
31  */
32 class VideoPauseController implements InCallStateListener, IncomingCallListener {
33     private static final String TAG = "VideoPauseController";
34 
35     /**
36      * Keeps track of the current active/foreground call.
37      */
38     private class CallContext {
CallContext(Call call)39         public CallContext(Call call) {
40             Preconditions.checkNotNull(call);
41             update(call);
42         }
43 
update(Call call)44         public void update(Call call) {
45             mCall = Preconditions.checkNotNull(call);
46             mState = call.getState();
47             mVideoState = call.getVideoState();
48         }
49 
getState()50         public int getState() {
51             return mState;
52         }
53 
getVideoState()54         public int getVideoState() {
55             return mVideoState;
56         }
57 
toString()58         public String toString() {
59             return String.format("CallContext {CallId=%s, State=%s, VideoState=%d}",
60                     mCall.getId(), mState, mVideoState);
61         }
62 
getCall()63         public Call getCall() {
64             return mCall;
65         }
66 
67         private int mState = State.INVALID;
68         private int mVideoState;
69         private Call mCall;
70     }
71 
72     private InCallPresenter mInCallPresenter;
73     private static VideoPauseController sVideoPauseController;
74 
75     /**
76      * The current call context, if applicable.
77      */
78     private CallContext mPrimaryCallContext = null;
79 
80     /**
81      * Tracks whether the application is in the background. {@code True} if the application is in
82      * the background, {@code false} otherwise.
83      */
84     private boolean mIsInBackground = false;
85 
86     /**
87      * Singleton accessor for the {@link VideoPauseController}.
88      * @return Singleton instance of the {@link VideoPauseController}.
89      */
90     /*package*/
getInstance()91     static synchronized VideoPauseController getInstance() {
92         if (sVideoPauseController == null) {
93             sVideoPauseController = new VideoPauseController();
94         }
95         return sVideoPauseController;
96     }
97 
98     /**
99      * Configures the {@link VideoPauseController} to listen to call events.  Configured via the
100      * {@link com.android.incallui.InCallPresenter}.
101      *
102      * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
103      */
setUp(InCallPresenter inCallPresenter)104     public void setUp(InCallPresenter inCallPresenter) {
105         log("setUp");
106         mInCallPresenter = Preconditions.checkNotNull(inCallPresenter);
107         mInCallPresenter.addListener(this);
108         mInCallPresenter.addIncomingCallListener(this);
109     }
110 
111     /**
112      * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its
113      * internal state.  Called from {@link com.android.incallui.InCallPresenter}.
114      */
tearDown()115     public void tearDown() {
116         log("tearDown...");
117         mInCallPresenter.removeListener(this);
118         mInCallPresenter.removeIncomingCallListener(this);
119         clear();
120     }
121 
122     /**
123      * Clears the internal state for the {@link VideoPauseController}.
124      */
clear()125     private void clear() {
126         mInCallPresenter = null;
127         mPrimaryCallContext = null;
128         mIsInBackground = false;
129     }
130 
131     /**
132      * Handles changes in the {@link InCallState}.  Triggers pause and resumption of video for the
133      * current foreground call.
134      *
135      * @param oldState The previous {@link InCallState}.
136      * @param newState The current {@link InCallState}.
137      * @param callList List of current call.
138      */
139     @Override
onStateChange(InCallState oldState, InCallState newState, CallList callList)140     public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
141         log("onStateChange, OldState=" + oldState + " NewState=" + newState);
142 
143         Call call = null;
144         if (newState == InCallState.INCOMING) {
145             call = callList.getIncomingCall();
146         } else if (newState == InCallState.WAITING_FOR_ACCOUNT) {
147             call = callList.getWaitingForAccountCall();
148         } else if (newState == InCallState.PENDING_OUTGOING) {
149             call = callList.getPendingOutgoingCall();
150         } else if (newState == InCallState.OUTGOING) {
151             call = callList.getOutgoingCall();
152         } else {
153             call = callList.getActiveCall();
154         }
155 
156         boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
157         boolean canVideoPause = VideoUtils.canVideoPause(call);
158         log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
159         log("onStateChange, canVideoPause=" + canVideoPause);
160         log("onStateChange, IsInBackground=" + mIsInBackground);
161 
162         if (hasPrimaryCallChanged) {
163             onPrimaryCallChanged(call);
164             return;
165         }
166 
167         if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
168             // Bring UI to foreground if outgoing request becomes active while UI is in
169             // background.
170             bringToForeground();
171         } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
172             // Bring UI to foreground if VoLTE call becomes active while UI is in
173             // background.
174             bringToForeground();
175         }
176 
177         updatePrimaryCallContext(call);
178     }
179 
180     /**
181      * Handles a change to the primary call.
182      * <p>
183      * Reject incoming or hangup dialing call: Where the previous call was an incoming call or a
184      * call in dialing state, resume the new primary call.
185      * Call swap: Where the new primary call is incoming, pause video on the previous primary call.
186      *
187      * @param call The new primary call.
188      */
onPrimaryCallChanged(Call call)189     private void onPrimaryCallChanged(Call call) {
190         log("onPrimaryCallChanged: New call = " + call);
191         log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
192         log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
193 
194         Preconditions.checkState(!areSame(call, mPrimaryCallContext));
195         final boolean canVideoPause = VideoUtils.canVideoPause(call);
196 
197         if ((isIncomingCall(mPrimaryCallContext) || isDialing(mPrimaryCallContext) ||
198                 (call != null && VideoProfile.isPaused(call.getVideoState())))
199                 && canVideoPause && !mIsInBackground) {
200             // Send resume request for the active call, if user rejects incoming call, ends dialing
201             // call, or the call was previously in a paused state and UI is in the foreground.
202             sendRequest(call, true);
203         } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
204             // Send pause request if there is an active video call, and we just received a new
205             // incoming call.
206             sendRequest(mPrimaryCallContext.getCall(), false);
207         }
208 
209         updatePrimaryCallContext(call);
210     }
211 
212     /**
213      * Handles new incoming calls by triggering a change in the primary call.
214      *
215      * @param oldState the old {@link InCallState}.
216      * @param newState the new {@link InCallState}.
217      * @param call the incoming call.
218      */
219     @Override
onIncomingCall(InCallState oldState, InCallState newState, Call call)220     public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
221         log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " Call=" + call);
222 
223         if (areSame(call, mPrimaryCallContext)) {
224             return;
225         }
226 
227         onPrimaryCallChanged(call);
228     }
229 
230     /**
231      * Caches a reference to the primary call and stores its previous state.
232      *
233      * @param call The new primary call.
234      */
updatePrimaryCallContext(Call call)235     private void updatePrimaryCallContext(Call call) {
236         if (call == null) {
237             mPrimaryCallContext = null;
238         } else if (mPrimaryCallContext != null) {
239             mPrimaryCallContext.update(call);
240         } else {
241             mPrimaryCallContext = new CallContext(call);
242         }
243     }
244 
245     /**
246      * Called when UI goes in/out of the foreground.
247      * @param showing true if UI is in the foreground, false otherwise.
248      */
onUiShowing(boolean showing)249     public void onUiShowing(boolean showing) {
250         // Only send pause/unpause requests if we are in the INCALL state.
251         if (mInCallPresenter == null || mInCallPresenter.getInCallState() != InCallState.INCALL) {
252             return;
253         }
254 
255         if (showing) {
256             onResume();
257         } else {
258             onPause();
259         }
260     }
261 
262     /**
263      * Called when UI is brought to the foreground.  Sends a session modification request to resume
264      * the outgoing video.
265      */
onResume()266     private void onResume() {
267         log("onResume");
268 
269         mIsInBackground = false;
270         if (canVideoPause(mPrimaryCallContext)) {
271             sendRequest(mPrimaryCallContext.getCall(), true);
272         } else {
273             log("onResume. Ignoring...");
274         }
275     }
276 
277     /**
278      * Called when UI is sent to the background.  Sends a session modification request to pause the
279      * outgoing video.
280      */
onPause()281     private void onPause() {
282         log("onPause");
283 
284         mIsInBackground = true;
285         if (canVideoPause(mPrimaryCallContext)) {
286             sendRequest(mPrimaryCallContext.getCall(), false);
287         } else {
288             log("onPause, Ignoring...");
289         }
290     }
291 
bringToForeground()292     private void bringToForeground() {
293         if (mInCallPresenter != null) {
294             log("Bringing UI to foreground");
295             mInCallPresenter.bringToForeground(false);
296         } else {
297             loge("InCallPresenter is null. Cannot bring UI to foreground");
298         }
299     }
300 
301     /**
302      * Sends Pause/Resume request.
303      *
304      * @param call Call to be paused/resumed.
305      * @param resume If true resume request will be sent, otherwise pause request.
306      */
sendRequest(Call call, boolean resume)307     private void sendRequest(Call call, boolean resume) {
308         // Check if this call supports pause/un-pause.
309         if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
310             return;
311         }
312 
313         if (resume) {
314             log("sending resume request, call=" + call);
315             call.getVideoCall()
316                     .sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
317         } else {
318             log("sending pause request, call=" + call);
319             call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
320         }
321     }
322 
323     /**
324      * Determines if a given call is the same one stored in a {@link CallContext}.
325      *
326      * @param call The call.
327      * @param callContext The call context.
328      * @return {@code true} if the {@link Call} is the same as the one referenced in the
329      *      {@link CallContext}.
330      */
areSame(Call call, CallContext callContext)331     private static boolean areSame(Call call, CallContext callContext) {
332         if (call == null && callContext == null) {
333             return true;
334         } else if (call == null || callContext == null) {
335             return false;
336         }
337         return call.equals(callContext.getCall());
338     }
339 
340     /**
341      * Determines if a video call can be paused.  Only a video call which is active can be paused.
342      *
343      * @param callContext The call context to check.
344      * @return {@code true} if the call is an active video call.
345      */
canVideoPause(CallContext callContext)346     private static boolean canVideoPause(CallContext callContext) {
347         return isVideoCall(callContext) && callContext.getState() == Call.State.ACTIVE;
348     }
349 
350     /**
351      * Determines if a call referenced by a {@link CallContext} is a video call.
352      *
353      * @param callContext The call context.
354      * @return {@code true} if the call is a video call, {@code false} otherwise.
355      */
isVideoCall(CallContext callContext)356     private static boolean isVideoCall(CallContext callContext) {
357         return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
358     }
359 
360     /**
361      * Determines if call is in incoming/waiting state.
362      *
363      * @param call The call context.
364      * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
365      */
isIncomingCall(CallContext call)366     private static boolean isIncomingCall(CallContext call) {
367         return call != null && isIncomingCall(call.getCall());
368     }
369 
370     /**
371      * Determines if a call is in incoming/waiting state.
372      *
373      * @param call The call.
374      * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
375      */
isIncomingCall(Call call)376     private static boolean isIncomingCall(Call call) {
377         return call != null && (call.getState() == Call.State.CALL_WAITING
378                 || call.getState() == Call.State.INCOMING);
379     }
380 
381     /**
382      * Determines if a call is dialing.
383      *
384      * @param call The call context.
385      * @return {@code true} if the call is dialing, {@code false} otherwise.
386      */
isDialing(CallContext call)387     private static boolean isDialing(CallContext call) {
388         return call != null && Call.State.isDialing(call.getState());
389     }
390 
391     /**
392      * Determines if a call is holding.
393      *
394      * @param call The call context.
395      * @return {@code true} if the call is holding, {@code false} otherwise.
396      */
isHolding(CallContext call)397     private static boolean isHolding(CallContext call) {
398         return call != null && call.getState() == Call.State.ONHOLD;
399     }
400 
401     /**
402      * Logs a debug message.
403      *
404      * @param msg The message.
405      */
log(String msg)406     private void log(String msg) {
407         Log.d(this, TAG + msg);
408     }
409 
410     /**
411      * Logs an error message.
412      *
413      * @param msg The message.
414      */
loge(String msg)415     private void loge(String msg) {
416         Log.e(this, TAG + msg);
417     }
418 }
419