1 /* 2 * Copyright (C) 2016 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.video.impl; 18 19 import android.Manifest.permission; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.graphics.Point; 23 import android.graphics.drawable.Animatable; 24 import android.os.Bundle; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.VisibleForTesting; 28 import android.support.v4.app.Fragment; 29 import android.support.v4.app.FragmentTransaction; 30 import android.support.v4.view.animation.FastOutLinearInInterpolator; 31 import android.support.v4.view.animation.LinearOutSlowInInterpolator; 32 import android.telecom.CallAudioState; 33 import android.text.TextUtils; 34 import android.view.LayoutInflater; 35 import android.view.Surface; 36 import android.view.SurfaceView; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.view.View.OnSystemUiVisibilityChangeListener; 40 import android.view.ViewGroup; 41 import android.view.ViewGroup.MarginLayoutParams; 42 import android.view.ViewTreeObserver; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.animation.AccelerateDecelerateInterpolator; 45 import android.view.animation.Interpolator; 46 import android.widget.FrameLayout; 47 import android.widget.ImageButton; 48 import android.widget.TextView; 49 import com.android.dialer.common.Assert; 50 import com.android.dialer.common.FragmentUtils; 51 import com.android.dialer.common.LogUtil; 52 import com.android.dialer.compat.ActivityCompat; 53 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; 54 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; 55 import com.android.incallui.contactgrid.ContactGridManager; 56 import com.android.incallui.hold.OnHoldFragment; 57 import com.android.incallui.incall.protocol.InCallButtonIds; 58 import com.android.incallui.incall.protocol.InCallButtonIdsExtension; 59 import com.android.incallui.incall.protocol.InCallButtonUi; 60 import com.android.incallui.incall.protocol.InCallButtonUiDelegate; 61 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; 62 import com.android.incallui.incall.protocol.InCallScreen; 63 import com.android.incallui.incall.protocol.InCallScreenDelegate; 64 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; 65 import com.android.incallui.incall.protocol.PrimaryCallState; 66 import com.android.incallui.incall.protocol.PrimaryInfo; 67 import com.android.incallui.incall.protocol.SecondaryInfo; 68 import com.android.incallui.video.impl.CameraPermissionDialogFragment.CameraPermissionDialogCallback; 69 import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener; 70 import com.android.incallui.video.protocol.VideoCallScreen; 71 import com.android.incallui.video.protocol.VideoCallScreenDelegate; 72 import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory; 73 import com.android.incallui.videotech.utils.VideoUtils; 74 75 /** 76 * Contains UI elements for a video call. 77 * 78 * <p>This version is used by RCS Video Share since Dreamchip requires a SurfaceView instead of the 79 * TextureView, which is present in {@link VideoCallFragment} and used by IMS. 80 */ 81 public class SurfaceViewVideoCallFragment extends Fragment 82 implements InCallScreen, 83 InCallButtonUi, 84 VideoCallScreen, 85 OnClickListener, 86 OnCheckedChangeListener, 87 AudioRouteSelectorPresenter, 88 OnSystemUiVisibilityChangeListener, 89 CameraPermissionDialogCallback { 90 91 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 92 static final String ARG_CALL_ID = "call_id"; 93 94 private static final int CAMERA_PERMISSION_REQUEST_CODE = 1; 95 private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG = 96 "CameraPermissionDialogFragment"; 97 private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L; 98 private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L; 99 100 private InCallScreenDelegate inCallScreenDelegate; 101 private VideoCallScreenDelegate videoCallScreenDelegate; 102 private InCallButtonUiDelegate inCallButtonUiDelegate; 103 private View endCallButton; 104 private CheckableImageButton speakerButton; 105 private SpeakerButtonController speakerButtonController; 106 private CheckableImageButton muteButton; 107 private CheckableImageButton cameraOffButton; 108 private ImageButton swapCameraButton; 109 private View switchOnHoldButton; 110 private View onHoldContainer; 111 private SwitchOnHoldCallController switchOnHoldCallController; 112 private TextView remoteVideoOff; 113 private View mutePreviewOverlay; 114 private View previewOffOverlay; 115 private View controls; 116 private View controlsContainer; 117 private SurfaceView previewSurfaceView; 118 private SurfaceView remoteSurfaceView; 119 private View greenScreenBackgroundView; 120 private View fullscreenBackgroundView; 121 private FrameLayout previewRoot; 122 private boolean shouldShowRemote; 123 private boolean shouldShowPreview; 124 private boolean isInFullscreenMode; 125 private boolean isInGreenScreenMode; 126 private boolean hasInitializedScreenModes; 127 private boolean isRemotelyHeld; 128 private ContactGridManager contactGridManager; 129 private SecondaryInfo savedSecondaryInfo; 130 private final Runnable cameraPermissionDialogRunnable = 131 new Runnable() { 132 @Override 133 public void run() { 134 if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) { 135 LogUtil.i( 136 "SurfaceViewVideoCallFragment.cameraPermissionDialogRunnable", "showing dialog"); 137 checkCameraPermission(); 138 } 139 } 140 }; 141 newInstance(String callId)142 public static SurfaceViewVideoCallFragment newInstance(String callId) { 143 Bundle bundle = new Bundle(); 144 bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId)); 145 146 SurfaceViewVideoCallFragment instance = new SurfaceViewVideoCallFragment(); 147 instance.setArguments(bundle); 148 return instance; 149 } 150 151 @Override onCreate(@ullable Bundle savedInstanceState)152 public void onCreate(@Nullable Bundle savedInstanceState) { 153 super.onCreate(savedInstanceState); 154 LogUtil.i("SurfaceViewVideoCallFragment.onCreate", null); 155 156 inCallButtonUiDelegate = 157 FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) 158 .newInCallButtonUiDelegate(); 159 if (savedInstanceState != null) { 160 inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); 161 } 162 } 163 164 @Override onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)165 public void onRequestPermissionsResult( 166 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 167 if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { 168 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 169 LogUtil.i( 170 "SurfaceViewVideoCallFragment.onRequestPermissionsResult", 171 "Camera permission granted."); 172 videoCallScreenDelegate.onCameraPermissionGranted(); 173 } else { 174 LogUtil.i( 175 "SurfaceViewVideoCallFragment.onRequestPermissionsResult", "Camera permission denied."); 176 } 177 } 178 super.onRequestPermissionsResult(requestCode, permissions, grantResults); 179 } 180 181 @Nullable 182 @Override onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)183 public View onCreateView( 184 LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { 185 LogUtil.i("SurfaceViewVideoCallFragment.onCreateView", null); 186 187 View view = layoutInflater.inflate(R.layout.frag_videocall_surfaceview, viewGroup, false); 188 contactGridManager = 189 new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */); 190 191 controls = view.findViewById(R.id.videocall_video_controls); 192 controls.setVisibility( 193 ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE); 194 controlsContainer = view.findViewById(R.id.videocall_video_controls_container); 195 speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button); 196 muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button); 197 muteButton.setOnCheckedChangeListener(this); 198 mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay); 199 cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video); 200 cameraOffButton.setOnCheckedChangeListener(this); 201 previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay); 202 swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video); 203 swapCameraButton.setOnClickListener(this); 204 view.findViewById(R.id.videocall_switch_controls) 205 .setVisibility( 206 ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE); 207 switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold); 208 onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner); 209 remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off); 210 remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 211 endCallButton = view.findViewById(R.id.videocall_end_call); 212 endCallButton.setOnClickListener(this); 213 previewSurfaceView = (SurfaceView) view.findViewById(R.id.videocall_video_preview); 214 previewSurfaceView.setZOrderMediaOverlay(true); 215 previewOffOverlay.setOnClickListener( 216 new OnClickListener() { 217 @Override 218 public void onClick(View v) { 219 checkCameraPermission(); 220 } 221 }); 222 remoteSurfaceView = (SurfaceView) view.findViewById(R.id.videocall_video_remote); 223 remoteSurfaceView.setOnClickListener( 224 surfaceView -> { 225 videoCallScreenDelegate.resetAutoFullscreenTimer(); 226 if (isInFullscreenMode) { 227 updateFullscreenAndGreenScreenMode( 228 false /* shouldShowFullscreen */, false /* shouldShowGreenScreen */); 229 } else { 230 updateFullscreenAndGreenScreenMode( 231 true /* shouldShowFullscreen */, false /* shouldShowGreenScreen */); 232 } 233 }); 234 greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background); 235 fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background); 236 previewRoot = (FrameLayout) view.findViewById(R.id.videocall_preview_root); 237 238 // We need the texture view size to be able to scale the remote video. At this point the view 239 // layout won't be complete so add a layout listener. 240 ViewTreeObserver observer = remoteSurfaceView.getViewTreeObserver(); 241 observer.addOnGlobalLayoutListener( 242 new ViewTreeObserver.OnGlobalLayoutListener() { 243 @Override 244 public void onGlobalLayout() { 245 LogUtil.i("SurfaceViewVideoCallFragment.onGlobalLayout", null); 246 updateVideoOffViews(); 247 // Remove the listener so we don't continually re-layout. 248 ViewTreeObserver observer = remoteSurfaceView.getViewTreeObserver(); 249 if (observer.isAlive()) { 250 observer.removeOnGlobalLayoutListener(this); 251 } 252 } 253 }); 254 255 return view; 256 } 257 258 @Override onViewCreated(View view, @Nullable Bundle bundle)259 public void onViewCreated(View view, @Nullable Bundle bundle) { 260 super.onViewCreated(view, bundle); 261 LogUtil.i("SurfaceViewVideoCallFragment.onViewCreated", null); 262 263 inCallScreenDelegate = 264 FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) 265 .newInCallScreenDelegate(); 266 videoCallScreenDelegate = 267 FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class) 268 .newVideoCallScreenDelegate(this); 269 270 speakerButtonController = 271 new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate); 272 switchOnHoldCallController = 273 new SwitchOnHoldCallController( 274 switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate); 275 276 videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this); 277 278 inCallScreenDelegate.onInCallScreenDelegateInit(this); 279 inCallScreenDelegate.onInCallScreenReady(); 280 inCallButtonUiDelegate.onInCallButtonUiReady(this); 281 282 view.setOnSystemUiVisibilityChangeListener(this); 283 } 284 285 @Override onSaveInstanceState(Bundle outState)286 public void onSaveInstanceState(Bundle outState) { 287 super.onSaveInstanceState(outState); 288 inCallButtonUiDelegate.onSaveInstanceState(outState); 289 } 290 291 @Override onDestroyView()292 public void onDestroyView() { 293 super.onDestroyView(); 294 LogUtil.i("SurfaceViewVideoCallFragment.onDestroyView", null); 295 inCallButtonUiDelegate.onInCallButtonUiUnready(); 296 inCallScreenDelegate.onInCallScreenUnready(); 297 } 298 299 @Override onAttach(Context context)300 public void onAttach(Context context) { 301 super.onAttach(context); 302 if (savedSecondaryInfo != null) { 303 setSecondary(savedSecondaryInfo); 304 } 305 } 306 307 @Override onStart()308 public void onStart() { 309 super.onStart(); 310 LogUtil.i("SurfaceViewVideoCallFragment.onStart", null); 311 onVideoScreenStart(); 312 } 313 314 @Override onVideoScreenStart()315 public void onVideoScreenStart() { 316 inCallButtonUiDelegate.refreshMuteState(); 317 videoCallScreenDelegate.onVideoCallScreenUiReady(); 318 getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS); 319 } 320 321 @Override onResume()322 public void onResume() { 323 super.onResume(); 324 LogUtil.i("SurfaceViewVideoCallFragment.onResume", null); 325 inCallScreenDelegate.onInCallScreenResumed(); 326 } 327 328 @Override onPause()329 public void onPause() { 330 super.onPause(); 331 LogUtil.i("SurfaceViewVideoCallFragment.onPause", null); 332 inCallScreenDelegate.onInCallScreenPaused(); 333 } 334 335 @Override onStop()336 public void onStop() { 337 super.onStop(); 338 LogUtil.i("SurfaceViewVideoCallFragment.onStop", null); 339 onVideoScreenStop(); 340 } 341 342 @Override onVideoScreenStop()343 public void onVideoScreenStop() { 344 getView().removeCallbacks(cameraPermissionDialogRunnable); 345 videoCallScreenDelegate.onVideoCallScreenUiUnready(); 346 } 347 exitFullscreenMode()348 private void exitFullscreenMode() { 349 LogUtil.i("SurfaceViewVideoCallFragment.exitFullscreenMode", null); 350 351 if (!getView().isAttachedToWindow()) { 352 LogUtil.i("SurfaceViewVideoCallFragment.exitFullscreenMode", "not attached"); 353 return; 354 } 355 356 showSystemUI(); 357 358 LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); 359 360 // Animate the controls to the shown state. 361 controls 362 .animate() 363 .translationX(0) 364 .translationY(0) 365 .setInterpolator(linearOutSlowInInterpolator) 366 .alpha(1) 367 .start(); 368 369 // Animate onHold to the shown state. 370 switchOnHoldButton 371 .animate() 372 .translationX(0) 373 .translationY(0) 374 .setInterpolator(linearOutSlowInInterpolator) 375 .alpha(1) 376 .withStartAction( 377 new Runnable() { 378 @Override 379 public void run() { 380 switchOnHoldCallController.setOnScreen(); 381 } 382 }); 383 384 View contactGridView = contactGridManager.getContainerView(); 385 // Animate contact grid to the shown state. 386 contactGridView 387 .animate() 388 .translationX(0) 389 .translationY(0) 390 .setInterpolator(linearOutSlowInInterpolator) 391 .alpha(1) 392 .withStartAction( 393 new Runnable() { 394 @Override 395 public void run() { 396 contactGridManager.show(); 397 } 398 }); 399 400 endCallButton 401 .animate() 402 .translationX(0) 403 .translationY(0) 404 .setInterpolator(linearOutSlowInInterpolator) 405 .alpha(1) 406 .withStartAction( 407 new Runnable() { 408 @Override 409 public void run() { 410 endCallButton.setVisibility(View.VISIBLE); 411 } 412 }) 413 .start(); 414 415 // Animate all the preview controls up to make room for the navigation bar. 416 // In green screen mode we don't need this because the preview takes up the whole screen and has 417 // a fixed position. 418 if (!isInGreenScreenMode) { 419 Point previewOffsetStartShown = getPreviewOffsetStartShown(); 420 for (View view : getAllPreviewRelatedViews()) { 421 // Animate up with the preview offset above the navigation bar. 422 view.animate() 423 .translationX(previewOffsetStartShown.x) 424 .translationY(previewOffsetStartShown.y) 425 .setInterpolator(new AccelerateDecelerateInterpolator()) 426 .start(); 427 } 428 } 429 430 updateOverlayBackground(); 431 } 432 showSystemUI()433 private void showSystemUI() { 434 View view = getView(); 435 if (view != null) { 436 // Code is more expressive with all flags present, even though some may be combined 437 //noinspection PointlessBitwiseExpression 438 view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 439 } 440 } 441 442 /** Set view flags to hide the system UI. System UI will return on any touch event */ hideSystemUI()443 private void hideSystemUI() { 444 View view = getView(); 445 if (view != null) { 446 view.setSystemUiVisibility( 447 View.SYSTEM_UI_FLAG_FULLSCREEN 448 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 449 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 450 } 451 } 452 getControlsOffsetEndHidden(View controls)453 private Point getControlsOffsetEndHidden(View controls) { 454 if (isLandscape()) { 455 return new Point(0, getOffsetBottom(controls)); 456 } else { 457 return new Point(getOffsetStart(controls), 0); 458 } 459 } 460 getSwitchOnHoldOffsetEndHidden(View swapCallButton)461 private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) { 462 if (isLandscape()) { 463 return new Point(0, getOffsetTop(swapCallButton)); 464 } else { 465 return new Point(getOffsetEnd(swapCallButton), 0); 466 } 467 } 468 getContactGridOffsetEndHidden(View view)469 private Point getContactGridOffsetEndHidden(View view) { 470 return new Point(0, getOffsetTop(view)); 471 } 472 getEndCallOffsetEndHidden(View endCallButton)473 private Point getEndCallOffsetEndHidden(View endCallButton) { 474 if (isLandscape()) { 475 return new Point(getOffsetEnd(endCallButton), 0); 476 } else { 477 return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin); 478 } 479 } 480 getPreviewOffsetStartShown()481 private Point getPreviewOffsetStartShown() { 482 // No insets in multiwindow mode, and rootWindowInsets will get the display's insets. 483 if (ActivityCompat.isInMultiWindowMode(getActivity())) { 484 return new Point(); 485 } 486 if (isLandscape()) { 487 int stableInsetEnd = 488 getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL 489 ? getView().getRootWindowInsets().getStableInsetLeft() 490 : -getView().getRootWindowInsets().getStableInsetRight(); 491 return new Point(stableInsetEnd, 0); 492 } else { 493 return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom()); 494 } 495 } 496 getAllPreviewRelatedViews()497 private View[] getAllPreviewRelatedViews() { 498 return new View[] {previewRoot}; 499 } 500 getOffsetTop(View view)501 private int getOffsetTop(View view) { 502 return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin); 503 } 504 getOffsetBottom(View view)505 private int getOffsetBottom(View view) { 506 return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin; 507 } 508 getOffsetStart(View view)509 private int getOffsetStart(View view) { 510 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart(); 511 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 512 offset = -offset; 513 } 514 return -offset; 515 } 516 getOffsetEnd(View view)517 private int getOffsetEnd(View view) { 518 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd(); 519 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 520 offset = -offset; 521 } 522 return offset; 523 } 524 enterFullscreenMode()525 private void enterFullscreenMode() { 526 LogUtil.i("SurfaceViewVideoCallFragment.enterFullscreenMode", null); 527 528 hideSystemUI(); 529 530 Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator(); 531 532 // Animate controls to the hidden state. 533 Point offset = getControlsOffsetEndHidden(controls); 534 controls 535 .animate() 536 .translationX(offset.x) 537 .translationY(offset.y) 538 .setInterpolator(fastOutLinearInInterpolator) 539 .alpha(0) 540 .start(); 541 542 // Animate onHold to the hidden state. 543 offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton); 544 switchOnHoldButton 545 .animate() 546 .translationX(offset.x) 547 .translationY(offset.y) 548 .setInterpolator(fastOutLinearInInterpolator) 549 .alpha(0); 550 551 View contactGridView = contactGridManager.getContainerView(); 552 // Animate contact grid to the hidden state. 553 offset = getContactGridOffsetEndHidden(contactGridView); 554 contactGridView 555 .animate() 556 .translationX(offset.x) 557 .translationY(offset.y) 558 .setInterpolator(fastOutLinearInInterpolator) 559 .alpha(0); 560 561 offset = getEndCallOffsetEndHidden(endCallButton); 562 // Use a fast out interpolator to quickly fade out the button. This is important because the 563 // button can't draw under the navigation bar which means that it'll look weird if it just 564 // abruptly disappears when it reaches the edge of the naivgation bar. 565 endCallButton 566 .animate() 567 .translationX(offset.x) 568 .translationY(offset.y) 569 .setInterpolator(fastOutLinearInInterpolator) 570 .alpha(0) 571 .withEndAction( 572 new Runnable() { 573 @Override 574 public void run() { 575 endCallButton.setVisibility(View.INVISIBLE); 576 } 577 }) 578 .setInterpolator(new FastOutLinearInInterpolator()) 579 .start(); 580 581 // Animate all the preview controls down now that the navigation bar is hidden. 582 // In green screen mode we don't need this because the preview takes up the whole screen and has 583 // a fixed position. 584 if (!isInGreenScreenMode) { 585 for (View view : getAllPreviewRelatedViews()) { 586 // Animate down with the navigation bar hidden. 587 view.animate() 588 .translationX(0) 589 .translationY(0) 590 .setInterpolator(new AccelerateDecelerateInterpolator()) 591 .start(); 592 } 593 } 594 updateOverlayBackground(); 595 } 596 597 @Override onClick(View v)598 public void onClick(View v) { 599 if (v == endCallButton) { 600 LogUtil.i("SurfaceViewVideoCallFragment.onClick", "end call button clicked"); 601 inCallButtonUiDelegate.onEndCallClicked(); 602 videoCallScreenDelegate.resetAutoFullscreenTimer(); 603 } else if (v == swapCameraButton) { 604 if (swapCameraButton.getDrawable() instanceof Animatable) { 605 ((Animatable) swapCameraButton.getDrawable()).start(); 606 } 607 inCallButtonUiDelegate.toggleCameraClicked(); 608 videoCallScreenDelegate.resetAutoFullscreenTimer(); 609 } 610 } 611 612 @Override onCheckedChanged(CheckableImageButton button, boolean isChecked)613 public void onCheckedChanged(CheckableImageButton button, boolean isChecked) { 614 if (button == cameraOffButton) { 615 if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) { 616 LogUtil.i("SurfaceViewVideoCallFragment.onCheckedChanged", "show camera permission dialog"); 617 checkCameraPermission(); 618 } else { 619 inCallButtonUiDelegate.pauseVideoClicked(isChecked); 620 videoCallScreenDelegate.resetAutoFullscreenTimer(); 621 } 622 } else if (button == muteButton) { 623 inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */); 624 videoCallScreenDelegate.resetAutoFullscreenTimer(); 625 } 626 } 627 628 @Override showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)629 public void showVideoViews( 630 boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) { 631 LogUtil.i( 632 "SurfaceViewVideoCallFragment.showVideoViews", 633 "showPreview: %b, shouldShowRemote: %b", 634 shouldShowPreview, 635 shouldShowRemote); 636 637 this.shouldShowPreview = shouldShowPreview; 638 this.shouldShowRemote = shouldShowRemote; 639 this.isRemotelyHeld = isRemotelyHeld; 640 641 previewSurfaceView.setVisibility(shouldShowPreview ? View.VISIBLE : View.INVISIBLE); 642 643 videoCallScreenDelegate.setSurfaceViews(previewSurfaceView, remoteSurfaceView); 644 updateVideoOffViews(); 645 } 646 647 /** 648 * This method scales the video feed inside the texture view, it doesn't change the texture view's 649 * size. In the old UI we would change the view size to match the aspect ratio of the video. In 650 * the new UI the view is always square (with the circular clip) so we have to do additional work 651 * to make sure the non-square video doesn't look squished. 652 */ 653 @Override onLocalVideoDimensionsChanged()654 public void onLocalVideoDimensionsChanged() { 655 LogUtil.i("SurfaceViewVideoCallFragment.onLocalVideoDimensionsChanged", null); 656 } 657 658 @Override onLocalVideoOrientationChanged()659 public void onLocalVideoOrientationChanged() { 660 LogUtil.i("SurfaceViewVideoCallFragment.onLocalVideoOrientationChanged", null); 661 } 662 663 /** Called when the remote video's dimensions change. */ 664 @Override onRemoteVideoDimensionsChanged()665 public void onRemoteVideoDimensionsChanged() { 666 LogUtil.i("SurfaceViewVideoCallFragment.onRemoteVideoDimensionsChanged", null); 667 } 668 669 @Override updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)670 public void updateFullscreenAndGreenScreenMode( 671 boolean shouldShowFullscreen, boolean shouldShowGreenScreen) { 672 LogUtil.i( 673 "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode", 674 "shouldShowFullscreen: %b, shouldShowGreenScreen: %b", 675 shouldShowFullscreen, 676 shouldShowGreenScreen); 677 678 if (getActivity() == null) { 679 LogUtil.i( 680 "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode", 681 "not attached to activity"); 682 return; 683 } 684 685 // Check if anything is actually going to change. The first time this function is called we 686 // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen 687 // and green screen modes to update even if only one has changed. That's because they both 688 // depend on each other. 689 if (hasInitializedScreenModes 690 && shouldShowGreenScreen == isInGreenScreenMode 691 && shouldShowFullscreen == isInFullscreenMode) { 692 LogUtil.i( 693 "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode", 694 "no change to screen modes"); 695 return; 696 } 697 hasInitializedScreenModes = true; 698 isInGreenScreenMode = shouldShowGreenScreen; 699 isInFullscreenMode = shouldShowFullscreen; 700 701 if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) { 702 controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets()); 703 } 704 if (shouldShowGreenScreen) { 705 enterGreenScreenMode(); 706 } else { 707 exitGreenScreenMode(); 708 } 709 if (shouldShowFullscreen) { 710 enterFullscreenMode(); 711 } else { 712 exitFullscreenMode(); 713 } 714 updateVideoOffViews(); 715 716 OnHoldFragment onHoldFragment = 717 ((OnHoldFragment) 718 getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner)); 719 if (onHoldFragment != null) { 720 onHoldFragment.setPadTopInset(!isInFullscreenMode); 721 } 722 } 723 724 @Override getVideoCallScreenFragment()725 public Fragment getVideoCallScreenFragment() { 726 return this; 727 } 728 729 @Override 730 @NonNull getCallId()731 public String getCallId() { 732 return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); 733 } 734 735 @Override showButton(@nCallButtonIds int buttonId, boolean show)736 public void showButton(@InCallButtonIds int buttonId, boolean show) { 737 LogUtil.v( 738 "SurfaceViewVideoCallFragment.showButton", 739 "buttonId: %s, show: %b", 740 InCallButtonIdsExtension.toString(buttonId), 741 show); 742 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 743 speakerButtonController.setEnabled(show); 744 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 745 muteButton.setEnabled(show); 746 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 747 cameraOffButton.setEnabled(show); 748 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 749 switchOnHoldCallController.setVisible(show); 750 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) { 751 swapCameraButton.setEnabled(show); 752 } 753 } 754 755 @Override enableButton(@nCallButtonIds int buttonId, boolean enable)756 public void enableButton(@InCallButtonIds int buttonId, boolean enable) { 757 LogUtil.v( 758 "SurfaceViewVideoCallFragment.setEnabled", 759 "buttonId: %s, enable: %b", 760 InCallButtonIdsExtension.toString(buttonId), 761 enable); 762 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 763 speakerButtonController.setEnabled(enable); 764 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 765 muteButton.setEnabled(enable); 766 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 767 cameraOffButton.setEnabled(enable); 768 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 769 switchOnHoldCallController.setEnabled(enable); 770 } 771 } 772 773 @Override setEnabled(boolean enabled)774 public void setEnabled(boolean enabled) { 775 LogUtil.v("SurfaceViewVideoCallFragment.setEnabled", "enabled: " + enabled); 776 speakerButtonController.setEnabled(enabled); 777 muteButton.setEnabled(enabled); 778 cameraOffButton.setEnabled(enabled); 779 switchOnHoldCallController.setEnabled(enabled); 780 } 781 782 @Override setHold(boolean value)783 public void setHold(boolean value) { 784 LogUtil.i("SurfaceViewVideoCallFragment.setHold", "value: " + value); 785 } 786 787 @Override setCameraSwitched(boolean isBackFacingCamera)788 public void setCameraSwitched(boolean isBackFacingCamera) { 789 LogUtil.i( 790 "SurfaceViewVideoCallFragment.setCameraSwitched", 791 "isBackFacingCamera: " + isBackFacingCamera); 792 } 793 794 @Override setVideoPaused(boolean isPaused)795 public void setVideoPaused(boolean isPaused) { 796 LogUtil.i("SurfaceViewVideoCallFragment.setVideoPaused", "isPaused: " + isPaused); 797 cameraOffButton.setChecked(isPaused); 798 } 799 800 @Override setAudioState(CallAudioState audioState)801 public void setAudioState(CallAudioState audioState) { 802 LogUtil.i("SurfaceViewVideoCallFragment.setAudioState", "audioState: " + audioState); 803 speakerButtonController.setAudioState(audioState); 804 muteButton.setChecked(audioState.isMuted()); 805 updateMutePreviewOverlayVisibility(); 806 } 807 808 @Override updateButtonStates()809 public void updateButtonStates() { 810 LogUtil.i("SurfaceViewVideoCallFragment.updateButtonState", null); 811 speakerButtonController.updateButtonState(); 812 switchOnHoldCallController.updateButtonState(); 813 } 814 815 @Override updateInCallButtonUiColors()816 public void updateInCallButtonUiColors() {} 817 818 @Override getInCallButtonUiFragment()819 public Fragment getInCallButtonUiFragment() { 820 return this; 821 } 822 823 @Override showAudioRouteSelector()824 public void showAudioRouteSelector() { 825 LogUtil.i("SurfaceViewVideoCallFragment.showAudioRouteSelector", null); 826 AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) 827 .show(getChildFragmentManager(), null); 828 } 829 830 @Override onAudioRouteSelected(int audioRoute)831 public void onAudioRouteSelected(int audioRoute) { 832 LogUtil.i("SurfaceViewVideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute); 833 inCallButtonUiDelegate.setAudioRoute(audioRoute); 834 } 835 836 @Override setPrimary(@onNull PrimaryInfo primaryInfo)837 public void setPrimary(@NonNull PrimaryInfo primaryInfo) { 838 LogUtil.i("SurfaceViewVideoCallFragment.setPrimary", primaryInfo.toString()); 839 contactGridManager.setPrimary(primaryInfo); 840 } 841 842 @Override setSecondary(@onNull SecondaryInfo secondaryInfo)843 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { 844 LogUtil.i("SurfaceViewVideoCallFragment.setSecondary", secondaryInfo.toString()); 845 if (!isAdded()) { 846 savedSecondaryInfo = secondaryInfo; 847 return; 848 } 849 savedSecondaryInfo = null; 850 switchOnHoldCallController.setSecondaryInfo(secondaryInfo); 851 updateButtonStates(); 852 FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); 853 Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner); 854 if (secondaryInfo.shouldShow) { 855 OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo); 856 onHoldFragment.setPadTopInset(!isInFullscreenMode); 857 transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment); 858 } else { 859 if (oldBanner != null) { 860 transaction.remove(oldBanner); 861 } 862 } 863 transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); 864 transaction.commitAllowingStateLoss(); 865 } 866 867 @Override setCallState(@onNull PrimaryCallState primaryCallState)868 public void setCallState(@NonNull PrimaryCallState primaryCallState) { 869 LogUtil.i("SurfaceViewVideoCallFragment.setCallState", primaryCallState.toString()); 870 contactGridManager.setCallState(primaryCallState); 871 } 872 873 @Override setEndCallButtonEnabled(boolean enabled, boolean animate)874 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 875 LogUtil.i("SurfaceViewVideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled); 876 } 877 878 @Override showManageConferenceCallButton(boolean visible)879 public void showManageConferenceCallButton(boolean visible) { 880 LogUtil.i("SurfaceViewVideoCallFragment.showManageConferenceCallButton", "visible: " + visible); 881 } 882 883 @Override isManageConferenceVisible()884 public boolean isManageConferenceVisible() { 885 LogUtil.i("SurfaceViewVideoCallFragment.isManageConferenceVisible", null); 886 return false; 887 } 888 889 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)890 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 891 contactGridManager.dispatchPopulateAccessibilityEvent(event); 892 } 893 894 @Override showNoteSentToast()895 public void showNoteSentToast() { 896 LogUtil.i("SurfaceViewVideoCallFragment.showNoteSentToast", null); 897 } 898 899 @Override updateInCallScreenColors()900 public void updateInCallScreenColors() { 901 LogUtil.i("SurfaceViewVideoCallFragment.updateColors", null); 902 } 903 904 @Override onInCallScreenDialpadVisibilityChange(boolean isShowing)905 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { 906 LogUtil.i("SurfaceViewVideoCallFragment.onInCallScreenDialpadVisibilityChange", null); 907 } 908 909 @Override getAnswerAndDialpadContainerResourceId()910 public int getAnswerAndDialpadContainerResourceId() { 911 return 0; 912 } 913 914 @Override getInCallScreenFragment()915 public Fragment getInCallScreenFragment() { 916 return this; 917 } 918 919 @Override isShowingLocationUi()920 public boolean isShowingLocationUi() { 921 return false; 922 } 923 924 @Override showLocationUi(Fragment locationUi)925 public void showLocationUi(Fragment locationUi) { 926 LogUtil.e( 927 "SurfaceViewVideoCallFragment.showLocationUi", "Emergency video calling not supported"); 928 // Do nothing 929 } 930 isLandscape()931 private boolean isLandscape() { 932 // Choose orientation based on display orientation, not window orientation 933 int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); 934 return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; 935 } 936 enterGreenScreenMode()937 private void enterGreenScreenMode() { 938 LogUtil.i("SurfaceViewVideoCallFragment.enterGreenScreenMode", null); 939 updateOverlayBackground(); 940 contactGridManager.setIsMiddleRowVisible(true); 941 updateMutePreviewOverlayVisibility(); 942 } 943 exitGreenScreenMode()944 private void exitGreenScreenMode() { 945 LogUtil.i("SurfaceViewVideoCallFragment.exitGreenScreenMode", null); 946 updateOverlayBackground(); 947 contactGridManager.setIsMiddleRowVisible(false); 948 updateMutePreviewOverlayVisibility(); 949 } 950 updateVideoOffViews()951 private void updateVideoOffViews() { 952 // Always hide the preview off and remote off views in green screen mode. 953 boolean previewEnabled = isInGreenScreenMode || shouldShowPreview; 954 previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE); 955 956 boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote; 957 boolean isResumed = remoteEnabled && !isRemotelyHeld; 958 if (isResumed) { 959 boolean wasRemoteVideoOff = 960 TextUtils.equals( 961 remoteVideoOff.getText(), 962 remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off)); 963 // The text needs to be updated and hidden after enough delay in order to be announced by 964 // talkback. 965 remoteVideoOff.setText( 966 wasRemoteVideoOff 967 ? R.string.videocall_remote_video_on 968 : R.string.videocall_remotely_resumed); 969 remoteVideoOff.postDelayed( 970 new Runnable() { 971 @Override 972 public void run() { 973 remoteVideoOff.setVisibility(View.GONE); 974 } 975 }, 976 VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS); 977 } else { 978 remoteVideoOff.setText( 979 isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off); 980 remoteVideoOff.setVisibility(View.VISIBLE); 981 } 982 } 983 updateOverlayBackground()984 private void updateOverlayBackground() { 985 if (isInGreenScreenMode) { 986 // We want to darken the preview view to make text and buttons readable. The fullscreen 987 // background is below the preview view so use the green screen background instead. 988 animateSetVisibility(greenScreenBackgroundView, View.VISIBLE); 989 animateSetVisibility(fullscreenBackgroundView, View.GONE); 990 } else if (!isInFullscreenMode) { 991 // We want to darken the remote view to make text and buttons readable. The green screen 992 // background is above the preview view so it would darken the preview too. Use the fullscreen 993 // background instead. 994 animateSetVisibility(greenScreenBackgroundView, View.GONE); 995 animateSetVisibility(fullscreenBackgroundView, View.VISIBLE); 996 } else { 997 animateSetVisibility(greenScreenBackgroundView, View.GONE); 998 animateSetVisibility(fullscreenBackgroundView, View.GONE); 999 } 1000 } 1001 updateMutePreviewOverlayVisibility()1002 private void updateMutePreviewOverlayVisibility() { 1003 // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen 1004 // mode the preview is fullscreen so there's no where to anchor it. 1005 mutePreviewOverlay.setVisibility( 1006 muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE); 1007 } 1008 animateSetVisibility(final View view, final int visibility)1009 private static void animateSetVisibility(final View view, final int visibility) { 1010 if (view.getVisibility() == visibility) { 1011 return; 1012 } 1013 1014 int startAlpha; 1015 int endAlpha; 1016 if (visibility == View.GONE) { 1017 startAlpha = 1; 1018 endAlpha = 0; 1019 } else if (visibility == View.VISIBLE) { 1020 startAlpha = 0; 1021 endAlpha = 1; 1022 } else { 1023 Assert.fail(); 1024 return; 1025 } 1026 1027 view.setAlpha(startAlpha); 1028 view.setVisibility(View.VISIBLE); 1029 view.animate() 1030 .alpha(endAlpha) 1031 .withEndAction( 1032 new Runnable() { 1033 @Override 1034 public void run() { 1035 view.setVisibility(visibility); 1036 } 1037 }) 1038 .start(); 1039 } 1040 1041 @Override onSystemUiVisibilityChange(int visibility)1042 public void onSystemUiVisibilityChange(int visibility) { 1043 boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; 1044 videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible); 1045 if (navBarVisible) { 1046 updateFullscreenAndGreenScreenMode( 1047 false /* shouldShowFullscreen */, false /* shouldShowGreenScreen */); 1048 } else { 1049 updateFullscreenAndGreenScreenMode( 1050 true /* shouldShowFullscreen */, false /* shouldShowGreenScreen */); 1051 } 1052 } 1053 1054 @Override onCameraPermissionGranted()1055 public void onCameraPermissionGranted() { 1056 videoCallScreenDelegate.onCameraPermissionGranted(); 1057 } 1058 checkCameraPermission()1059 private void checkCameraPermission() { 1060 // Checks if user has consent of camera permission and the permission is granted. 1061 // If camera permission is revoked, shows system permission dialog. 1062 // If camera permission is granted but user doesn't have consent of camera permission 1063 // (which means it's first time making video call), shows custom dialog instead. This 1064 // will only be shown to user once. 1065 if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) { 1066 videoCallScreenDelegate.onCameraPermissionDialogShown(); 1067 if (!VideoUtils.hasCameraPermission(getContext())) { 1068 requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE); 1069 } else { 1070 CameraPermissionDialogFragment.newInstance() 1071 .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG); 1072 } 1073 } 1074 } 1075 } 1076