1/*
2 * Copyright (C) 2014 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
17package com.android.incallui;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.os.Handler;
22import android.telecom.AudioState;
23import android.telecom.CameraCapabilities;
24import android.telecom.InCallService.VideoCall;
25import android.view.Surface;
26
27import com.android.contacts.common.CallUtil;
28import com.android.incallui.InCallPresenter.InCallDetailsListener;
29import com.android.incallui.InCallPresenter.InCallOrientationListener;
30import com.android.incallui.InCallPresenter.InCallStateListener;
31import com.android.incallui.InCallPresenter.IncomingCallListener;
32import com.android.incallui.InCallVideoCallListenerNotifier.SurfaceChangeListener;
33import com.android.incallui.InCallVideoCallListenerNotifier.VideoEventListener;
34import com.google.common.base.Preconditions;
35
36import java.util.Objects;
37
38/**
39 * Logic related to the {@link VideoCallFragment} and for managing changes to the video calling
40 * surfaces based on other user interface events and incoming events from the
41 * {@class VideoCallListener}.
42 * <p>
43 * When a call's video state changes to bi-directional video, the
44 * {@link com.android.incallui.VideoCallPresenter} performs the following negotiation with the
45 * telephony layer:
46 * <ul>
47 *     <li>{@code VideoCallPresenter} creates and informs telephony of the display surface.</li>
48 *     <li>{@code VideoCallPresenter} creates the preview surface.</li>
49 *     <li>{@code VideoCallPresenter} informs telephony of the currently selected camera.</li>
50 *     <li>Telephony layer sends {@link CameraCapabilities}, including the
51 *     dimensions of the video for the current camera.</li>
52 *     <li>{@code VideoCallPresenter} adjusts size of the preview surface to match the aspect
53 *     ratio of the camera.</li>
54 *     <li>{@code VideoCallPresenter} informs telephony of the new preview surface.</li>
55 * </ul>
56 * <p>
57 * When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both
58 * surfaces.
59 */
60public class VideoCallPresenter extends Presenter<VideoCallPresenter.VideoCallUi>  implements
61        IncomingCallListener, InCallOrientationListener, InCallStateListener,
62        InCallDetailsListener, SurfaceChangeListener, VideoEventListener,
63        InCallVideoCallListenerNotifier.SessionModificationListener {
64
65    /**
66     * Determines the device orientation (portrait/lanscape).
67     */
68    public int getDeviceOrientation() {
69        return mDeviceOrientation;
70    }
71
72    /**
73     * Defines the state of the preview surface negotiation with the telephony layer.
74     */
75    private class PreviewSurfaceState {
76        /**
77         * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet
78         * started.
79         */
80        private static final int NONE = 0;
81
82        /**
83         * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet
84         * been received.
85         */
86        private static final int CAMERA_SET = 1;
87
88        /**
89         * The camera capabilties have been received from telephony, but the surface has not yet
90         * been set on the {@link VideoCall}.
91         */
92        private static final int CAPABILITIES_RECEIVED = 2;
93
94        /**
95         * The surface has been set on the {@link VideoCall}.
96         */
97        private static final int SURFACE_SET = 3;
98    }
99
100    /**
101     * The minimum width or height of the preview surface.  Used when re-sizing the preview surface
102     * to match the aspect ratio of the currently selected camera.
103     */
104    private float mMinimumVideoDimension;
105
106    /**
107     * The current context.
108     */
109    private Context mContext;
110
111    /**
112     * The call the video surfaces are currently related to
113     */
114    private Call mPrimaryCall;
115
116    /**
117     * The {@link VideoCall} used to inform the video telephony layer of changes to the video
118     * surfaces.
119     */
120    private VideoCall mVideoCall;
121
122    /**
123     * Determines if the current UI state represents a video call.
124     */
125    private boolean mIsVideoCall;
126
127    /**
128     * Determines the device orientation (portrait/lanscape).
129     */
130    private int mDeviceOrientation;
131
132    /**
133     * Tracks the state of the preview surface negotiation with the telephony layer.
134     */
135    private int mPreviewSurfaceState = PreviewSurfaceState.NONE;
136
137    /**
138     * Determines whether the video surface is in full-screen mode.
139     */
140    private boolean mIsFullScreen = false;
141
142    /**
143     * Saves the audio mode which was selected prior to going into a video call.
144     */
145    private int mPreVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID;
146
147    /** Handler which resets request state to NO_REQUEST after an interval. */
148    private Handler mSessionModificationResetHandler;
149    private static final long SESSION_MODIFICATION_RESET_DELAY_MS = 3000;
150
151    /**
152     * Initializes the presenter.
153     *
154     * @param context The current context.
155     */
156    public void init(Context context) {
157        mContext = Preconditions.checkNotNull(context);
158        mMinimumVideoDimension = mContext.getResources().getDimension(
159                R.dimen.video_preview_small_dimension);
160        mSessionModificationResetHandler = new Handler();
161    }
162
163    /**
164     * Called when the user interface is ready to be used.
165     *
166     * @param ui The Ui implementation that is now ready to be used.
167     */
168    @Override
169    public void onUiReady(VideoCallUi ui) {
170        super.onUiReady(ui);
171
172        // Register for call state changes last
173        InCallPresenter.getInstance().addListener(this);
174        InCallPresenter.getInstance().addIncomingCallListener(this);
175        InCallPresenter.getInstance().addOrientationListener(this);
176
177        // Register for surface and video events from {@link InCallVideoCallListener}s.
178        InCallVideoCallListenerNotifier.getInstance().addSurfaceChangeListener(this);
179        InCallVideoCallListenerNotifier.getInstance().addVideoEventListener(this);
180        InCallVideoCallListenerNotifier.getInstance().addSessionModificationListener(this);
181        mIsVideoCall = false;
182    }
183
184    /**
185     * Called when the user interface is no longer ready to be used.
186     *
187     * @param ui The Ui implementation that is no longer ready to be used.
188     */
189    @Override
190    public void onUiUnready(VideoCallUi ui) {
191        super.onUiUnready(ui);
192
193        InCallPresenter.getInstance().removeListener(this);
194        InCallPresenter.getInstance().removeIncomingCallListener(this);
195        InCallPresenter.getInstance().removeOrientationListener(this);
196        InCallVideoCallListenerNotifier.getInstance().removeSurfaceChangeListener(this);
197        InCallVideoCallListenerNotifier.getInstance().removeVideoEventListener(this);
198        InCallVideoCallListenerNotifier.getInstance().removeSessionModificationListener(this);
199    }
200
201    /**
202     * @return The {@link VideoCall}.
203     */
204    private VideoCall getVideoCall() {
205        return mVideoCall;
206    }
207
208    /**
209     * Handles the creation of a surface in the {@link VideoCallFragment}.
210     *
211     * @param surface The surface which was created.
212     */
213    public void onSurfaceCreated(int surface) {
214        final VideoCallUi ui = getUi();
215
216        if (ui == null || mVideoCall == null) {
217            return;
218        }
219
220        // If the preview surface has just been created and we have already received camera
221        // capabilities, but not yet set the surface, we will set the surface now.
222        if (surface == VideoCallFragment.SURFACE_PREVIEW &&
223                mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) {
224
225            mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
226            mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface());
227        } else if (surface == VideoCallFragment.SURFACE_DISPLAY) {
228            mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface());
229        }
230    }
231
232    /**
233     * Handles structural changes (format or size) to a surface.
234     *
235     * @param surface The surface which changed.
236     * @param format The new PixelFormat of the surface.
237     * @param width The new width of the surface.
238     * @param height The new height of the surface.
239     */
240    public void onSurfaceChanged(int surface, int format, int width, int height) {
241        //Do stuff
242    }
243
244    /**
245     * Handles the destruction of a surface in the {@link VideoCallFragment}.
246     *
247     * @param surface The surface which was destroyed.
248     */
249    public void onSurfaceDestroyed(int surface) {
250        final VideoCallUi ui = getUi();
251        if (ui == null || mVideoCall == null) {
252            return;
253        }
254
255        if (surface == VideoCallFragment.SURFACE_DISPLAY) {
256            mVideoCall.setDisplaySurface(null);
257        } else if (surface == VideoCallFragment.SURFACE_PREVIEW) {
258            mVideoCall.setPreviewSurface(null);
259        }
260    }
261
262    /**
263     * Handles clicks on the video surfaces by toggling full screen state.
264     * Informs the {@link InCallPresenter} of the change so that it can inform the
265     * {@link CallCardPresenter} of the change.
266     *
267     * @param surfaceId The video surface receiving the click.
268     */
269    public void onSurfaceClick(int surfaceId) {
270        mIsFullScreen = !mIsFullScreen;
271        InCallPresenter.getInstance().setFullScreenVideoState(mIsFullScreen);
272    }
273
274
275    /**
276     * Handles incoming calls.
277     *
278     * @param state The in call state.
279     * @param call The call.
280     */
281    @Override
282    public void onIncomingCall(InCallPresenter.InCallState oldState,
283            InCallPresenter.InCallState newState, Call call) {
284        // same logic should happen as with onStateChange()
285        onStateChange(oldState, newState, CallList.getInstance());
286    }
287
288    /**
289     * Handles state changes (including incoming calls)
290     *
291     * @param newState The in call state.
292     * @param callList The call list.
293     */
294    @Override
295    public void onStateChange(InCallPresenter.InCallState oldState,
296            InCallPresenter.InCallState newState, CallList callList) {
297        // Bail if video calling is disabled for the device.
298        if (!CallUtil.isVideoEnabled(mContext)) {
299            return;
300        }
301
302        if (newState == InCallPresenter.InCallState.NO_CALLS) {
303            exitVideoMode();
304        }
305
306        // Determine the primary active call).
307        Call primary = null;
308        if (newState == InCallPresenter.InCallState.INCOMING) {
309            primary = callList.getIncomingCall();
310        } else if (newState == InCallPresenter.InCallState.OUTGOING) {
311            primary = callList.getOutgoingCall();
312        } else if (newState == InCallPresenter.InCallState.INCALL) {
313            primary = callList.getActiveCall();
314        }
315
316        final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary);
317        if (primaryChanged) {
318            mPrimaryCall = primary;
319
320            if (primary != null) {
321                checkForVideoCallChange();
322                mIsVideoCall = mPrimaryCall.isVideoCall(mContext);
323                if (mIsVideoCall) {
324                    enterVideoMode();
325                } else {
326                    exitVideoMode();
327                }
328            } else if (primary == null) {
329                // If no primary call, ensure we exit video state and clean up the video surfaces.
330                exitVideoMode();
331            }
332        }
333    }
334
335    /**
336     * Handles changes to the details of the call.  The {@link VideoCallPresenter} is interested in
337     * changes to the video state.
338     *
339     * @param call The call for which the details changed.
340     * @param details The new call details.
341     */
342    @Override
343    public void onDetailsChanged(Call call, android.telecom.Call.Details details) {
344        // If the details change is not for the currently active call no update is required.
345        if (!call.equals(mPrimaryCall)) {
346            return;
347        }
348
349        checkForVideoStateChange();
350    }
351
352    /**
353     * Checks for a change to the video call and changes it if required.
354     */
355    private void checkForVideoCallChange() {
356        VideoCall videoCall = mPrimaryCall.getTelecommCall().getVideoCall();
357        if (!Objects.equals(videoCall, mVideoCall)) {
358            changeVideoCall(videoCall);
359        }
360    }
361
362    /**
363     * Checks to see if the current video state has changed and updates the UI if required.
364     */
365    private void checkForVideoStateChange() {
366        boolean newVideoState = mPrimaryCall.isVideoCall(mContext);
367
368        // Check if video state changed
369        if (mIsVideoCall != newVideoState) {
370            mIsVideoCall = newVideoState;
371
372            if (mIsVideoCall) {
373                enterVideoMode();
374            } else {
375                exitVideoMode();
376            }
377        }
378    }
379
380    /**
381     * Handles a change to the video call.  Sets the surfaces on the previous call to null and sets
382     * the surfaces on the new video call accordingly.
383     *
384     * @param videoCall The new video call.
385     */
386    private void changeVideoCall(VideoCall videoCall) {
387        // Null out the surfaces on the previous video call.
388        if (mVideoCall != null) {
389            mVideoCall.setDisplaySurface(null);
390            mVideoCall.setPreviewSurface(null);
391        }
392
393        mVideoCall = videoCall;
394    }
395
396    /**
397     * Enters video mode by showing the video surfaces and making other adjustments (eg. audio).
398     * TODO(vt): Need to adjust size and orientation of preview surface here.
399     */
400    private void enterVideoMode() {
401        VideoCallUi ui = getUi();
402        if (ui == null) {
403            return;
404        }
405
406        ui.showVideoUi(true);
407        InCallPresenter.getInstance().setInCallAllowsOrientationChange(true);
408
409        // Communicate the current camera to telephony and make a request for the camera
410        // capabilities.
411        if (mVideoCall != null) {
412            // Do not reset the surfaces if we just restarted the activity due to an orientation
413            // change.
414            if (ui.isActivityRestart()) {
415                return;
416            }
417
418            mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET;
419            InCallCameraManager cameraManager = InCallPresenter.getInstance().
420                    getInCallCameraManager();
421            mVideoCall.setCamera(cameraManager.getActiveCameraId());
422            mVideoCall.requestCameraCapabilities();
423
424            if (ui.isDisplayVideoSurfaceCreated()) {
425                mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface());
426            }
427        }
428
429        mPreVideoAudioMode = AudioModeProvider.getInstance().getAudioMode();
430        TelecomAdapter.getInstance().setAudioRoute(AudioState.ROUTE_SPEAKER);
431    }
432
433    /**
434     * Exits video mode by hiding the video surfaces  and making other adjustments (eg. audio).
435     */
436    private void exitVideoMode() {
437        VideoCallUi ui = getUi();
438        if (ui == null) {
439            return;
440        }
441        InCallPresenter.getInstance().setInCallAllowsOrientationChange(false);
442        ui.showVideoUi(false);
443
444        if (mPreVideoAudioMode != AudioModeProvider.AUDIO_MODE_INVALID) {
445            TelecomAdapter.getInstance().setAudioRoute(mPreVideoAudioMode);
446            mPreVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID;
447        }
448    }
449
450    /**
451     * Handles peer video pause state changes.
452     *
453     * @param call The call which paused or un-pausedvideo transmission.
454     * @param paused {@code True} when the video transmission is paused, {@code false} when video
455     *               transmission resumes.
456     */
457    @Override
458    public void onPeerPauseStateChanged(Call call, boolean paused) {
459        if (!call.equals(mPrimaryCall)) {
460            return;
461        }
462
463        // TODO(vt): Show/hide the peer contact photo.
464    }
465
466    /**
467     * Handles peer video dimension changes.
468     *
469     * @param call The call which experienced a peer video dimension change.
470     * @param width The new peer video width .
471     * @param height The new peer video height.
472     */
473    @Override
474    public void onUpdatePeerDimensions(Call call, int width, int height) {
475        if (!call.equals(mPrimaryCall)) {
476            return;
477        }
478
479        // TODO(vt): Change display surface aspect ratio.
480    }
481
482    /**
483     * Handles a change to the dimensions of the local camera.  Receiving the camera capabilities
484     * triggers the creation of the video
485     *
486     * @param call The call which experienced the camera dimension change.
487     * @param width The new camera video width.
488     * @param height The new camera video height.
489     */
490    @Override
491    public void onCameraDimensionsChange(Call call, int width, int height) {
492        VideoCallUi ui = getUi();
493        if (ui == null) {
494            return;
495        }
496
497        if (!call.equals(mPrimaryCall)) {
498            return;
499        }
500
501        mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED;
502
503        // Configure the preview surface to the correct aspect ratio.
504        float aspectRatio = 1.0f;
505        if (width > 0 && height > 0) {
506            aspectRatio = (float) width / (float) height;
507        }
508        setPreviewSize(mDeviceOrientation, aspectRatio);
509
510        // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}.
511        // If it not yet ready, it will be set when when creation completes.
512        if (ui.isPreviewVideoSurfaceCreated()) {
513            mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
514            mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface());
515        }
516    }
517
518    /**
519     * Handles hanges to the device orientation.
520     * See: {@link Configuration.ORIENTATION_LANDSCAPE}, {@link Configuration.ORIENTATION_PORTRAIT}
521     * @param orientation The device orientation.
522     */
523    @Override
524    public void onDeviceOrientationChanged(int orientation) {
525        mDeviceOrientation = orientation;
526    }
527
528    @Override
529    public void onUpgradeToVideoRequest(Call call) {
530        mPrimaryCall.setSessionModificationState(
531                Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
532    }
533
534    @Override
535    public void onUpgradeToVideoSuccess(Call call) {
536        if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) {
537            return;
538        }
539
540        mPrimaryCall.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
541    }
542
543    @Override
544    public void onUpgradeToVideoFail(Call call) {
545        if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) {
546            return;
547        }
548
549        call.setSessionModificationState(Call.SessionModificationState.REQUEST_FAILED);
550
551        // Start handler to change state from REQUEST_FAILED to NO_REQUEST after an interval.
552        mSessionModificationResetHandler.postDelayed(new Runnable() {
553            @Override
554            public void run() {
555                mPrimaryCall.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
556            }
557        }, SESSION_MODIFICATION_RESET_DELAY_MS);
558    }
559
560    @Override
561    public void onDowngradeToAudio(Call call) {
562        // Implementing to satsify interface.
563    }
564
565    /**
566     * Sets the preview surface size based on the current device orientation.
567     * See: {@link Configuration.ORIENTATION_LANDSCAPE}, {@link Configuration.ORIENTATION_PORTRAIT}
568     *
569     * @param orientation The device orientation.
570     * @param aspectRatio The aspect ratio of the camera (width / height).
571     */
572    private void setPreviewSize(int orientation, float aspectRatio) {
573        VideoCallUi ui = getUi();
574        if (ui == null) {
575            return;
576        }
577
578        int height;
579        int width;
580
581        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
582            width = (int) (mMinimumVideoDimension * aspectRatio);
583            height = (int) mMinimumVideoDimension;
584        } else {
585            width = (int) mMinimumVideoDimension;
586            height = (int) (mMinimumVideoDimension * aspectRatio);
587        }
588        ui.setPreviewSize(width, height);
589    }
590
591    /**
592     * Defines the VideoCallUI interactions.
593     */
594    public interface VideoCallUi extends Ui {
595        void showVideoUi(boolean show);
596        boolean isDisplayVideoSurfaceCreated();
597        boolean isPreviewVideoSurfaceCreated();
598        Surface getDisplayVideoSurface();
599        Surface getPreviewVideoSurface();
600        void setPreviewSize(int width, int height);
601        void cleanupSurfaces();
602        boolean isActivityRestart();
603    }
604}
605