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