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