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.res.Configuration;
     20 import android.graphics.Point;
     21 import android.graphics.SurfaceTexture;
     22 import android.os.Bundle;
     23 import android.view.Display;
     24 import android.view.LayoutInflater;
     25 import android.view.Surface;
     26 import android.view.TextureView;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.ViewStub;
     30 import android.view.ViewTreeObserver;
     31 
     32 /**
     33  * Fragment containing video calling surfaces.
     34  */
     35 public class VideoCallFragment extends BaseFragment<VideoCallPresenter,
     36         VideoCallPresenter.VideoCallUi> implements VideoCallPresenter.VideoCallUi {
     37 
     38     /**
     39      * Used to indicate that the surface dimensions are not set.
     40      */
     41     private static final int DIMENSIONS_NOT_SET = -1;
     42 
     43     /**
     44      * Surface ID for the display surface.
     45      */
     46     public static final int SURFACE_DISPLAY = 1;
     47 
     48     /**
     49      * Surface ID for the preview surface.
     50      */
     51     public static final int SURFACE_PREVIEW = 2;
     52 
     53     // Static storage used to retain the video surfaces across Activity restart.
     54     // TextureViews are not parcelable, so it is not possible to store them in the saved state.
     55     private static boolean sVideoSurfacesInUse = false;
     56     private static VideoCallSurface sPreviewSurface = null;
     57     private static VideoCallSurface sDisplaySurface = null;
     58 
     59     /**
     60      * {@link ViewStub} holding the video call surfaces.  This is the parent for the
     61      * {@link VideoCallFragment}.  Used to ensure that the video surfaces are only inflated when
     62      * required.
     63      */
     64     private ViewStub mVideoViewsStub;
     65 
     66     /**
     67      * Inflated view containing the video call surfaces represented by the {@link ViewStub}.
     68      */
     69     private View mVideoViews;
     70 
     71     /**
     72      * {@code True} when the entering the activity again after a restart due to orientation change.
     73      */
     74     private boolean mIsActivityRestart;
     75 
     76     /**
     77      * {@code True} when the layout of the activity has been completed.
     78      */
     79     private boolean mIsLayoutComplete = false;
     80 
     81     /**
     82      * {@code True} if in landscape mode.
     83      */
     84     private boolean mIsLandscape;
     85 
     86     /**
     87      * The width of the surface.
     88      */
     89     private int mWidth = DIMENSIONS_NOT_SET;
     90 
     91     /**
     92      * The height of the surface.
     93      */
     94     private int mHeight = DIMENSIONS_NOT_SET;
     95 
     96     /**
     97      * Inner-class representing a {@link TextureView} and its associated {@link SurfaceTexture} and
     98      * {@link Surface}.  Used to manage the lifecycle of these objects across device orientation
     99      * changes.
    100      */
    101     private class VideoCallSurface implements TextureView.SurfaceTextureListener,
    102             View.OnClickListener {
    103         private int mSurfaceId;
    104         private TextureView mTextureView;
    105         private SurfaceTexture mSavedSurfaceTexture;
    106         private Surface mSavedSurface;
    107 
    108         /**
    109          * Creates an instance of a {@link VideoCallSurface}.
    110          *
    111          * @param surfaceId The surface ID of the surface.
    112          * @param textureView The {@link TextureView} for the surface.
    113          */
    114         public VideoCallSurface(int surfaceId, TextureView textureView) {
    115             this(surfaceId, textureView, DIMENSIONS_NOT_SET, DIMENSIONS_NOT_SET);
    116         }
    117 
    118         /**
    119          * Creates an instance of a {@link VideoCallSurface}.
    120          *
    121          * @param surfaceId The surface ID of the surface.
    122          * @param textureView The {@link TextureView} for the surface.
    123          * @param width The width of the surface.
    124          * @param height The height of the surface.
    125          */
    126         public VideoCallSurface(int surfaceId, TextureView textureView, int width, int height) {
    127             mWidth = width;
    128             mHeight = height;
    129             mSurfaceId = surfaceId;
    130 
    131             recreateView(textureView);
    132         }
    133 
    134         /**
    135          * Recreates a {@link VideoCallSurface} after a device orientation change.  Re-applies the
    136          * saved {@link SurfaceTexture} to the
    137          *
    138          * @param view The {@link TextureView}.
    139          */
    140         public void recreateView(TextureView view) {
    141             mTextureView = view;
    142             mTextureView.setSurfaceTextureListener(this);
    143             mTextureView.setOnClickListener(this);
    144 
    145             if (mSavedSurfaceTexture != null) {
    146                 mTextureView.setSurfaceTexture(mSavedSurfaceTexture);
    147             }
    148         }
    149 
    150         /**
    151          * Handles {@link SurfaceTexture} callback to indicate that a {@link SurfaceTexture} has
    152          * been successfully created.
    153          *
    154          * @param surfaceTexture The {@link SurfaceTexture} which has been created.
    155          * @param width The width of the {@link SurfaceTexture}.
    156          * @param height The height of the {@link SurfaceTexture}.
    157          */
    158         @Override
    159         public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
    160                 int height) {
    161             boolean surfaceCreated;
    162             // Where there is no saved {@link SurfaceTexture} available, use the newly created one.
    163             // If a saved {@link SurfaceTexture} is available, we are re-creating after an
    164             // orientation change.
    165             if (mSavedSurfaceTexture == null) {
    166                 mSavedSurfaceTexture = surfaceTexture;
    167                 surfaceCreated = createSurface();
    168             } else {
    169                 // A saved SurfaceTexture was found.
    170                 surfaceCreated = true;
    171             }
    172 
    173             // Inform presenter that the surface is available.
    174             if (surfaceCreated) {
    175                 getPresenter().onSurfaceCreated(mSurfaceId);
    176             }
    177         }
    178 
    179         /**
    180          * Handles a change in the {@link SurfaceTexture}'s size.
    181          *
    182          * @param surfaceTexture The {@link SurfaceTexture}.
    183          * @param width The new width.
    184          * @param height The new height.
    185          */
    186         @Override
    187         public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width,
    188                 int height) {
    189             // Not handled
    190         }
    191 
    192         /**
    193          * Handles {@link SurfaceTexture} destruct callback, indicating that it has been destroyed.
    194          *
    195          * @param surfaceTexture The {@link SurfaceTexture}.
    196          * @return {@code True} if the {@link TextureView} can release the {@link SurfaceTexture}.
    197          */
    198         @Override
    199         public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    200             /**
    201              * Destroying the surface texture; inform the presenter so it can null the surfaces.
    202              */
    203             if (mSavedSurfaceTexture == null) {
    204                 getPresenter().onSurfaceDestroyed(mSurfaceId);
    205                 if (mSavedSurface != null) {
    206                     mSavedSurface.release();
    207                     mSavedSurface = null;
    208                 }
    209             }
    210 
    211             // The saved SurfaceTexture will be null if we're shutting down, so we want to
    212             // return "true" in that case (indicating that TextureView can release the ST).
    213             return (mSavedSurfaceTexture == null);
    214         }
    215 
    216         /**
    217          * Handles {@link SurfaceTexture} update callback.
    218          * @param surface
    219          */
    220         @Override
    221         public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    222             // Not Handled
    223         }
    224 
    225         /**
    226          * Retrieves the current {@link TextureView}.
    227          *
    228          * @return The {@link TextureView}.
    229          */
    230         public TextureView getTextureView() {
    231             return mTextureView;
    232         }
    233 
    234         /**
    235          * Called by the user presenter to indicate that the surface is no longer required due to a
    236          * change in video state.  Releases and clears out the saved surface and surface textures.
    237          */
    238         public void setDoneWithSurface() {
    239             if (mSavedSurface != null) {
    240                 mSavedSurface.release();
    241                 mSavedSurface = null;
    242             }
    243             if (mSavedSurfaceTexture != null) {
    244                 mSavedSurfaceTexture.release();
    245                 mSavedSurfaceTexture = null;
    246             }
    247         }
    248 
    249         /**
    250          * Retrieves the saved surface instance.
    251          *
    252          * @return The surface.
    253          */
    254         public Surface getSurface() {
    255             return mSavedSurface;
    256         }
    257 
    258         /**
    259          * Sets the dimensions of the surface.
    260          *
    261          * @param width The width of the surface, in pixels.
    262          * @param height The height of the surface, in pixels.
    263          */
    264         public void setSurfaceDimensions(int width, int height) {
    265             mWidth = width;
    266             mHeight = height;
    267 
    268             if (mSavedSurfaceTexture != null) {
    269                 createSurface();
    270             }
    271         }
    272 
    273         /**
    274          * Creates the {@link Surface}, adjusting the {@link SurfaceTexture} buffer size.
    275          */
    276         private boolean createSurface() {
    277             if (mWidth != DIMENSIONS_NOT_SET && mHeight != DIMENSIONS_NOT_SET &&
    278                     mSavedSurfaceTexture != null) {
    279 
    280                 mSavedSurfaceTexture.setDefaultBufferSize(mWidth, mHeight);
    281                 mSavedSurface = new Surface(mSavedSurfaceTexture);
    282                 return true;
    283             }
    284             return false;
    285         }
    286 
    287         /**
    288          * Handles a user clicking the surface, which is the trigger to toggle the full screen
    289          * Video UI.
    290          *
    291          * @param view The view receiving the click.
    292          */
    293         @Override
    294         public void onClick(View view) {
    295             getPresenter().onSurfaceClick(mSurfaceId);
    296         }
    297     };
    298 
    299     @Override
    300     public void onCreate(Bundle savedInstanceState) {
    301         super.onCreate(savedInstanceState);
    302         mIsActivityRestart = sVideoSurfacesInUse;
    303     }
    304 
    305     /**
    306      * Handles creation of the activity and initialization of the presenter.
    307      *
    308      * @param savedInstanceState The saved instance state.
    309      */
    310     @Override
    311     public void onActivityCreated(Bundle savedInstanceState) {
    312         super.onActivityCreated(savedInstanceState);
    313 
    314         mIsLandscape = getResources().getConfiguration().orientation
    315                 == Configuration.ORIENTATION_LANDSCAPE;
    316 
    317         getPresenter().init(getActivity());
    318     }
    319 
    320     /**
    321      * Handles creation of the fragment view.
    322      *
    323      * @param inflater The inflater.
    324      * @param container The view group containing the fragment.
    325      * @param savedInstanceState The saved instance state.
    326      * @return
    327      */
    328     @Override
    329     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    330             Bundle savedInstanceState) {
    331 
    332         super.onCreateView(inflater, container, savedInstanceState);
    333 
    334         final View view = inflater.inflate(R.layout.video_call_fragment, container, false);
    335 
    336         // Attempt to center the incoming video view, if it is in the layout.
    337         final ViewTreeObserver observer = view.getViewTreeObserver();
    338         observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    339             @Override
    340             public void onGlobalLayout() {
    341                 // Check if the layout includes the incoming video surface -- this will only be the
    342                 // case for a video call.
    343                 View displayVideo = view.findViewById(R.id.incomingVideo);
    344                 if (displayVideo != null) {
    345                     centerDisplayView(displayVideo);
    346                 }
    347 
    348                 mIsLayoutComplete = true;
    349 
    350                 // Remove the listener so we don't continually re-layout.
    351                 ViewTreeObserver observer = view.getViewTreeObserver();
    352                 if (observer.isAlive()) {
    353                     observer.removeOnGlobalLayoutListener(this);
    354                 }
    355             }
    356         });
    357 
    358         return view;
    359     }
    360 
    361     /**
    362      * Centers the display view vertically for portrait orientation, and horizontally for
    363      * lanscape orientations.  The view is centered within the available space not occupied by
    364      * the call card.
    365      *
    366      * @param displayVideo The video view to center.
    367      */
    368     private void centerDisplayView(View displayVideo) {
    369         // In a lansdcape layout we need to ensure we horizontally center the view based on whether
    370         // the layout is left-to-right or right-to-left.
    371         // In a left-to-right locale, the space for the video view is to the right of the call card
    372         // so we need to translate it in the +X direction.
    373         // In a right-to-left locale, the space for the video view is to the left of the call card
    374         // so we need to translate it in the -X direction.
    375         final boolean isLayoutRtl = InCallPresenter.isRtl();
    376 
    377         float spaceBesideCallCard = InCallPresenter.getInstance().getSpaceBesideCallCard();
    378         if (mIsLandscape) {
    379             float videoViewTranslation = displayVideo.getWidth() / 2
    380                     - spaceBesideCallCard / 2;
    381             if (isLayoutRtl) {
    382                 displayVideo.setTranslationX(-videoViewTranslation);
    383             } else {
    384                 displayVideo.setTranslationX(videoViewTranslation);
    385             }
    386         } else {
    387             float videoViewTranslation = displayVideo.getHeight() / 2
    388                     - spaceBesideCallCard / 2;
    389             displayVideo.setTranslationY(videoViewTranslation);
    390         }
    391     }
    392 
    393     /**
    394      * After creation of the fragment view, retrieves the required views.
    395      *
    396      * @param view The fragment view.
    397      * @param savedInstanceState The saved instance state.
    398      */
    399     @Override
    400     public void onViewCreated(View view, Bundle savedInstanceState) {
    401         super.onViewCreated(view, savedInstanceState);
    402 
    403         mVideoViewsStub = (ViewStub) view.findViewById(R.id.videoCallViewsStub);
    404 
    405         // If the surfaces are already in use, we have just changed orientation or otherwise
    406         // re-created the fragment.  In this case we need to inflate the video call views and
    407         // restore the surfaces.
    408         if (sVideoSurfacesInUse) {
    409             inflateVideoCallViews();
    410         }
    411     }
    412 
    413     /**
    414      * Creates the presenter for the {@link VideoCallFragment}.
    415      * @return The presenter instance.
    416      */
    417     @Override
    418     public VideoCallPresenter createPresenter() {
    419         return new VideoCallPresenter();
    420     }
    421 
    422     /**
    423      * @return The user interface for the presenter, which is this fragment.
    424      */
    425     @Override
    426     VideoCallPresenter.VideoCallUi getUi() {
    427         return this;
    428     }
    429 
    430     /**
    431      * Toggles visibility of the video UI.
    432      *
    433      * @param show {@code True} if the video surfaces should be shown.
    434      */
    435     @Override
    436     public void showVideoUi(boolean show) {
    437         int visibility = show ? View.VISIBLE : View.GONE;
    438         getView().setVisibility(visibility);
    439 
    440         if (show) {
    441             inflateVideoCallViews();
    442         } else {
    443             cleanupSurfaces();
    444         }
    445 
    446         if (mVideoViews != null ) {
    447             mVideoViews.setVisibility(visibility);
    448         }
    449     }
    450 
    451     /**
    452      * Cleans up the video telephony surfaces.  Used when the presenter indicates a change to an
    453      * audio-only state.  Since the surfaces are static, it is important to ensure they are cleaned
    454      * up promptly.
    455      */
    456     @Override
    457     public void cleanupSurfaces() {
    458         if (sDisplaySurface != null) {
    459             sDisplaySurface.setDoneWithSurface();
    460             sDisplaySurface = null;
    461         }
    462         if (sPreviewSurface != null) {
    463             sPreviewSurface.setDoneWithSurface();
    464             sPreviewSurface = null;
    465         }
    466         sVideoSurfacesInUse = false;
    467     }
    468 
    469     @Override
    470     public boolean isActivityRestart() {
    471         return mIsActivityRestart;
    472     }
    473 
    474     /**
    475      * @return {@code True} if the display video surface has been created.
    476      */
    477     @Override
    478     public boolean isDisplayVideoSurfaceCreated() {
    479         return sDisplaySurface != null && sDisplaySurface.getSurface() != null;
    480     }
    481 
    482     /**
    483      * @return {@code True} if the preview video surface has been created.
    484      */
    485     @Override
    486     public boolean isPreviewVideoSurfaceCreated() {
    487         return sPreviewSurface != null && sPreviewSurface.getSurface() != null;
    488     }
    489 
    490     /**
    491      * {@link android.view.Surface} on which incoming video for a video call is displayed.
    492      * {@code Null} until the video views {@link android.view.ViewStub} is inflated.
    493      */
    494     @Override
    495     public Surface getDisplayVideoSurface() {
    496         return sDisplaySurface == null ? null : sDisplaySurface.getSurface();
    497     }
    498 
    499     /**
    500      * {@link android.view.Surface} on which a preview of the outgoing video for a video call is
    501      * displayed.  {@code Null} until the video views {@link android.view.ViewStub} is inflated.
    502      */
    503     @Override
    504     public Surface getPreviewVideoSurface() {
    505         return sPreviewSurface == null ? null : sPreviewSurface.getSurface();
    506     }
    507 
    508     /**
    509      * Changes the dimensions of the preview surface.  Called when the dimensions change due to a
    510      * device orientation change.
    511      *
    512      * @param width The new width.
    513      * @param height The new height.
    514      */
    515     @Override
    516     public void setPreviewSize(int width, int height) {
    517         if (sPreviewSurface != null) {
    518             TextureView preview = sPreviewSurface.getTextureView();
    519 
    520             if (preview == null ) {
    521                 return;
    522             }
    523 
    524             ViewGroup.LayoutParams params = preview.getLayoutParams();
    525             params.width = width;
    526             params.height = height;
    527             preview.setLayoutParams(params);
    528 
    529             sPreviewSurface.setSurfaceDimensions(width, height);
    530         }
    531     }
    532 
    533     /**
    534      * Inflates the {@link ViewStub} containing the incoming and outgoing surfaces, if necessary,
    535      * and creates {@link VideoCallSurface} instances to track the surfaces.
    536      */
    537     private void inflateVideoCallViews() {
    538         if (mVideoViews == null ) {
    539             mVideoViews = mVideoViewsStub.inflate();
    540         }
    541 
    542         if (mVideoViews != null) {
    543             TextureView displaySurface = (TextureView) mVideoViews.findViewById(R.id.incomingVideo);
    544 
    545             Point screenSize = getScreenSize();
    546             setSurfaceSizeAndTranslation(displaySurface, screenSize);
    547 
    548             if (!sVideoSurfacesInUse) {
    549                 // Where the video surfaces are not already in use (first time creating them),
    550                 // setup new VideoCallSurface instances to track them.
    551                 sDisplaySurface = new VideoCallSurface(SURFACE_DISPLAY,
    552                         (TextureView) mVideoViews.findViewById(R.id.incomingVideo), screenSize.x,
    553                         screenSize.y);
    554                 sPreviewSurface = new VideoCallSurface(SURFACE_PREVIEW,
    555                         (TextureView) mVideoViews.findViewById(R.id.previewVideo));
    556                 sVideoSurfacesInUse = true;
    557             } else {
    558                 // In this case, the video surfaces are already in use (we are recreating the
    559                 // Fragment after a destroy/create cycle resulting from a rotation.
    560                 sDisplaySurface.recreateView((TextureView) mVideoViews.findViewById(
    561                         R.id.incomingVideo));
    562                 sPreviewSurface.recreateView((TextureView) mVideoViews.findViewById(
    563                         R.id.previewVideo));
    564             }
    565         }
    566     }
    567 
    568     /**
    569      * Resizes a surface so that it has the same size as the full screen and so that it is
    570      * centered vertically below the call card.
    571      *
    572      * @param textureView The {@link TextureView} to resize and position.
    573      * @param size The size of the screen.
    574      */
    575     private void setSurfaceSizeAndTranslation(TextureView textureView, Point size) {
    576         // Set the surface to have that size.
    577         ViewGroup.LayoutParams params = textureView.getLayoutParams();
    578         params.width = size.x;
    579         params.height = size.y;
    580         textureView.setLayoutParams(params);
    581 
    582         // It is only possible to center the display view if layout of the views has completed.
    583         // It is only after layout is complete that the dimensions of the Call Card has been
    584         // established, which is a prerequisite to centering the view.
    585         // Incoming video calls will center the view
    586         if (mIsLayoutComplete && ((mIsLandscape && textureView.getTranslationX() == 0) || (
    587                 !mIsLandscape && textureView.getTranslationY() == 0))) {
    588             centerDisplayView(textureView);
    589         }
    590     }
    591 
    592     /**
    593      * Determines the size of the device screen.
    594      *
    595      * @return {@link Point} specifying the width and height of the screen.
    596      */
    597     private Point getScreenSize() {
    598         // Get current screen size.
    599         Display display = getActivity().getWindowManager().getDefaultDisplay();
    600         Point size = new Point();
    601         display.getSize(size);
    602 
    603         return size;
    604     }
    605 }
    606