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.database.Cursor;
21import android.graphics.Point;
22import android.net.Uri;
23import android.os.AsyncTask;
24import android.os.Handler;
25import android.os.Looper;
26import android.provider.ContactsContract;
27import android.telecom.CallAudioState;
28import android.telecom.Connection;
29import android.telecom.InCallService.VideoCall;
30import android.telecom.VideoProfile;
31import android.telecom.VideoProfile.CameraCapabilities;
32import android.view.Surface;
33import android.widget.ImageView;
34
35import com.android.contacts.common.ContactPhotoManager;
36import com.android.incallui.InCallPresenter.InCallDetailsListener;
37import com.android.incallui.InCallPresenter.InCallOrientationListener;
38import com.android.incallui.InCallPresenter.InCallStateListener;
39import com.android.incallui.InCallPresenter.IncomingCallListener;
40import com.android.incallui.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
41import com.android.incallui.InCallVideoCallCallbackNotifier.VideoEventListener;
42
43import java.util.Objects;
44
45/**
46 * Logic related to the {@link VideoCallFragment} and for managing changes to the video calling
47 * surfaces based on other user interface events and incoming events from the
48 * {@class VideoCallListener}.
49 * <p>
50 * When a call's video state changes to bi-directional video, the
51 * {@link com.android.incallui.VideoCallPresenter} performs the following negotiation with the
52 * telephony layer:
53 * <ul>
54 *     <li>{@code VideoCallPresenter} creates and informs telephony of the display surface.</li>
55 *     <li>{@code VideoCallPresenter} creates the preview surface.</li>
56 *     <li>{@code VideoCallPresenter} informs telephony of the currently selected camera.</li>
57 *     <li>Telephony layer sends {@link CameraCapabilities}, including the
58 *     dimensions of the video for the current camera.</li>
59 *     <li>{@code VideoCallPresenter} adjusts size of the preview surface to match the aspect
60 *     ratio of the camera.</li>
61 *     <li>{@code VideoCallPresenter} informs telephony of the new preview surface.</li>
62 * </ul>
63 * <p>
64 * When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both
65 * surfaces.
66 */
67public class VideoCallPresenter extends Presenter<VideoCallPresenter.VideoCallUi> implements
68        IncomingCallListener, InCallOrientationListener, InCallStateListener,
69        InCallDetailsListener, SurfaceChangeListener, VideoEventListener,
70        InCallVideoCallCallbackNotifier.SessionModificationListener,
71        InCallPresenter.InCallEventListener {
72    public static final String TAG = "VideoCallPresenter";
73
74    public static final boolean DEBUG = false;
75
76    /**
77     * Runnable which is posted to schedule automatically entering fullscreen mode.
78     */
79    private Runnable mAutoFullscreenRunnable =  new Runnable() {
80        @Override
81        public void run() {
82            if (mAutoFullScreenPending) {
83                Log.v(this, "Automatically entering fullscreen mode.");
84                InCallPresenter.getInstance().setFullScreen(true);
85                mAutoFullScreenPending = false;
86            } else {
87                Log.v(this, "Skipping scheduled fullscreen mode.");
88            }
89        }
90    };
91
92    /**
93     * Defines the state of the preview surface negotiation with the telephony layer.
94     */
95    private class PreviewSurfaceState {
96        /**
97         * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet
98         * started.
99         */
100        private static final int NONE = 0;
101
102        /**
103         * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet
104         * been received.
105         */
106        private static final int CAMERA_SET = 1;
107
108        /**
109         * The camera capabilties have been received from telephony, but the surface has not yet
110         * been set on the {@link VideoCall}.
111         */
112        private static final int CAPABILITIES_RECEIVED = 2;
113
114        /**
115         * The surface has been set on the {@link VideoCall}.
116         */
117        private static final int SURFACE_SET = 3;
118    }
119
120    /**
121     * The minimum width or height of the preview surface.  Used when re-sizing the preview surface
122     * to match the aspect ratio of the currently selected camera.
123     */
124    private float mMinimumVideoDimension;
125
126    /**
127     * The current context.
128     */
129    private Context mContext;
130
131    /**
132     * The call the video surfaces are currently related to
133     */
134    private Call mPrimaryCall;
135
136    /**
137     * The {@link VideoCall} used to inform the video telephony layer of changes to the video
138     * surfaces.
139     */
140    private VideoCall mVideoCall;
141
142    /**
143     * Determines if the current UI state represents a video call.
144     */
145    private int mCurrentVideoState;
146
147    /**
148     * Call's current state
149     */
150    private int mCurrentCallState = Call.State.INVALID;
151
152    /**
153     * Determines the device orientation (portrait/lanscape).
154     */
155    private int mDeviceOrientation;
156
157    /**
158     * Tracks the state of the preview surface negotiation with the telephony layer.
159     */
160    private int mPreviewSurfaceState = PreviewSurfaceState.NONE;
161
162    /**
163     * Saves the audio mode which was selected prior to going into a video call.
164     */
165    private static int sPrevVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID;
166
167    private static boolean mIsVideoMode = false;
168
169    /**
170     * Contact photo manager to retrieve cached contact photo information.
171     */
172    private ContactPhotoManager mContactPhotoManager = null;
173
174    /**
175     * The URI for the user's profile photo, or {@code null} if not specified.
176     */
177    private ContactInfoCache.ContactCacheEntry mProfileInfo = null;
178
179    /**
180     * UI thread handler used for delayed task execution.
181     */
182    private Handler mHandler;
183
184    /**
185     * Determines whether video calls should automatically enter full screen mode after
186     * {@link #mAutoFullscreenTimeoutMillis} milliseconds.
187     */
188    private boolean mIsAutoFullscreenEnabled = false;
189
190    /**
191     * Determines the number of milliseconds after which a video call will automatically enter
192     * fullscreen mode.  Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}.
193     */
194    private int mAutoFullscreenTimeoutMillis = 0;
195
196    /**
197     * Determines if the countdown is currently running to automatically enter full screen video
198     * mode.
199     */
200    private boolean mAutoFullScreenPending = false;
201
202    /**
203     * Initializes the presenter.
204     *
205     * @param context The current context.
206     */
207    public void init(Context context) {
208        mContext = context;
209        mMinimumVideoDimension = mContext.getResources().getDimension(
210                R.dimen.video_preview_small_dimension);
211        mHandler = new Handler(Looper.getMainLooper());
212        mIsAutoFullscreenEnabled = mContext.getResources()
213                .getBoolean(R.bool.video_call_auto_fullscreen);
214        mAutoFullscreenTimeoutMillis = mContext.getResources().getInteger(
215                R.integer.video_call_auto_fullscreen_timeout);
216    }
217
218    /**
219     * Called when the user interface is ready to be used.
220     *
221     * @param ui The Ui implementation that is now ready to be used.
222     */
223    @Override
224    public void onUiReady(VideoCallUi ui) {
225        super.onUiReady(ui);
226        Log.d(this, "onUiReady:");
227
228        // Register for call state changes last
229        InCallPresenter.getInstance().addListener(this);
230        InCallPresenter.getInstance().addDetailsListener(this);
231        InCallPresenter.getInstance().addIncomingCallListener(this);
232        InCallPresenter.getInstance().addOrientationListener(this);
233        // To get updates of video call details changes
234        InCallPresenter.getInstance().addDetailsListener(this);
235
236        // Register for surface and video events from {@link InCallVideoCallListener}s.
237        InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
238        InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
239        InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
240        mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
241        mCurrentCallState = Call.State.INVALID;
242    }
243
244    /**
245     * Called when the user interface is no longer ready to be used.
246     *
247     * @param ui The Ui implementation that is no longer ready to be used.
248     */
249    @Override
250    public void onUiUnready(VideoCallUi ui) {
251        super.onUiUnready(ui);
252        Log.d(this, "onUiUnready:");
253
254        InCallPresenter.getInstance().removeListener(this);
255        InCallPresenter.getInstance().removeDetailsListener(this);
256        InCallPresenter.getInstance().removeIncomingCallListener(this);
257        InCallPresenter.getInstance().removeOrientationListener(this);
258
259        InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
260        InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
261        InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
262    }
263
264    /**
265     * Handles the creation of a surface in the {@link VideoCallFragment}.
266     *
267     * @param surface The surface which was created.
268     */
269    public void onSurfaceCreated(int surface) {
270        Log.d(this, "onSurfaceCreated surface=" + surface + " mVideoCall=" + mVideoCall);
271        Log.d(this, "onSurfaceCreated PreviewSurfaceState=" + mPreviewSurfaceState);
272        Log.d(this, "onSurfaceCreated presenter=" + this);
273
274        final VideoCallUi ui = getUi();
275        if (ui == null || mVideoCall == null) {
276            Log.w(this, "onSurfaceCreated: Error bad state VideoCallUi=" + ui + " mVideoCall="
277                    + mVideoCall);
278            return;
279        }
280
281        // If the preview surface has just been created and we have already received camera
282        // capabilities, but not yet set the surface, we will set the surface now.
283        if (surface == VideoCallFragment.SURFACE_PREVIEW ) {
284            if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) {
285                mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
286                mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface());
287            } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()){
288                enableCamera(mVideoCall, true);
289            }
290        } else if (surface == VideoCallFragment.SURFACE_DISPLAY) {
291            mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface());
292        }
293    }
294
295    /**
296     * Handles structural changes (format or size) to a surface.
297     *
298     * @param surface The surface which changed.
299     * @param format The new PixelFormat of the surface.
300     * @param width The new width of the surface.
301     * @param height The new height of the surface.
302     */
303    public void onSurfaceChanged(int surface, int format, int width, int height) {
304        //Do stuff
305    }
306
307    /**
308     * Handles the destruction of a surface in the {@link VideoCallFragment}.
309     * Note: The surface is being released, that is, it is no longer valid.
310     *
311     * @param surface The surface which was destroyed.
312     */
313    public void onSurfaceReleased(int surface) {
314        Log.d(this, "onSurfaceReleased: mSurfaceId=" + surface);
315        if ( mVideoCall == null) {
316            Log.w(this, "onSurfaceReleased: VideoCall is null. mSurfaceId=" +
317                    surface);
318            return;
319        }
320
321        if (surface == VideoCallFragment.SURFACE_DISPLAY) {
322            mVideoCall.setDisplaySurface(null);
323        } else if (surface == VideoCallFragment.SURFACE_PREVIEW) {
324            mVideoCall.setPreviewSurface(null);
325            enableCamera(mVideoCall, false);
326        }
327    }
328
329    /**
330     * Called by {@link VideoCallFragment} when the surface is detached from UI (TextureView).
331     * Note: The surface will be cached by {@link VideoCallFragment}, so we don't immediately
332     * null out incoming video surface.
333     * @see VideoCallPresenter#onSurfaceReleased(int)
334     *
335     * @param surface The surface which was detached.
336     */
337    public void onSurfaceDestroyed(int surface) {
338        Log.d(this, "onSurfaceDestroyed: mSurfaceId=" + surface);
339        if (mVideoCall == null) {
340            return;
341        }
342
343        final boolean isChangingConfigurations =
344                InCallPresenter.getInstance().isChangingConfigurations();
345        Log.d(this, "onSurfaceDestroyed: isChangingConfigurations=" + isChangingConfigurations);
346
347        if (surface == VideoCallFragment.SURFACE_PREVIEW) {
348            if (!isChangingConfigurations) {
349                enableCamera(mVideoCall, false);
350            } else {
351                Log.w(this, "onSurfaceDestroyed: Activity is being destroyed due "
352                        + "to configuration changes. Not closing the camera.");
353            }
354        }
355    }
356
357    /**
358     * Handles clicks on the video surfaces by toggling full screen state.
359     * Informs the {@link InCallPresenter} of the change so that it can inform the
360     * {@link CallCardPresenter} of the change.
361     *
362     * @param surfaceId The video surface receiving the click.
363     */
364    public void onSurfaceClick(int surfaceId) {
365        boolean isFullscreen = InCallPresenter.getInstance().toggleFullscreenMode();
366        Log.v(this, "toggleFullScreen = " + isFullscreen);
367    }
368
369    /**
370     * Handles incoming calls.
371     *
372     * @param oldState The old in call state.
373     * @param newState The new in call state.
374     * @param call The call.
375     */
376    @Override
377    public void onIncomingCall(InCallPresenter.InCallState oldState,
378            InCallPresenter.InCallState newState, Call call) {
379        // same logic should happen as with onStateChange()
380        onStateChange(oldState, newState, CallList.getInstance());
381    }
382
383    /**
384     * Handles state changes (including incoming calls)
385     *
386     * @param newState The in call state.
387     * @param callList The call list.
388     */
389    @Override
390    public void onStateChange(InCallPresenter.InCallState oldState,
391            InCallPresenter.InCallState newState, CallList callList) {
392        Log.d(this, "onStateChange oldState" + oldState + " newState=" + newState +
393                " isVideoMode=" + isVideoMode());
394
395        if (newState == InCallPresenter.InCallState.NO_CALLS) {
396            updateAudioMode(false);
397
398            if (isVideoMode()) {
399                exitVideoMode();
400            }
401
402            cleanupSurfaces();
403        }
404
405        // Determine the primary active call).
406        Call primary = null;
407
408        // Determine the call which is the focus of the user's attention.  In the case of an
409        // incoming call waiting call, the primary call is still the active video call, however
410        // the determination of whether we should be in fullscreen mode is based on the type of the
411        // incoming call, not the active video call.
412        Call currentCall = null;
413
414        if (newState == InCallPresenter.InCallState.INCOMING) {
415            // We don't want to replace active video call (primary call)
416            // with a waiting call, since user may choose to ignore/decline the waiting call and
417            // this should have no impact on current active video call, that is, we should not
418            // change the camera or UI unless the waiting VT call becomes active.
419            primary = callList.getActiveCall();
420            currentCall = callList.getIncomingCall();
421            if (!CallUtils.isActiveVideoCall(primary)) {
422                primary = callList.getIncomingCall();
423            }
424        } else if (newState == InCallPresenter.InCallState.OUTGOING) {
425            currentCall = primary = callList.getOutgoingCall();
426        } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) {
427            currentCall = primary = callList.getPendingOutgoingCall();
428        } else if (newState == InCallPresenter.InCallState.INCALL) {
429            currentCall = primary = callList.getActiveCall();
430        }
431
432        final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary);
433        Log.d(this, "onStateChange primaryChanged=" + primaryChanged);
434        Log.d(this, "onStateChange primary= " + primary);
435        Log.d(this, "onStateChange mPrimaryCall = " + mPrimaryCall);
436        if (primaryChanged) {
437            onPrimaryCallChanged(primary);
438        } else if (mPrimaryCall != null) {
439            updateVideoCall(primary);
440        }
441        updateCallCache(primary);
442
443        // If the call context changed, potentially exit fullscreen or schedule auto enter of
444        // fullscreen mode.
445        // If the current call context is no longer a video call, exit fullscreen mode.
446        maybeExitFullscreen(currentCall);
447        // Schedule auto-enter of fullscreen mode if the current call context is a video call
448        maybeAutoEnterFullscreen(currentCall);
449    }
450
451    /**
452     * Handles a change to the fullscreen mode of the app.
453     *
454     * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise.
455     */
456    @Override
457    public void onFullscreenModeChanged(boolean isFullscreenMode) {
458        cancelAutoFullScreen();
459    }
460
461    private void checkForVideoStateChange(Call call) {
462        final boolean isVideoCall = CallUtils.isVideoCall(call);
463        final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState();
464
465        Log.d(this, "checkForVideoStateChange: isVideoCall= " + isVideoCall
466                + " hasVideoStateChanged=" + hasVideoStateChanged + " isVideoMode="
467                + isVideoMode() + " previousVideoState: " +
468                VideoProfile.videoStateToString(mCurrentVideoState) + " newVideoState: "
469                + VideoProfile.videoStateToString(call.getVideoState()));
470
471        if (!hasVideoStateChanged) {
472            return;
473        }
474
475        updateCameraSelection(call);
476
477        if (isVideoCall) {
478            enterVideoMode(call);
479        } else if (isVideoMode()) {
480            exitVideoMode();
481        }
482    }
483
484    private void checkForCallStateChange(Call call) {
485        final boolean isVideoCall = CallUtils.isVideoCall(call);
486        final boolean hasCallStateChanged = mCurrentCallState != call.getState();
487
488        Log.d(this, "checkForCallStateChange: isVideoCall= " + isVideoCall
489                + " hasCallStateChanged=" +
490                hasCallStateChanged + " isVideoMode=" + isVideoMode());
491
492        if (!hasCallStateChanged) {
493            return;
494        }
495
496        if (isVideoCall) {
497            final InCallCameraManager cameraManager = InCallPresenter.getInstance().
498                    getInCallCameraManager();
499
500            String prevCameraId = cameraManager.getActiveCameraId();
501            updateCameraSelection(call);
502            String newCameraId = cameraManager.getActiveCameraId();
503
504            if (!Objects.equals(prevCameraId, newCameraId) && CallUtils.isActiveVideoCall(call)) {
505                enableCamera(call.getVideoCall(), true);
506            }
507        }
508
509        // Make sure we hide or show the video UI if needed.
510        showVideoUi(call.getVideoState(), call.getState());
511    }
512
513    private void cleanupSurfaces() {
514        final VideoCallUi ui = getUi();
515        if (ui == null) {
516            Log.w(this, "cleanupSurfaces");
517            return;
518        }
519        ui.cleanupSurfaces();
520    }
521
522    private void onPrimaryCallChanged(Call newPrimaryCall) {
523        final boolean isVideoCall = CallUtils.isVideoCall(newPrimaryCall);
524        final boolean isVideoMode = isVideoMode();
525
526        Log.d(this, "onPrimaryCallChanged: isVideoCall=" + isVideoCall + " isVideoMode="
527                + isVideoMode);
528
529        if (!isVideoCall && isVideoMode) {
530            // Terminate video mode if new primary call is not a video call
531            // and we are currently in video mode.
532            Log.d(this, "onPrimaryCallChanged: Exiting video mode...");
533            exitVideoMode();
534        } else if (isVideoCall) {
535            Log.d(this, "onPrimaryCallChanged: Entering video mode...");
536
537            updateCameraSelection(newPrimaryCall);
538            enterVideoMode(newPrimaryCall);
539        }
540    }
541
542    private boolean isVideoMode() {
543        return mIsVideoMode;
544    }
545
546    private void updateCallCache(Call call) {
547        if (call == null) {
548            mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
549            mCurrentCallState = Call.State.INVALID;
550            mVideoCall = null;
551            mPrimaryCall = null;
552        } else {
553            mCurrentVideoState = call.getVideoState();
554            mVideoCall = call.getVideoCall();
555            mCurrentCallState = call.getState();
556            mPrimaryCall = call;
557        }
558    }
559
560    /**
561     * Handles changes to the details of the call.  The {@link VideoCallPresenter} is interested in
562     * changes to the video state.
563     *
564     * @param call The call for which the details changed.
565     * @param details The new call details.
566     */
567    @Override
568    public void onDetailsChanged(Call call, android.telecom.Call.Details details) {
569        Log.d(this, " onDetailsChanged call=" + call + " details=" + details + " mPrimaryCall="
570                + mPrimaryCall);
571        if (call == null) {
572            return;
573        }
574        // If the details change is not for the currently active call no update is required.
575        if (!call.equals(mPrimaryCall)) {
576            Log.d(this," onDetailsChanged: Details not for current active call so returning. ");
577            return;
578        }
579
580        updateVideoCall(call);
581
582        updateCallCache(call);
583    }
584
585    private void updateVideoCall(Call call) {
586        checkForVideoCallChange(call);
587        checkForVideoStateChange(call);
588        checkForCallStateChange(call);
589        checkForOrientationAllowedChange(call);
590    }
591
592    private void checkForOrientationAllowedChange(Call call) {
593        InCallPresenter.getInstance().setInCallAllowsOrientationChange(CallUtils.isVideoCall(call));
594    }
595
596    /**
597     * Checks for a change to the video call and changes it if required.
598     */
599    private void checkForVideoCallChange(Call call) {
600        final VideoCall videoCall = call.getTelecommCall().getVideoCall();
601        Log.d(this, "checkForVideoCallChange: videoCall=" + videoCall + " mVideoCall="
602                + mVideoCall);
603        if (!Objects.equals(videoCall, mVideoCall)) {
604            changeVideoCall(call);
605        }
606    }
607
608    /**
609     * Handles a change to the video call. Sets the surfaces on the previous call to null and sets
610     * the surfaces on the new video call accordingly.
611     *
612     * @param videoCall The new video call.
613     */
614    private void changeVideoCall(Call call) {
615        final VideoCall videoCall = call.getTelecommCall().getVideoCall();
616        Log.d(this, "changeVideoCall to videoCall=" + videoCall + " mVideoCall=" + mVideoCall);
617        // Null out the surfaces on the previous video call.
618        if (mVideoCall != null) {
619            // Log.d(this, "Null out the surfaces on the previous video call.");
620            // mVideoCall.setDisplaySurface(null);
621            // mVideoCall.setPreviewSurface(null);
622        }
623
624        final boolean hasChanged = mVideoCall == null && videoCall != null;
625
626        mVideoCall = videoCall;
627        if (mVideoCall == null || call == null) {
628            Log.d(this, "Video call or primary call is null. Return");
629            return;
630        }
631
632        if (CallUtils.isVideoCall(call) && hasChanged) {
633            enterVideoMode(call);
634        }
635    }
636
637    private static boolean isCameraRequired(int videoState) {
638        return VideoProfile.isBidirectional(videoState) ||
639                VideoProfile.isTransmissionEnabled(videoState);
640    }
641
642    private boolean isCameraRequired() {
643        return mPrimaryCall != null ? isCameraRequired(mPrimaryCall.getVideoState()) : false;
644    }
645
646    /**
647     * Enters video mode by showing the video surfaces and making other adjustments (eg. audio).
648     * TODO(vt): Need to adjust size and orientation of preview surface here.
649     */
650    private void enterVideoMode(Call call) {
651        VideoCall videoCall = call.getVideoCall();
652        int newVideoState = call.getVideoState();
653
654        Log.d(this, "enterVideoMode videoCall= " + videoCall + " videoState: " + newVideoState);
655        VideoCallUi ui = getUi();
656        if (ui == null) {
657            Log.e(this, "Error VideoCallUi is null so returning");
658            return;
659        }
660
661        showVideoUi(newVideoState, call.getState());
662
663        // Communicate the current camera to telephony and make a request for the camera
664        // capabilities.
665        if (videoCall != null) {
666            if (ui.isDisplayVideoSurfaceCreated()) {
667                Log.d(this, "Calling setDisplaySurface with " + ui.getDisplayVideoSurface());
668                videoCall.setDisplaySurface(ui.getDisplayVideoSurface());
669            }
670
671            final int rotation = ui.getCurrentRotation();
672            if (rotation != VideoCallFragment.ORIENTATION_UNKNOWN) {
673                videoCall.setDeviceOrientation(InCallPresenter.toRotationAngle(rotation));
674            }
675
676            enableCamera(videoCall, isCameraRequired(newVideoState));
677        }
678        mCurrentVideoState = newVideoState;
679        updateAudioMode(true);
680
681        mIsVideoMode = true;
682
683        maybeAutoEnterFullscreen(call);
684    }
685
686    //TODO: Move this into Telecom. InCallUI should not be this close to audio functionality.
687    private void updateAudioMode(boolean enableSpeaker) {
688        if (!isSpeakerEnabledForVideoCalls()) {
689            Log.d(this, "Speaker is disabled. Can't update audio mode");
690            return;
691        }
692
693        final TelecomAdapter telecomAdapter = TelecomAdapter.getInstance();
694        final boolean isPrevAudioModeValid =
695            sPrevVideoAudioMode != AudioModeProvider.AUDIO_MODE_INVALID;
696
697        Log.d(this, "Is previous audio mode valid = " + isPrevAudioModeValid + " enableSpeaker is "
698                + enableSpeaker);
699
700        // Set audio mode to previous mode if enableSpeaker is false.
701        if (isPrevAudioModeValid && !enableSpeaker) {
702            telecomAdapter.setAudioRoute(sPrevVideoAudioMode);
703            sPrevVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID;
704            return;
705        }
706
707        int currentAudioMode = AudioModeProvider.getInstance().getAudioMode();
708
709        // Set audio mode to speaker if enableSpeaker is true and bluetooth or headset are not
710        // connected and it's a video call.
711        if (!isAudioRouteEnabled(currentAudioMode,
712            CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET) &&
713            !isPrevAudioModeValid && enableSpeaker && CallUtils.isVideoCall(mPrimaryCall)) {
714            sPrevVideoAudioMode = currentAudioMode;
715
716            Log.d(this, "Routing audio to speaker");
717            telecomAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
718        }
719    }
720
721    private static boolean isSpeakerEnabledForVideoCalls() {
722        // TODO: Make this a carrier configurable setting. For now this is always true. b/20090407
723        return true;
724    }
725
726    private void enableCamera(VideoCall videoCall, boolean isCameraRequired) {
727        Log.d(this, "enableCamera: VideoCall=" + videoCall + " enabling=" + isCameraRequired);
728        if (videoCall == null) {
729            Log.w(this, "enableCamera: VideoCall is null.");
730            return;
731        }
732
733        if (isCameraRequired) {
734            InCallCameraManager cameraManager = InCallPresenter.getInstance().
735                    getInCallCameraManager();
736            videoCall.setCamera(cameraManager.getActiveCameraId());
737            mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET;
738
739            videoCall.requestCameraCapabilities();
740        } else {
741            mPreviewSurfaceState = PreviewSurfaceState.NONE;
742            videoCall.setCamera(null);
743        }
744    }
745
746    /**
747     * Exits video mode by hiding the video surfaces and making other adjustments (eg. audio).
748     */
749    private void exitVideoMode() {
750        Log.d(this, "exitVideoMode");
751
752        showVideoUi(VideoProfile.STATE_AUDIO_ONLY, Call.State.ACTIVE);
753        enableCamera(mVideoCall, false);
754        InCallPresenter.getInstance().setFullScreen(false);
755
756        mIsVideoMode = false;
757    }
758
759    /**
760     * Based on the current video state and call state, show or hide the incoming and
761     * outgoing video surfaces.  The outgoing video surface is shown any time video is transmitting.
762     * The incoming video surface is shown whenever the video is un-paused and active.
763     *
764     * @param videoState The video state.
765     * @param callState The call state.
766     */
767    private void showVideoUi(int videoState, int callState) {
768        VideoCallUi ui = getUi();
769        if (ui == null) {
770            Log.e(this, "showVideoUi, VideoCallUi is null returning");
771            return;
772        }
773        boolean isPaused = VideoProfile.isPaused(videoState);
774        boolean isCallActive = callState == Call.State.ACTIVE;
775        if (VideoProfile.isBidirectional(videoState)) {
776            ui.showVideoViews(true, !isPaused && isCallActive);
777        } else if (VideoProfile.isTransmissionEnabled(videoState)) {
778            ui.showVideoViews(true, false);
779        } else if (VideoProfile.isReceptionEnabled(videoState)) {
780            ui.showVideoViews(false, !isPaused && isCallActive);
781            loadProfilePhotoAsync();
782        } else {
783            ui.hideVideoUi();
784        }
785
786        InCallPresenter.getInstance().enableScreenTimeout(
787                VideoProfile.isAudioOnly(videoState));
788    }
789
790    /**
791     * Handles peer video pause state changes.
792     *
793     * @param call The call which paused or un-pausedvideo transmission.
794     * @param paused {@code True} when the video transmission is paused, {@code false} when video
795     *               transmission resumes.
796     */
797    @Override
798    public void onPeerPauseStateChanged(Call call, boolean paused) {
799        if (!call.equals(mPrimaryCall)) {
800            return;
801        }
802
803        // TODO(vt): Show/hide the peer contact photo.
804    }
805
806    /**
807     * Handles peer video dimension changes.
808     *
809     * @param call The call which experienced a peer video dimension change.
810     * @param width The new peer video width .
811     * @param height The new peer video height.
812     */
813    @Override
814    public void onUpdatePeerDimensions(Call call, int width, int height) {
815        Log.d(this, "onUpdatePeerDimensions: width= " + width + " height= " + height);
816        VideoCallUi ui = getUi();
817        if (ui == null) {
818            Log.e(this, "VideoCallUi is null. Bail out");
819            return;
820        }
821        if (!call.equals(mPrimaryCall)) {
822            Log.e(this, "Current call is not equal to primary call. Bail out");
823            return;
824        }
825
826        // Change size of display surface to match the peer aspect ratio
827        if (width > 0 && height > 0) {
828            setDisplayVideoSize(width, height);
829        }
830    }
831
832    /**
833     * Handles any video quality changes in the call.
834     *
835     * @param call The call which experienced a video quality change.
836     * @param videoQuality The new video call quality.
837     */
838    @Override
839    public void onVideoQualityChanged(Call call, int videoQuality) {
840        // No-op
841    }
842
843    /**
844     * Handles a change to the dimensions of the local camera.  Receiving the camera capabilities
845     * triggers the creation of the video
846     *
847     * @param call The call which experienced the camera dimension change.
848     * @param width The new camera video width.
849     * @param height The new camera video height.
850     */
851    @Override
852    public void onCameraDimensionsChange(Call call, int width, int height) {
853        Log.d(this, "onCameraDimensionsChange call=" + call + " width=" + width + " height="
854                + height);
855        VideoCallUi ui = getUi();
856        if (ui == null) {
857            Log.e(this, "onCameraDimensionsChange ui is null");
858            return;
859        }
860
861        if (!call.equals(mPrimaryCall)) {
862            Log.e(this, "Call is not primary call");
863            return;
864        }
865
866        mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED;
867        changePreviewDimensions(width, height);
868
869        // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}.
870        // If it not yet ready, it will be set when when creation completes.
871        if (ui.isPreviewVideoSurfaceCreated()) {
872            mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
873            mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface());
874        }
875    }
876
877    /**
878     * Changes the dimensions of the preview surface.
879     *
880     * @param width The new width.
881     * @param height The new height.
882     */
883    private void changePreviewDimensions(int width, int height) {
884        VideoCallUi ui = getUi();
885        if (ui == null) {
886            return;
887        }
888
889        // Resize the surface used to display the preview video
890        ui.setPreviewSurfaceSize(width, height);
891
892        // Configure the preview surface to the correct aspect ratio.
893        float aspectRatio = 1.0f;
894        if (width > 0 && height > 0) {
895            aspectRatio = (float) width / (float) height;
896        }
897
898        // Resize the textureview housing the preview video and rotate it appropriately based on
899        // the device orientation
900        setPreviewSize(mDeviceOrientation, aspectRatio);
901    }
902
903    /**
904     * Called when call session event is raised.
905     *
906     * @param event The call session event.
907     */
908    @Override
909    public void onCallSessionEvent(int event) {
910        StringBuilder sb = new StringBuilder();
911        sb.append("onCallSessionEvent = ");
912
913        switch (event) {
914            case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
915                sb.append("rx_pause");
916                break;
917            case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
918                sb.append("rx_resume");
919                break;
920            case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
921                sb.append("camera_failure");
922                break;
923            case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
924                sb.append("camera_ready");
925                break;
926            default:
927                sb.append("unknown event = ");
928                sb.append(event);
929                break;
930        }
931        Log.d(this, sb.toString());
932    }
933
934    /**
935     * Handles a change to the call data usage
936     *
937     * @param dataUsage call data usage value
938     */
939    @Override
940    public void onCallDataUsageChange(long dataUsage) {
941        Log.d(this, "onCallDataUsageChange dataUsage=" + dataUsage);
942    }
943
944    /**
945     * Handles changes to the device orientation.
946     *
947     * @param orientation The device orientation (one of: {@link Surface#ROTATION_0},
948     *      {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180},
949     *      {@link Surface#ROTATION_270}).
950     */
951    @Override
952    public void onDeviceOrientationChanged(int orientation) {
953        mDeviceOrientation = orientation;
954        Point previewDimensions = getUi().getPreviewSize();
955        if (previewDimensions == null) {
956            return;
957        }
958        Log.d(this, "onDeviceOrientationChanged: orientation=" + orientation + " size: "
959                + previewDimensions);
960        changePreviewDimensions(previewDimensions.x, previewDimensions.y);
961    }
962
963    @Override
964    public void onUpgradeToVideoRequest(Call call, int videoState) {
965        Log.d(this, "onUpgradeToVideoRequest call = " + call + " new video state = " + videoState);
966        if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) {
967            Log.w(this, "UpgradeToVideoRequest received for non-primary call");
968        }
969
970        if (call == null) {
971            return;
972        }
973
974        call.setSessionModificationTo(videoState);
975    }
976
977    @Override
978    public void onUpgradeToVideoSuccess(Call call) {
979        Log.d(this, "onUpgradeToVideoSuccess call=" + call);
980        if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) {
981            Log.w(this, "UpgradeToVideoSuccess received for non-primary call");
982        }
983
984        if (call == null) {
985            return;
986        }
987    }
988
989    @Override
990    public void onUpgradeToVideoFail(int status, Call call) {
991        Log.d(this, "onUpgradeToVideoFail call=" + call);
992        if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) {
993            Log.w(this, "UpgradeToVideoFail received for non-primary call");
994        }
995
996        if (call == null) {
997            return;
998        }
999    }
1000
1001    @Override
1002    public void onDowngradeToAudio(Call call) {
1003        call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
1004        // exit video mode
1005        exitVideoMode();
1006    }
1007
1008    /**
1009     * Sets the preview surface size based on the current device orientation.
1010     *
1011     * @param orientation The device orientation (one of: {@link Surface#ROTATION_0},
1012     *      {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180},
1013     *      {@link Surface#ROTATION_270}).
1014     * @param aspectRatio The aspect ratio of the camera (width / height).
1015     */
1016    private void setPreviewSize(int orientation, float aspectRatio) {
1017        VideoCallUi ui = getUi();
1018        if (ui == null) {
1019            return;
1020        }
1021
1022        int height;
1023        int width;
1024
1025        if (orientation == Surface.ROTATION_90 || orientation == Surface.ROTATION_270) {
1026            // Landscape or reverse landscape orientation.
1027            width = (int) (mMinimumVideoDimension * aspectRatio);
1028            height = (int) mMinimumVideoDimension;
1029        } else {
1030            // Portrait or reverse portrait orientation.
1031            width = (int) mMinimumVideoDimension;
1032            height = (int) (mMinimumVideoDimension * aspectRatio);
1033        }
1034        ui.setPreviewSize(width, height);
1035    }
1036
1037    /**
1038     * Sets the display video surface size based on peer width and height
1039     *
1040     * @param width peer width
1041     * @param height peer height
1042     */
1043    private void setDisplayVideoSize(int width, int height) {
1044        Log.d(this, "setDisplayVideoSize:Received peer width=" + width + " peer height=" + height);
1045        VideoCallUi ui = getUi();
1046        if (ui == null) {
1047            return;
1048        }
1049
1050        // Get current display size
1051        Point size = ui.getScreenSize();
1052        Log.d("VideoCallPresenter", "setDisplayVideoSize: windowmgr width=" + size.x
1053                + " windowmgr height=" + size.y);
1054        if (size.y * width > size.x * height) {
1055            // current display height is too much. Correct it
1056            size.y = (int) (size.x * height / width);
1057        } else if (size.y * width < size.x * height) {
1058            // current display width is too much. Correct it
1059            size.x = (int) (size.y * width / height);
1060        }
1061        ui.setDisplayVideoSize(size.x, size.y);
1062    }
1063
1064    /**
1065     * Exits fullscreen mode if the current call context has changed to a non-video call.
1066     *
1067     * @param call The call.
1068     */
1069    protected void maybeExitFullscreen(Call call) {
1070        if (call == null) {
1071            return;
1072        }
1073
1074        if (!CallUtils.isVideoCall(call) || call.getState() == Call.State.INCOMING) {
1075            InCallPresenter.getInstance().setFullScreen(false);
1076        }
1077    }
1078
1079    /**
1080     * Schedules auto-entering of fullscreen mode.
1081     * Will not enter full screen mode if any of the following conditions are met:
1082     * 1. No call
1083     * 2. Call is not active
1084     * 3. Call is not video call
1085     * 4. Already in fullscreen mode
1086     *
1087     * @param call The current call.
1088     */
1089    protected void maybeAutoEnterFullscreen(Call call) {
1090        if (!mIsAutoFullscreenEnabled) {
1091            return;
1092        }
1093
1094        if (call == null || (
1095                call != null && (call.getState() != Call.State.ACTIVE ||
1096                        !CallUtils.isVideoCall(call)) ||
1097                        InCallPresenter.getInstance().isFullscreen())) {
1098            // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
1099            cancelAutoFullScreen();
1100            return;
1101        }
1102
1103        if (mAutoFullScreenPending) {
1104            Log.v(this, "maybeAutoEnterFullscreen : already pending.");
1105            return;
1106        }
1107        Log.v(this, "maybeAutoEnterFullscreen : scheduled");
1108        mAutoFullScreenPending = true;
1109        mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis);
1110    }
1111
1112    /**
1113     * Cancels pending auto fullscreen mode.
1114     */
1115    public void cancelAutoFullScreen() {
1116        if (!mAutoFullScreenPending) {
1117            Log.v(this, "cancelAutoFullScreen : none pending.");
1118            return;
1119        }
1120        Log.v(this, "cancelAutoFullScreen : cancelling pending");
1121        mAutoFullScreenPending = false;
1122    }
1123
1124    private static boolean isAudioRouteEnabled(int audioRoute, int audioRouteMask) {
1125        return ((audioRoute & audioRouteMask) != 0);
1126    }
1127
1128    private static void updateCameraSelection(Call call) {
1129        Log.d(TAG, "updateCameraSelection: call=" + call);
1130        Log.d(TAG, "updateCameraSelection: call=" + toSimpleString(call));
1131
1132        final Call activeCall = CallList.getInstance().getActiveCall();
1133        int cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
1134
1135        // this function should never be called with null call object, however if it happens we
1136        // should handle it gracefully.
1137        if (call == null) {
1138            cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
1139            com.android.incallui.Log.e(TAG, "updateCameraSelection: Call object is null."
1140                    + " Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
1141        }
1142
1143        // Clear camera direction if this is not a video call.
1144        else if (CallUtils.isAudioCall(call)) {
1145            cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
1146            call.getVideoSettings().setCameraDir(cameraDir);
1147        }
1148
1149        // If this is a waiting video call, default to active call's camera,
1150        // since we don't want to change the current camera for waiting call
1151        // without user's permission.
1152        else if (CallUtils.isVideoCall(activeCall) && CallUtils.isIncomingVideoCall(call)) {
1153            cameraDir = activeCall.getVideoSettings().getCameraDir();
1154        }
1155
1156        // Infer the camera direction from the video state and store it,
1157        // if this is an outgoing video call.
1158        else if (CallUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call) ) {
1159            cameraDir = toCameraDirection(call.getVideoState());
1160            call.getVideoSettings().setCameraDir(cameraDir);
1161        }
1162
1163        // Use the stored camera dir if this is an outgoing video call for which camera direction
1164        // is set.
1165        else if (CallUtils.isOutgoingVideoCall(call)) {
1166            cameraDir = call.getVideoSettings().getCameraDir();
1167        }
1168
1169        // Infer the camera direction from the video state and store it,
1170        // if this is an active video call and camera direction is not set.
1171        else if (CallUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
1172            cameraDir = toCameraDirection(call.getVideoState());
1173            call.getVideoSettings().setCameraDir(cameraDir);
1174        }
1175
1176        // Use the stored camera dir if this is an active video call for which camera direction
1177        // is set.
1178        else if (CallUtils.isActiveVideoCall(call)) {
1179            cameraDir = call.getVideoSettings().getCameraDir();
1180        }
1181
1182        // For all other cases infer the camera direction but don't store it in the call object.
1183        else {
1184            cameraDir = toCameraDirection(call.getVideoState());
1185        }
1186
1187        com.android.incallui.Log.d(TAG, "updateCameraSelection: Setting camera direction to " +
1188                cameraDir + " Call=" + call);
1189        final InCallCameraManager cameraManager = InCallPresenter.getInstance().
1190                getInCallCameraManager();
1191        cameraManager.setUseFrontFacingCamera(cameraDir ==
1192                Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
1193    }
1194
1195    private static int toCameraDirection(int videoState) {
1196        return VideoProfile.isTransmissionEnabled(videoState) &&
1197                !VideoProfile.isBidirectional(videoState)
1198                ? Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING
1199                : Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
1200    }
1201
1202    private static boolean isCameraDirectionSet(Call call) {
1203        return CallUtils.isVideoCall(call) && call.getVideoSettings().getCameraDir()
1204                    != Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
1205    }
1206
1207    private static String toSimpleString(Call call) {
1208        return call == null ? null : call.toSimpleString();
1209    }
1210
1211    /**
1212     * Starts an asynchronous load of the user's profile photo.
1213     */
1214    public void loadProfilePhotoAsync() {
1215        final VideoCallUi ui = getUi();
1216        if (ui == null) {
1217            return;
1218        }
1219
1220        final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
1221            /**
1222             * Performs asynchronous load of the user profile information.
1223             *
1224             * @param params The parameters of the task.
1225             *
1226             * @return {@code null}.
1227             */
1228            @Override
1229            protected Void doInBackground(Void... params) {
1230                if (mProfileInfo == null) {
1231                    // Try and read the photo URI from the local profile.
1232                    mProfileInfo = new ContactInfoCache.ContactCacheEntry();
1233                    final Cursor cursor = mContext.getContentResolver().query(
1234                            ContactsContract.Profile.CONTENT_URI, new String[]{
1235                                    ContactsContract.CommonDataKinds.Phone._ID,
1236                                    ContactsContract.CommonDataKinds.Phone.PHOTO_URI,
1237                                    ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY,
1238                                    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
1239                            }, null, null, null);
1240                    if (cursor != null) {
1241                        try {
1242                            if (cursor.moveToFirst()) {
1243                                mProfileInfo.lookupKey = cursor.getString(cursor.getColumnIndex(
1244                                        ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY));
1245                                String photoUri = cursor.getString(cursor.getColumnIndex(
1246                                        ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
1247                                mProfileInfo.displayPhotoUri = photoUri == null ? null
1248                                        : Uri.parse(photoUri);
1249                                mProfileInfo.name = cursor.getString(cursor.getColumnIndex(
1250                                        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
1251                            }
1252                        } finally {
1253                            cursor.close();
1254                        }
1255                    }
1256                }
1257                return null;
1258            }
1259
1260            @Override
1261            protected void onPostExecute(Void result) {
1262                // If user profile information was found, issue an async request to load the user's
1263                // profile photo.
1264                if (mProfileInfo != null) {
1265                    if (mContactPhotoManager == null) {
1266                        mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
1267                    }
1268                    ContactPhotoManager.DefaultImageRequest imageRequest = (mProfileInfo != null)
1269                            ? null :
1270                            new ContactPhotoManager.DefaultImageRequest(mProfileInfo.name,
1271                                    mProfileInfo.lookupKey, false /* isCircularPhoto */);
1272
1273                    ImageView photoView = ui.getPreviewPhotoView();
1274                    if (photoView == null) {
1275                        return;
1276                    }
1277                    mContactPhotoManager.loadDirectoryPhoto(photoView,
1278                                    mProfileInfo.displayPhotoUri,
1279                                    false /* darkTheme */, false /* isCircular */, imageRequest);
1280                }
1281            }
1282        };
1283
1284        task.execute();
1285    }
1286
1287    /**
1288     * Defines the VideoCallUI interactions.
1289     */
1290    public interface VideoCallUi extends Ui {
1291        void showVideoViews(boolean showPreview, boolean showIncoming);
1292        void hideVideoUi();
1293        boolean isDisplayVideoSurfaceCreated();
1294        boolean isPreviewVideoSurfaceCreated();
1295        Surface getDisplayVideoSurface();
1296        Surface getPreviewVideoSurface();
1297        int getCurrentRotation();
1298        void setPreviewSize(int width, int height);
1299        void setPreviewSurfaceSize(int width, int height);
1300        void setDisplayVideoSize(int width, int height);
1301        Point getScreenSize();
1302        Point getPreviewSize();
1303        void cleanupSurfaces();
1304        ImageView getPreviewPhotoView();
1305    }
1306}
1307