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.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.Outline; 25 import android.graphics.Point; 26 import android.graphics.drawable.Animatable; 27 import android.os.Bundle; 28 import android.os.SystemClock; 29 import android.renderscript.Allocation; 30 import android.renderscript.Element; 31 import android.renderscript.RenderScript; 32 import android.renderscript.ScriptIntrinsicBlur; 33 import android.support.annotation.ColorInt; 34 import android.support.annotation.NonNull; 35 import android.support.annotation.Nullable; 36 import android.support.annotation.VisibleForTesting; 37 import android.support.v4.app.Fragment; 38 import android.support.v4.app.FragmentTransaction; 39 import android.support.v4.view.animation.FastOutLinearInInterpolator; 40 import android.support.v4.view.animation.LinearOutSlowInInterpolator; 41 import android.telecom.CallAudioState; 42 import android.text.TextUtils; 43 import android.view.LayoutInflater; 44 import android.view.Surface; 45 import android.view.TextureView; 46 import android.view.View; 47 import android.view.View.OnClickListener; 48 import android.view.View.OnLayoutChangeListener; 49 import android.view.View.OnSystemUiVisibilityChangeListener; 50 import android.view.ViewGroup; 51 import android.view.ViewGroup.MarginLayoutParams; 52 import android.view.ViewOutlineProvider; 53 import android.view.accessibility.AccessibilityEvent; 54 import android.view.animation.AccelerateDecelerateInterpolator; 55 import android.view.animation.Interpolator; 56 import android.widget.ImageButton; 57 import android.widget.ImageView; 58 import android.widget.RelativeLayout; 59 import android.widget.TextView; 60 import com.android.dialer.common.Assert; 61 import com.android.dialer.common.FragmentUtils; 62 import com.android.dialer.common.LogUtil; 63 import com.android.dialer.util.PermissionsUtil; 64 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; 65 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; 66 import com.android.incallui.contactgrid.ContactGridManager; 67 import com.android.incallui.hold.OnHoldFragment; 68 import com.android.incallui.incall.protocol.InCallButtonIds; 69 import com.android.incallui.incall.protocol.InCallButtonIdsExtension; 70 import com.android.incallui.incall.protocol.InCallButtonUi; 71 import com.android.incallui.incall.protocol.InCallButtonUiDelegate; 72 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; 73 import com.android.incallui.incall.protocol.InCallScreen; 74 import com.android.incallui.incall.protocol.InCallScreenDelegate; 75 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; 76 import com.android.incallui.incall.protocol.PrimaryCallState; 77 import com.android.incallui.incall.protocol.PrimaryInfo; 78 import com.android.incallui.incall.protocol.SecondaryInfo; 79 import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener; 80 import com.android.incallui.video.protocol.VideoCallScreen; 81 import com.android.incallui.video.protocol.VideoCallScreenDelegate; 82 import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory; 83 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings; 84 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture; 85 import com.android.incallui.videotech.utils.VideoUtils; 86 87 /** Contains UI elements for a video call. */ 88 89 public class VideoCallFragment extends Fragment 90 implements InCallScreen, 91 InCallButtonUi, 92 VideoCallScreen, 93 OnClickListener, 94 OnCheckedChangeListener, 95 AudioRouteSelectorPresenter, 96 OnSystemUiVisibilityChangeListener { 97 98 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 99 static final String ARG_CALL_ID = "call_id"; 100 101 private static final String TAG_VIDEO_CHARGES_ALERT = "tag_video_charges_alert"; 102 103 @VisibleForTesting static final float BLUR_PREVIEW_RADIUS = 16.0f; 104 @VisibleForTesting static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f; 105 private static final float BLUR_REMOTE_RADIUS = 25.0f; 106 private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f; 107 private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f; 108 109 private static final int CAMERA_PERMISSION_REQUEST_CODE = 1; 110 private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L; 111 private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L; 112 private static final long VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS = 500L; 113 114 private final ViewOutlineProvider circleOutlineProvider = 115 new ViewOutlineProvider() { 116 @Override 117 public void getOutline(View view, Outline outline) { 118 int x = view.getWidth() / 2; 119 int y = view.getHeight() / 2; 120 int radius = Math.min(x, y); 121 outline.setOval(x - radius, y - radius, x + radius, y + radius); 122 } 123 }; 124 125 private InCallScreenDelegate inCallScreenDelegate; 126 private VideoCallScreenDelegate videoCallScreenDelegate; 127 private InCallButtonUiDelegate inCallButtonUiDelegate; 128 private View endCallButton; 129 private CheckableImageButton speakerButton; 130 private SpeakerButtonController speakerButtonController; 131 private CheckableImageButton muteButton; 132 private CheckableImageButton cameraOffButton; 133 private ImageButton swapCameraButton; 134 private View switchOnHoldButton; 135 private View onHoldContainer; 136 private SwitchOnHoldCallController switchOnHoldCallController; 137 private TextView remoteVideoOff; 138 private ImageView remoteOffBlurredImageView; 139 private View mutePreviewOverlay; 140 private View previewOffOverlay; 141 private ImageView previewOffBlurredImageView; 142 private View controls; 143 private View controlsContainer; 144 private TextureView previewTextureView; 145 private TextureView remoteTextureView; 146 private View greenScreenBackgroundView; 147 private View fullscreenBackgroundView; 148 private boolean shouldShowRemote; 149 private boolean shouldShowPreview; 150 private boolean isInFullscreenMode; 151 private boolean isInGreenScreenMode; 152 private boolean hasInitializedScreenModes; 153 private boolean isRemotelyHeld; 154 private ContactGridManager contactGridManager; 155 private SecondaryInfo savedSecondaryInfo; 156 private final Runnable cameraPermissionDialogRunnable = 157 new Runnable() { 158 @Override 159 public void run() { 160 if (videoCallScreenDelegate.shouldShowCameraPermissionToast()) { 161 LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog"); 162 checkCameraPermission(); 163 } 164 } 165 }; 166 167 private final Runnable videoChargesAlertDialogRunnable = 168 () -> { 169 VideoChargesAlertDialogFragment existingVideoChargesAlertFragment = 170 (VideoChargesAlertDialogFragment) 171 getChildFragmentManager().findFragmentByTag(TAG_VIDEO_CHARGES_ALERT); 172 if (existingVideoChargesAlertFragment != null) { 173 LogUtil.i( 174 "VideoCallFragment.videoChargesAlertDialogRunnable", "already shown for this call"); 175 return; 176 } 177 178 if (VideoChargesAlertDialogFragment.shouldShow(getContext(), getCallId())) { 179 LogUtil.i("VideoCallFragment.videoChargesAlertDialogRunnable", "showing dialog"); 180 VideoChargesAlertDialogFragment.newInstance(getCallId()) 181 .show(getChildFragmentManager(), TAG_VIDEO_CHARGES_ALERT); 182 } 183 }; 184 newInstance(String callId)185 public static VideoCallFragment newInstance(String callId) { 186 Bundle bundle = new Bundle(); 187 bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId)); 188 189 VideoCallFragment instance = new VideoCallFragment(); 190 instance.setArguments(bundle); 191 return instance; 192 } 193 194 @Override onCreate(@ullable Bundle savedInstanceState)195 public void onCreate(@Nullable Bundle savedInstanceState) { 196 super.onCreate(savedInstanceState); 197 LogUtil.i("VideoCallFragment.onCreate", null); 198 199 inCallButtonUiDelegate = 200 FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) 201 .newInCallButtonUiDelegate(); 202 if (savedInstanceState != null) { 203 inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); 204 } 205 } 206 207 @Override onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)208 public void onRequestPermissionsResult( 209 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 210 if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { 211 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 212 LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted."); 213 videoCallScreenDelegate.onCameraPermissionGranted(); 214 } else { 215 LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied."); 216 } 217 } 218 super.onRequestPermissionsResult(requestCode, permissions, grantResults); 219 } 220 221 @Nullable 222 @Override onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)223 public View onCreateView( 224 LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { 225 LogUtil.i("VideoCallFragment.onCreateView", null); 226 227 View view = 228 layoutInflater.inflate( 229 isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall, 230 viewGroup, 231 false); 232 contactGridManager = 233 new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */); 234 235 controls = view.findViewById(R.id.videocall_video_controls); 236 controls.setVisibility(getActivity().isInMultiWindowMode() ? View.GONE : View.VISIBLE); 237 controlsContainer = view.findViewById(R.id.videocall_video_controls_container); 238 speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button); 239 muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button); 240 muteButton.setOnCheckedChangeListener(this); 241 mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay); 242 cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video); 243 cameraOffButton.setOnCheckedChangeListener(this); 244 previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay); 245 previewOffBlurredImageView = 246 (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view); 247 swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video); 248 swapCameraButton.setOnClickListener(this); 249 view.findViewById(R.id.videocall_switch_controls) 250 .setVisibility(getActivity().isInMultiWindowMode() ? View.GONE : View.VISIBLE); 251 switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold); 252 onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner); 253 remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off); 254 remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 255 remoteOffBlurredImageView = 256 (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view); 257 endCallButton = view.findViewById(R.id.videocall_end_call); 258 endCallButton.setOnClickListener(this); 259 previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview); 260 previewTextureView.setClipToOutline(true); 261 previewOffOverlay.setOnClickListener( 262 new OnClickListener() { 263 @Override 264 public void onClick(View v) { 265 checkCameraPermission(); 266 } 267 }); 268 remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote); 269 greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background); 270 fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background); 271 272 remoteTextureView.addOnLayoutChangeListener( 273 new OnLayoutChangeListener() { 274 @Override 275 public void onLayoutChange( 276 View v, 277 int left, 278 int top, 279 int right, 280 int bottom, 281 int oldLeft, 282 int oldTop, 283 int oldRight, 284 int oldBottom) { 285 LogUtil.i("VideoCallFragment.onLayoutChange", "remoteTextureView layout changed"); 286 updateRemoteVideoScaling(); 287 updateRemoteOffView(); 288 } 289 }); 290 291 previewTextureView.addOnLayoutChangeListener( 292 new OnLayoutChangeListener() { 293 @Override 294 public void onLayoutChange( 295 View v, 296 int left, 297 int top, 298 int right, 299 int bottom, 300 int oldLeft, 301 int oldTop, 302 int oldRight, 303 int oldBottom) { 304 LogUtil.i("VideoCallFragment.onLayoutChange", "previewTextureView layout changed"); 305 updatePreviewVideoScaling(); 306 updatePreviewOffView(); 307 } 308 }); 309 310 controls.addOnLayoutChangeListener( 311 new OnLayoutChangeListener() { 312 @Override 313 public void onLayoutChange( 314 View v, 315 int left, 316 int top, 317 int right, 318 int bottom, 319 int oldLeft, 320 int oldTop, 321 int oldRight, 322 int oldBottom) { 323 LogUtil.i("VideoCallFragment.onLayoutChange", "controls layout changed"); 324 if (getActivity() != null && getView() != null) { 325 controls.removeOnLayoutChangeListener(this); 326 if (isInFullscreenMode) { 327 enterFullscreenMode(); 328 } 329 } 330 } 331 }); 332 333 return view; 334 } 335 336 @Override onViewCreated(View view, @Nullable Bundle bundle)337 public void onViewCreated(View view, @Nullable Bundle bundle) { 338 super.onViewCreated(view, bundle); 339 LogUtil.i("VideoCallFragment.onViewCreated", null); 340 341 inCallScreenDelegate = 342 FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) 343 .newInCallScreenDelegate(); 344 videoCallScreenDelegate = 345 FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class) 346 .newVideoCallScreenDelegate(this); 347 348 speakerButtonController = 349 new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate); 350 switchOnHoldCallController = 351 new SwitchOnHoldCallController( 352 switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate); 353 354 videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this); 355 356 inCallScreenDelegate.onInCallScreenDelegateInit(this); 357 inCallScreenDelegate.onInCallScreenReady(); 358 inCallButtonUiDelegate.onInCallButtonUiReady(this); 359 360 view.setOnSystemUiVisibilityChangeListener(this); 361 362 if (videoCallScreenDelegate.isFullscreen()) { 363 controls.setVisibility(View.INVISIBLE); 364 contactGridManager.getContainerView().setVisibility(View.INVISIBLE); 365 endCallButton.setVisibility(View.INVISIBLE); 366 } 367 } 368 369 @Override onSaveInstanceState(Bundle outState)370 public void onSaveInstanceState(Bundle outState) { 371 super.onSaveInstanceState(outState); 372 inCallButtonUiDelegate.onSaveInstanceState(outState); 373 } 374 375 @Override onDestroyView()376 public void onDestroyView() { 377 super.onDestroyView(); 378 LogUtil.i("VideoCallFragment.onDestroyView", null); 379 inCallButtonUiDelegate.onInCallButtonUiUnready(); 380 inCallScreenDelegate.onInCallScreenUnready(); 381 } 382 383 @Override onAttach(Context context)384 public void onAttach(Context context) { 385 super.onAttach(context); 386 if (savedSecondaryInfo != null) { 387 setSecondary(savedSecondaryInfo); 388 } 389 } 390 391 @Override onStart()392 public void onStart() { 393 super.onStart(); 394 LogUtil.i("VideoCallFragment.onStart", null); 395 onVideoScreenStart(); 396 } 397 398 @Override onVideoScreenStart()399 public void onVideoScreenStart() { 400 videoCallScreenDelegate.onVideoCallScreenUiReady(); 401 getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS); 402 getView() 403 .postDelayed(videoChargesAlertDialogRunnable, VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS); 404 } 405 406 @Override onResume()407 public void onResume() { 408 super.onResume(); 409 LogUtil.i("VideoCallFragment.onResume", null); 410 inCallScreenDelegate.onInCallScreenResumed(); 411 } 412 413 @Override onPause()414 public void onPause() { 415 super.onPause(); 416 LogUtil.i("VideoCallFragment.onPause", null); 417 inCallScreenDelegate.onInCallScreenPaused(); 418 } 419 420 @Override onStop()421 public void onStop() { 422 super.onStop(); 423 LogUtil.i("VideoCallFragment.onStop", null); 424 onVideoScreenStop(); 425 } 426 427 @Override onVideoScreenStop()428 public void onVideoScreenStop() { 429 getView().removeCallbacks(videoChargesAlertDialogRunnable); 430 getView().removeCallbacks(cameraPermissionDialogRunnable); 431 videoCallScreenDelegate.onVideoCallScreenUiUnready(); 432 } 433 exitFullscreenMode()434 private void exitFullscreenMode() { 435 LogUtil.i("VideoCallFragment.exitFullscreenMode", null); 436 437 if (!getView().isAttachedToWindow()) { 438 LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached"); 439 return; 440 } 441 442 showSystemUI(); 443 444 LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); 445 446 // Animate the controls to the shown state. 447 controls 448 .animate() 449 .translationX(0) 450 .translationY(0) 451 .setInterpolator(linearOutSlowInInterpolator) 452 .alpha(1) 453 .withStartAction( 454 new Runnable() { 455 @Override 456 public void run() { 457 controls.setVisibility(View.VISIBLE); 458 } 459 }) 460 .start(); 461 462 // Animate onHold to the shown state. 463 switchOnHoldButton 464 .animate() 465 .translationX(0) 466 .translationY(0) 467 .setInterpolator(linearOutSlowInInterpolator) 468 .alpha(1) 469 .withStartAction( 470 new Runnable() { 471 @Override 472 public void run() { 473 switchOnHoldCallController.setOnScreen(); 474 } 475 }); 476 477 View contactGridView = contactGridManager.getContainerView(); 478 // Animate contact grid to the shown state. 479 contactGridView 480 .animate() 481 .translationX(0) 482 .translationY(0) 483 .setInterpolator(linearOutSlowInInterpolator) 484 .alpha(1) 485 .withStartAction( 486 new Runnable() { 487 @Override 488 public void run() { 489 contactGridManager.show(); 490 } 491 }); 492 493 endCallButton 494 .animate() 495 .translationX(0) 496 .translationY(0) 497 .setInterpolator(linearOutSlowInInterpolator) 498 .alpha(1) 499 .withStartAction( 500 new Runnable() { 501 @Override 502 public void run() { 503 endCallButton.setVisibility(View.VISIBLE); 504 } 505 }) 506 .start(); 507 508 // Animate all the preview controls up to make room for the navigation bar. 509 // In green screen mode we don't need this because the preview takes up the whole screen and has 510 // a fixed position. 511 if (!isInGreenScreenMode) { 512 Point previewOffsetStartShown = getPreviewOffsetStartShown(); 513 for (View view : getAllPreviewRelatedViews()) { 514 // Animate up with the preview offset above the navigation bar. 515 view.animate() 516 .translationX(previewOffsetStartShown.x) 517 .translationY(previewOffsetStartShown.y) 518 .setInterpolator(new AccelerateDecelerateInterpolator()) 519 .start(); 520 } 521 } 522 523 updateOverlayBackground(); 524 } 525 showSystemUI()526 private void showSystemUI() { 527 View view = getView(); 528 if (view != null) { 529 // Code is more expressive with all flags present, even though some may be combined 530 // noinspection PointlessBitwiseExpression 531 view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 532 } 533 } 534 535 /** Set view flags to hide the system UI. System UI will return on any touch event */ hideSystemUI()536 private void hideSystemUI() { 537 View view = getView(); 538 if (view != null) { 539 view.setSystemUiVisibility( 540 View.SYSTEM_UI_FLAG_FULLSCREEN 541 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 542 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 543 } 544 } 545 getControlsOffsetEndHidden(View controls)546 private Point getControlsOffsetEndHidden(View controls) { 547 if (isLandscape()) { 548 return new Point(0, getOffsetBottom(controls)); 549 } else { 550 return new Point(getOffsetStart(controls), 0); 551 } 552 } 553 getSwitchOnHoldOffsetEndHidden(View swapCallButton)554 private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) { 555 if (isLandscape()) { 556 return new Point(0, getOffsetTop(swapCallButton)); 557 } else { 558 return new Point(getOffsetEnd(swapCallButton), 0); 559 } 560 } 561 getContactGridOffsetEndHidden(View view)562 private Point getContactGridOffsetEndHidden(View view) { 563 return new Point(0, getOffsetTop(view)); 564 } 565 getEndCallOffsetEndHidden(View endCallButton)566 private Point getEndCallOffsetEndHidden(View endCallButton) { 567 if (isLandscape()) { 568 return new Point(getOffsetEnd(endCallButton), 0); 569 } else { 570 return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin); 571 } 572 } 573 getPreviewOffsetStartShown()574 private Point getPreviewOffsetStartShown() { 575 // No insets in multiwindow mode, and rootWindowInsets will get the display's insets. 576 if (getActivity().isInMultiWindowMode()) { 577 return new Point(); 578 } 579 if (isLandscape()) { 580 int systemWindowInsetEnd = 581 getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL 582 ? getView().getRootWindowInsets().getSystemWindowInsetLeft() 583 : -getView().getRootWindowInsets().getSystemWindowInsetRight(); 584 return new Point(systemWindowInsetEnd, 0); 585 } else { 586 return new Point(0, -getView().getRootWindowInsets().getSystemWindowInsetBottom()); 587 } 588 } 589 getAllPreviewRelatedViews()590 private View[] getAllPreviewRelatedViews() { 591 return new View[] { 592 previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay, 593 }; 594 } 595 getOffsetTop(View view)596 private int getOffsetTop(View view) { 597 return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin); 598 } 599 getOffsetBottom(View view)600 private int getOffsetBottom(View view) { 601 return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin; 602 } 603 getOffsetStart(View view)604 private int getOffsetStart(View view) { 605 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart(); 606 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 607 offset = -offset; 608 } 609 return -offset; 610 } 611 getOffsetEnd(View view)612 private int getOffsetEnd(View view) { 613 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd(); 614 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 615 offset = -offset; 616 } 617 return offset; 618 } 619 enterFullscreenMode()620 private void enterFullscreenMode() { 621 LogUtil.i("VideoCallFragment.enterFullscreenMode", null); 622 623 hideSystemUI(); 624 625 Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator(); 626 627 // Animate controls to the hidden state. 628 Point offset = getControlsOffsetEndHidden(controls); 629 controls 630 .animate() 631 .translationX(offset.x) 632 .translationY(offset.y) 633 .setInterpolator(fastOutLinearInInterpolator) 634 .alpha(0) 635 .start(); 636 637 // Animate onHold to the hidden state. 638 offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton); 639 switchOnHoldButton 640 .animate() 641 .translationX(offset.x) 642 .translationY(offset.y) 643 .setInterpolator(fastOutLinearInInterpolator) 644 .alpha(0); 645 646 View contactGridView = contactGridManager.getContainerView(); 647 // Animate contact grid to the hidden state. 648 offset = getContactGridOffsetEndHidden(contactGridView); 649 contactGridView 650 .animate() 651 .translationX(offset.x) 652 .translationY(offset.y) 653 .setInterpolator(fastOutLinearInInterpolator) 654 .alpha(0); 655 656 offset = getEndCallOffsetEndHidden(endCallButton); 657 // Use a fast out interpolator to quickly fade out the button. This is important because the 658 // button can't draw under the navigation bar which means that it'll look weird if it just 659 // abruptly disappears when it reaches the edge of the naivgation bar. 660 endCallButton 661 .animate() 662 .translationX(offset.x) 663 .translationY(offset.y) 664 .setInterpolator(fastOutLinearInInterpolator) 665 .alpha(0) 666 .withEndAction( 667 new Runnable() { 668 @Override 669 public void run() { 670 endCallButton.setVisibility(View.INVISIBLE); 671 } 672 }) 673 .setInterpolator(new FastOutLinearInInterpolator()) 674 .start(); 675 676 // Animate all the preview controls down now that the navigation bar is hidden. 677 // In green screen mode we don't need this because the preview takes up the whole screen and has 678 // a fixed position. 679 if (!isInGreenScreenMode) { 680 for (View view : getAllPreviewRelatedViews()) { 681 // Animate down with the navigation bar hidden. 682 view.animate() 683 .translationX(0) 684 .translationY(0) 685 .setInterpolator(new AccelerateDecelerateInterpolator()) 686 .start(); 687 } 688 } 689 updateOverlayBackground(); 690 } 691 692 @Override onClick(View v)693 public void onClick(View v) { 694 if (v == endCallButton) { 695 LogUtil.i("VideoCallFragment.onClick", "end call button clicked"); 696 inCallButtonUiDelegate.onEndCallClicked(); 697 videoCallScreenDelegate.resetAutoFullscreenTimer(); 698 } else if (v == swapCameraButton) { 699 if (swapCameraButton.getDrawable() instanceof Animatable) { 700 ((Animatable) swapCameraButton.getDrawable()).start(); 701 } 702 inCallButtonUiDelegate.toggleCameraClicked(); 703 videoCallScreenDelegate.resetAutoFullscreenTimer(); 704 } 705 } 706 707 @Override onCheckedChanged(CheckableImageButton button, boolean isChecked)708 public void onCheckedChanged(CheckableImageButton button, boolean isChecked) { 709 if (button == cameraOffButton) { 710 if (!isChecked && !VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) { 711 LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog"); 712 checkCameraPermission(); 713 } else { 714 inCallButtonUiDelegate.pauseVideoClicked(isChecked); 715 videoCallScreenDelegate.resetAutoFullscreenTimer(); 716 } 717 } else if (button == muteButton) { 718 inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */); 719 videoCallScreenDelegate.resetAutoFullscreenTimer(); 720 } 721 } 722 723 @Override showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)724 public void showVideoViews( 725 boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) { 726 LogUtil.i( 727 "VideoCallFragment.showVideoViews", 728 "showPreview: %b, shouldShowRemote: %b", 729 shouldShowPreview, 730 shouldShowRemote); 731 732 videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView); 733 videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView); 734 735 boolean updateRemoteOffView = false; 736 if (this.shouldShowRemote != shouldShowRemote) { 737 this.shouldShowRemote = shouldShowRemote; 738 updateRemoteOffView = true; 739 } 740 if (this.isRemotelyHeld != isRemotelyHeld) { 741 this.isRemotelyHeld = isRemotelyHeld; 742 updateRemoteOffView = true; 743 } 744 745 if (updateRemoteOffView) { 746 updateRemoteOffView(); 747 } 748 if (this.shouldShowPreview != shouldShowPreview) { 749 this.shouldShowPreview = shouldShowPreview; 750 updatePreviewOffView(); 751 } 752 } 753 754 @Override onLocalVideoDimensionsChanged()755 public void onLocalVideoDimensionsChanged() { 756 LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null); 757 updatePreviewVideoScaling(); 758 } 759 760 @Override onLocalVideoOrientationChanged()761 public void onLocalVideoOrientationChanged() { 762 LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null); 763 updatePreviewVideoScaling(); 764 } 765 766 /** Called when the remote video's dimensions change. */ 767 @Override onRemoteVideoDimensionsChanged()768 public void onRemoteVideoDimensionsChanged() { 769 LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null); 770 updateRemoteVideoScaling(); 771 } 772 773 @Override updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)774 public void updateFullscreenAndGreenScreenMode( 775 boolean shouldShowFullscreen, boolean shouldShowGreenScreen) { 776 LogUtil.i( 777 "VideoCallFragment.updateFullscreenAndGreenScreenMode", 778 "shouldShowFullscreen: %b, shouldShowGreenScreen: %b", 779 shouldShowFullscreen, 780 shouldShowGreenScreen); 781 782 if (getActivity() == null) { 783 LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity"); 784 return; 785 } 786 787 // Check if anything is actually going to change. The first time this function is called we 788 // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen 789 // and green screen modes to update even if only one has changed. That's because they both 790 // depend on each other. 791 if (hasInitializedScreenModes 792 && shouldShowGreenScreen == isInGreenScreenMode 793 && shouldShowFullscreen == isInFullscreenMode) { 794 LogUtil.i( 795 "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes"); 796 return; 797 } 798 hasInitializedScreenModes = true; 799 isInGreenScreenMode = shouldShowGreenScreen; 800 isInFullscreenMode = shouldShowFullscreen; 801 802 if (getView().isAttachedToWindow() && !getActivity().isInMultiWindowMode()) { 803 controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets()); 804 } 805 if (shouldShowGreenScreen) { 806 enterGreenScreenMode(); 807 } else { 808 exitGreenScreenMode(); 809 } 810 if (shouldShowFullscreen) { 811 enterFullscreenMode(); 812 } else { 813 exitFullscreenMode(); 814 } 815 816 OnHoldFragment onHoldFragment = 817 ((OnHoldFragment) 818 getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner)); 819 if (onHoldFragment != null) { 820 onHoldFragment.setPadTopInset(!isInFullscreenMode); 821 } 822 } 823 824 @Override getVideoCallScreenFragment()825 public Fragment getVideoCallScreenFragment() { 826 return this; 827 } 828 829 @Override 830 @NonNull getCallId()831 public String getCallId() { 832 return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); 833 } 834 835 @Override onHandoverFromWiFiToLte()836 public void onHandoverFromWiFiToLte() { 837 getView().post(videoChargesAlertDialogRunnable); 838 } 839 840 @Override showButton(@nCallButtonIds int buttonId, boolean show)841 public void showButton(@InCallButtonIds int buttonId, boolean show) { 842 LogUtil.v( 843 "VideoCallFragment.showButton", 844 "buttonId: %s, show: %b", 845 InCallButtonIdsExtension.toString(buttonId), 846 show); 847 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 848 speakerButtonController.setEnabled(show); 849 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 850 muteButton.setEnabled(show); 851 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 852 cameraOffButton.setEnabled(show); 853 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 854 switchOnHoldCallController.setVisible(show); 855 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) { 856 swapCameraButton.setEnabled(show); 857 } 858 } 859 860 @Override enableButton(@nCallButtonIds int buttonId, boolean enable)861 public void enableButton(@InCallButtonIds int buttonId, boolean enable) { 862 LogUtil.v( 863 "VideoCallFragment.setEnabled", 864 "buttonId: %s, enable: %b", 865 InCallButtonIdsExtension.toString(buttonId), 866 enable); 867 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 868 speakerButtonController.setEnabled(enable); 869 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 870 muteButton.setEnabled(enable); 871 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 872 cameraOffButton.setEnabled(enable); 873 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 874 switchOnHoldCallController.setEnabled(enable); 875 } 876 } 877 878 @Override setEnabled(boolean enabled)879 public void setEnabled(boolean enabled) { 880 LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled); 881 speakerButtonController.setEnabled(enabled); 882 muteButton.setEnabled(enabled); 883 cameraOffButton.setEnabled(enabled); 884 switchOnHoldCallController.setEnabled(enabled); 885 } 886 887 @Override setHold(boolean value)888 public void setHold(boolean value) { 889 LogUtil.i("VideoCallFragment.setHold", "value: " + value); 890 } 891 892 @Override setCameraSwitched(boolean isBackFacingCamera)893 public void setCameraSwitched(boolean isBackFacingCamera) { 894 LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera); 895 } 896 897 @Override setVideoPaused(boolean isPaused)898 public void setVideoPaused(boolean isPaused) { 899 LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused); 900 cameraOffButton.setChecked(isPaused); 901 } 902 903 @Override setAudioState(CallAudioState audioState)904 public void setAudioState(CallAudioState audioState) { 905 LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState); 906 speakerButtonController.setAudioState(audioState); 907 muteButton.setChecked(audioState.isMuted()); 908 updateMutePreviewOverlayVisibility(); 909 } 910 911 @Override updateButtonStates()912 public void updateButtonStates() { 913 LogUtil.i("VideoCallFragment.updateButtonState", null); 914 speakerButtonController.updateButtonState(); 915 switchOnHoldCallController.updateButtonState(); 916 } 917 918 @Override updateInCallButtonUiColors(@olorInt int color)919 public void updateInCallButtonUiColors(@ColorInt int color) {} 920 921 @Override getInCallButtonUiFragment()922 public Fragment getInCallButtonUiFragment() { 923 return this; 924 } 925 926 @Override showAudioRouteSelector()927 public void showAudioRouteSelector() { 928 LogUtil.i("VideoCallFragment.showAudioRouteSelector", null); 929 AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) 930 .show(getChildFragmentManager(), null); 931 } 932 933 @Override onAudioRouteSelected(int audioRoute)934 public void onAudioRouteSelected(int audioRoute) { 935 LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute); 936 inCallButtonUiDelegate.setAudioRoute(audioRoute); 937 } 938 939 @Override onAudioRouteSelectorDismiss()940 public void onAudioRouteSelectorDismiss() {} 941 942 @Override setPrimary(@onNull PrimaryInfo primaryInfo)943 public void setPrimary(@NonNull PrimaryInfo primaryInfo) { 944 LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString()); 945 contactGridManager.setPrimary(primaryInfo); 946 } 947 948 @Override setSecondary(@onNull SecondaryInfo secondaryInfo)949 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { 950 LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString()); 951 if (!isAdded()) { 952 savedSecondaryInfo = secondaryInfo; 953 return; 954 } 955 savedSecondaryInfo = null; 956 switchOnHoldCallController.setSecondaryInfo(secondaryInfo); 957 updateButtonStates(); 958 FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); 959 Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner); 960 if (secondaryInfo.shouldShow()) { 961 OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo); 962 onHoldFragment.setPadTopInset(!isInFullscreenMode); 963 transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment); 964 } else { 965 if (oldBanner != null) { 966 transaction.remove(oldBanner); 967 } 968 } 969 transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); 970 transaction.commitAllowingStateLoss(); 971 } 972 973 @Override setCallState(@onNull PrimaryCallState primaryCallState)974 public void setCallState(@NonNull PrimaryCallState primaryCallState) { 975 LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString()); 976 contactGridManager.setCallState(primaryCallState); 977 } 978 979 @Override setEndCallButtonEnabled(boolean enabled, boolean animate)980 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 981 LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled); 982 } 983 984 @Override showManageConferenceCallButton(boolean visible)985 public void showManageConferenceCallButton(boolean visible) { 986 LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible); 987 } 988 989 @Override isManageConferenceVisible()990 public boolean isManageConferenceVisible() { 991 LogUtil.i("VideoCallFragment.isManageConferenceVisible", null); 992 return false; 993 } 994 995 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)996 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 997 contactGridManager.dispatchPopulateAccessibilityEvent(event); 998 } 999 1000 @Override showNoteSentToast()1001 public void showNoteSentToast() { 1002 LogUtil.i("VideoCallFragment.showNoteSentToast", null); 1003 } 1004 1005 @Override updateInCallScreenColors()1006 public void updateInCallScreenColors() { 1007 LogUtil.i("VideoCallFragment.updateColors", null); 1008 } 1009 1010 @Override onInCallScreenDialpadVisibilityChange(boolean isShowing)1011 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { 1012 LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null); 1013 } 1014 1015 @Override getAnswerAndDialpadContainerResourceId()1016 public int getAnswerAndDialpadContainerResourceId() { 1017 return 0; 1018 } 1019 1020 @Override getInCallScreenFragment()1021 public Fragment getInCallScreenFragment() { 1022 return this; 1023 } 1024 1025 @Override isShowingLocationUi()1026 public boolean isShowingLocationUi() { 1027 return false; 1028 } 1029 1030 @Override showLocationUi(Fragment locationUi)1031 public void showLocationUi(Fragment locationUi) { 1032 LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported"); 1033 // Do nothing 1034 } 1035 updatePreviewVideoScaling()1036 private void updatePreviewVideoScaling() { 1037 if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) { 1038 LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet"); 1039 return; 1040 } 1041 VideoSurfaceTexture localVideoSurfaceTexture = 1042 videoCallScreenDelegate.getLocalVideoSurfaceTexture(); 1043 Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions(); 1044 if (cameraDimensions == null) { 1045 LogUtil.i( 1046 "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set"); 1047 return; 1048 } 1049 if (isLandscape()) { 1050 VideoSurfaceBindings.scaleVideoAndFillView( 1051 previewTextureView, 1052 cameraDimensions.x, 1053 cameraDimensions.y, 1054 videoCallScreenDelegate.getDeviceOrientation()); 1055 } else { 1056 VideoSurfaceBindings.scaleVideoAndFillView( 1057 previewTextureView, 1058 cameraDimensions.y, 1059 cameraDimensions.x, 1060 videoCallScreenDelegate.getDeviceOrientation()); 1061 } 1062 } 1063 updateRemoteVideoScaling()1064 private void updateRemoteVideoScaling() { 1065 VideoSurfaceTexture remoteVideoSurfaceTexture = 1066 videoCallScreenDelegate.getRemoteVideoSurfaceTexture(); 1067 Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions(); 1068 if (videoSize == null) { 1069 LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null"); 1070 return; 1071 } 1072 if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) { 1073 LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet"); 1074 return; 1075 } 1076 1077 // If the video and display aspect ratio's are close then scale video to fill display 1078 float videoAspectRatio = ((float) videoSize.x) / videoSize.y; 1079 float displayAspectRatio = 1080 ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight(); 1081 float delta = Math.abs(videoAspectRatio - displayAspectRatio); 1082 float sum = videoAspectRatio + displayAspectRatio; 1083 if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) { 1084 VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0); 1085 } else { 1086 VideoSurfaceBindings.scaleVideoMaintainingAspectRatio( 1087 remoteTextureView, videoSize.x, videoSize.y); 1088 } 1089 } 1090 isLandscape()1091 private boolean isLandscape() { 1092 // Choose orientation based on display orientation, not window orientation 1093 int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); 1094 return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; 1095 } 1096 enterGreenScreenMode()1097 private void enterGreenScreenMode() { 1098 LogUtil.i("VideoCallFragment.enterGreenScreenMode", null); 1099 RelativeLayout.LayoutParams params = 1100 new RelativeLayout.LayoutParams( 1101 RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); 1102 params.addRule(RelativeLayout.ALIGN_PARENT_START); 1103 params.addRule(RelativeLayout.ALIGN_PARENT_TOP); 1104 previewTextureView.setLayoutParams(params); 1105 previewTextureView.setOutlineProvider(null); 1106 updateOverlayBackground(); 1107 contactGridManager.setIsMiddleRowVisible(true); 1108 updateMutePreviewOverlayVisibility(); 1109 1110 previewOffBlurredImageView.setLayoutParams(params); 1111 previewOffBlurredImageView.setOutlineProvider(null); 1112 previewOffBlurredImageView.setClipToOutline(false); 1113 } 1114 exitGreenScreenMode()1115 private void exitGreenScreenMode() { 1116 LogUtil.i("VideoCallFragment.exitGreenScreenMode", null); 1117 Resources resources = getResources(); 1118 RelativeLayout.LayoutParams params = 1119 new RelativeLayout.LayoutParams( 1120 (int) resources.getDimension(R.dimen.videocall_preview_width), 1121 (int) resources.getDimension(R.dimen.videocall_preview_height)); 1122 params.setMargins( 1123 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom)); 1124 if (isLandscape()) { 1125 params.addRule(RelativeLayout.ALIGN_PARENT_END); 1126 params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end)); 1127 } else { 1128 params.addRule(RelativeLayout.ALIGN_PARENT_START); 1129 params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start)); 1130 } 1131 params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 1132 previewTextureView.setLayoutParams(params); 1133 previewTextureView.setOutlineProvider(circleOutlineProvider); 1134 updateOverlayBackground(); 1135 contactGridManager.setIsMiddleRowVisible(false); 1136 updateMutePreviewOverlayVisibility(); 1137 1138 previewOffBlurredImageView.setLayoutParams(params); 1139 previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider); 1140 previewOffBlurredImageView.setClipToOutline(true); 1141 } 1142 updatePreviewOffView()1143 private void updatePreviewOffView() { 1144 LogUtil.enterBlock("VideoCallFragment.updatePreviewOffView"); 1145 1146 // Always hide the preview off and remote off views in green screen mode. 1147 boolean previewEnabled = isInGreenScreenMode || shouldShowPreview; 1148 previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE); 1149 updateBlurredImageView( 1150 previewTextureView, 1151 previewOffBlurredImageView, 1152 shouldShowPreview, 1153 BLUR_PREVIEW_RADIUS, 1154 BLUR_PREVIEW_SCALE_FACTOR); 1155 } 1156 updateRemoteOffView()1157 private void updateRemoteOffView() { 1158 LogUtil.enterBlock("VideoCallFragment.updateRemoteOffView"); 1159 boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote; 1160 boolean isResumed = remoteEnabled && !isRemotelyHeld; 1161 if (isResumed) { 1162 boolean wasRemoteVideoOff = 1163 TextUtils.equals( 1164 remoteVideoOff.getText(), 1165 remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off)); 1166 // The text needs to be updated and hidden after enough delay in order to be announced by 1167 // talkback. 1168 remoteVideoOff.setText( 1169 wasRemoteVideoOff 1170 ? R.string.videocall_remote_video_on 1171 : R.string.videocall_remotely_resumed); 1172 remoteVideoOff.postDelayed( 1173 new Runnable() { 1174 @Override 1175 public void run() { 1176 remoteVideoOff.setVisibility(View.GONE); 1177 } 1178 }, 1179 VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS); 1180 } else { 1181 remoteVideoOff.setText( 1182 isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off); 1183 remoteVideoOff.setVisibility(View.VISIBLE); 1184 } 1185 updateBlurredImageView( 1186 remoteTextureView, 1187 remoteOffBlurredImageView, 1188 shouldShowRemote, 1189 BLUR_REMOTE_RADIUS, 1190 BLUR_REMOTE_SCALE_FACTOR); 1191 } 1192 1193 @VisibleForTesting updateBlurredImageView( TextureView textureView, ImageView blurredImageView, boolean isVideoEnabled, float blurRadius, float scaleFactor)1194 void updateBlurredImageView( 1195 TextureView textureView, 1196 ImageView blurredImageView, 1197 boolean isVideoEnabled, 1198 float blurRadius, 1199 float scaleFactor) { 1200 Context context = getContext(); 1201 1202 if (isVideoEnabled || context == null) { 1203 blurredImageView.setImageBitmap(null); 1204 blurredImageView.setVisibility(View.GONE); 1205 return; 1206 } 1207 1208 long startTimeMillis = SystemClock.elapsedRealtime(); 1209 int width = Math.round(textureView.getWidth() * scaleFactor); 1210 int height = Math.round(textureView.getHeight() * scaleFactor); 1211 1212 LogUtil.i("VideoCallFragment.updateBlurredImageView", "width: %d, height: %d", width, height); 1213 1214 // This call takes less than 10 milliseconds. 1215 Bitmap bitmap = textureView.getBitmap(width, height); 1216 1217 if (bitmap == null) { 1218 blurredImageView.setImageBitmap(null); 1219 blurredImageView.setVisibility(View.GONE); 1220 return; 1221 } 1222 1223 // TODO(mdooley): When the view is first displayed after a rotation the bitmap is empty 1224 // and thus this blur has no effect. 1225 // This call can take 100 milliseconds. 1226 blur(getContext(), bitmap, blurRadius); 1227 1228 // TODO(mdooley): Figure out why only have to apply the transform in landscape mode 1229 if (width > height) { 1230 bitmap = 1231 Bitmap.createBitmap( 1232 bitmap, 1233 0, 1234 0, 1235 bitmap.getWidth(), 1236 bitmap.getHeight(), 1237 textureView.getTransform(null), 1238 true); 1239 } 1240 1241 blurredImageView.setImageBitmap(bitmap); 1242 blurredImageView.setVisibility(View.VISIBLE); 1243 1244 LogUtil.i( 1245 "VideoCallFragment.updateBlurredImageView", 1246 "took %d millis", 1247 (SystemClock.elapsedRealtime() - startTimeMillis)); 1248 } 1249 updateOverlayBackground()1250 private void updateOverlayBackground() { 1251 if (isInGreenScreenMode) { 1252 // We want to darken the preview view to make text and buttons readable. The fullscreen 1253 // background is below the preview view so use the green screen background instead. 1254 animateSetVisibility(greenScreenBackgroundView, View.VISIBLE); 1255 animateSetVisibility(fullscreenBackgroundView, View.GONE); 1256 } else if (!isInFullscreenMode) { 1257 // We want to darken the remote view to make text and buttons readable. The green screen 1258 // background is above the preview view so it would darken the preview too. Use the fullscreen 1259 // background instead. 1260 animateSetVisibility(greenScreenBackgroundView, View.GONE); 1261 animateSetVisibility(fullscreenBackgroundView, View.VISIBLE); 1262 } else { 1263 animateSetVisibility(greenScreenBackgroundView, View.GONE); 1264 animateSetVisibility(fullscreenBackgroundView, View.GONE); 1265 } 1266 } 1267 updateMutePreviewOverlayVisibility()1268 private void updateMutePreviewOverlayVisibility() { 1269 // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen 1270 // mode the preview is fullscreen so there's no where to anchor it. 1271 mutePreviewOverlay.setVisibility( 1272 muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE); 1273 } 1274 animateSetVisibility(final View view, final int visibility)1275 private static void animateSetVisibility(final View view, final int visibility) { 1276 if (view.getVisibility() == visibility) { 1277 return; 1278 } 1279 1280 int startAlpha; 1281 int endAlpha; 1282 if (visibility == View.GONE) { 1283 startAlpha = 1; 1284 endAlpha = 0; 1285 } else if (visibility == View.VISIBLE) { 1286 startAlpha = 0; 1287 endAlpha = 1; 1288 } else { 1289 Assert.fail(); 1290 return; 1291 } 1292 1293 view.setAlpha(startAlpha); 1294 view.setVisibility(View.VISIBLE); 1295 view.animate() 1296 .alpha(endAlpha) 1297 .withEndAction( 1298 new Runnable() { 1299 @Override 1300 public void run() { 1301 view.setVisibility(visibility); 1302 } 1303 }) 1304 .start(); 1305 } 1306 blur(Context context, Bitmap image, float blurRadius)1307 private static void blur(Context context, Bitmap image, float blurRadius) { 1308 RenderScript renderScript = RenderScript.create(context); 1309 ScriptIntrinsicBlur blurScript = 1310 ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)); 1311 Allocation allocationIn = Allocation.createFromBitmap(renderScript, image); 1312 Allocation allocationOut = Allocation.createFromBitmap(renderScript, image); 1313 blurScript.setRadius(blurRadius); 1314 blurScript.setInput(allocationIn); 1315 blurScript.forEach(allocationOut); 1316 allocationOut.copyTo(image); 1317 blurScript.destroy(); 1318 allocationIn.destroy(); 1319 allocationOut.destroy(); 1320 } 1321 1322 @Override onSystemUiVisibilityChange(int visibility)1323 public void onSystemUiVisibilityChange(int visibility) { 1324 boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; 1325 videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible); 1326 } 1327 checkCameraPermission()1328 private void checkCameraPermission() { 1329 // Checks if user has consent of camera permission and the permission is granted. 1330 // If camera permission is revoked, shows system permission dialog. 1331 // If camera permission is granted but user doesn't have consent of camera permission 1332 // (which means it's first time making video call), shows custom dialog instead. This 1333 // will only be shown to user once. 1334 if (!VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) { 1335 videoCallScreenDelegate.onCameraPermissionDialogShown(); 1336 if (!VideoUtils.hasCameraPermission(getContext())) { 1337 requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE); 1338 } else { 1339 PermissionsUtil.showCameraPermissionToast(getContext()); 1340 videoCallScreenDelegate.onCameraPermissionGranted(); 1341 } 1342 } 1343 } 1344 } 1345 1346