Home | History | Annotate | Download | only in incallui
      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      */
     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