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