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 
17 package com.android.incallui;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.os.Handler;
22 import android.telecom.AudioState;
23 import android.telecom.CameraCapabilities;
24 import android.telecom.InCallService.VideoCall;
25 import android.view.Surface;
26 
27 import com.android.contacts.common.CallUtil;
28 import com.android.incallui.InCallPresenter.InCallDetailsListener;
29 import com.android.incallui.InCallPresenter.InCallOrientationListener;
30 import com.android.incallui.InCallPresenter.InCallStateListener;
31 import com.android.incallui.InCallPresenter.IncomingCallListener;
32 import com.android.incallui.InCallVideoCallListenerNotifier.SurfaceChangeListener;
33 import com.android.incallui.InCallVideoCallListenerNotifier.VideoEventListener;
34 import com.google.common.base.Preconditions;
35 
36 import 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  */
60 public 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      */
getDeviceOrientation()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      */
init(Context context)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
onUiReady(VideoCallUi ui)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
onUiUnready(VideoCallUi ui)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      */
getVideoCall()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      */
onSurfaceCreated(int surface)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      */
onSurfaceChanged(int surface, int format, int width, int height)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      */
onSurfaceDestroyed(int surface)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      */
onSurfaceClick(int surfaceId)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
onIncomingCall(InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, Call call)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
onStateChange(InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, CallList callList)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
onDetailsChanged(Call call, android.telecom.Call.Details details)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      */
checkForVideoCallChange()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      */
checkForVideoStateChange()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      */
changeVideoCall(VideoCall videoCall)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      */
enterVideoMode()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      */
exitVideoMode()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
onPeerPauseStateChanged(Call call, boolean paused)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
onUpdatePeerDimensions(Call call, int width, int height)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
onCameraDimensionsChange(Call call, int width, int height)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
onDeviceOrientationChanged(int orientation)524     public void onDeviceOrientationChanged(int orientation) {
525         mDeviceOrientation = orientation;
526     }
527 
528     @Override
onUpgradeToVideoRequest(Call call)529     public void onUpgradeToVideoRequest(Call call) {
530         mPrimaryCall.setSessionModificationState(
531                 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
532     }
533 
534     @Override
onUpgradeToVideoSuccess(Call call)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
onUpgradeToVideoFail(Call call)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
onDowngradeToAudio(Call call)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      */
setPreviewSize(int orientation, float aspectRatio)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 {
showVideoUi(boolean show)595         void showVideoUi(boolean show);
isDisplayVideoSurfaceCreated()596         boolean isDisplayVideoSurfaceCreated();
isPreviewVideoSurfaceCreated()597         boolean isPreviewVideoSurfaceCreated();
getDisplayVideoSurface()598         Surface getDisplayVideoSurface();
getPreviewVideoSurface()599         Surface getPreviewVideoSurface();
setPreviewSize(int width, int height)600         void setPreviewSize(int width, int height);
cleanupSurfaces()601         void cleanupSurfaces();
isActivityRestart()602         boolean isActivityRestart();
603     }
604 }
605