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 return view; 310 } 311 312 @Override onViewCreated(View view, @Nullable Bundle bundle)313 public void onViewCreated(View view, @Nullable Bundle bundle) { 314 super.onViewCreated(view, bundle); 315 LogUtil.i("VideoCallFragment.onViewCreated", null); 316 317 inCallScreenDelegate = 318 FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) 319 .newInCallScreenDelegate(); 320 videoCallScreenDelegate = 321 FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class) 322 .newVideoCallScreenDelegate(this); 323 324 speakerButtonController = 325 new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate); 326 switchOnHoldCallController = 327 new SwitchOnHoldCallController( 328 switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate); 329 330 videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this); 331 332 inCallScreenDelegate.onInCallScreenDelegateInit(this); 333 inCallScreenDelegate.onInCallScreenReady(); 334 inCallButtonUiDelegate.onInCallButtonUiReady(this); 335 336 view.setOnSystemUiVisibilityChangeListener(this); 337 } 338 339 @Override onSaveInstanceState(Bundle outState)340 public void onSaveInstanceState(Bundle outState) { 341 super.onSaveInstanceState(outState); 342 inCallButtonUiDelegate.onSaveInstanceState(outState); 343 } 344 345 @Override onDestroyView()346 public void onDestroyView() { 347 super.onDestroyView(); 348 LogUtil.i("VideoCallFragment.onDestroyView", null); 349 inCallButtonUiDelegate.onInCallButtonUiUnready(); 350 inCallScreenDelegate.onInCallScreenUnready(); 351 } 352 353 @Override onAttach(Context context)354 public void onAttach(Context context) { 355 super.onAttach(context); 356 if (savedSecondaryInfo != null) { 357 setSecondary(savedSecondaryInfo); 358 } 359 } 360 361 @Override onStart()362 public void onStart() { 363 super.onStart(); 364 LogUtil.i("VideoCallFragment.onStart", null); 365 onVideoScreenStart(); 366 } 367 368 @Override onVideoScreenStart()369 public void onVideoScreenStart() { 370 inCallButtonUiDelegate.refreshMuteState(); 371 videoCallScreenDelegate.onVideoCallScreenUiReady(); 372 getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS); 373 getView() 374 .postDelayed(videoChargesAlertDialogRunnable, VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS); 375 } 376 377 @Override onResume()378 public void onResume() { 379 super.onResume(); 380 LogUtil.i("VideoCallFragment.onResume", null); 381 inCallScreenDelegate.onInCallScreenResumed(); 382 } 383 384 @Override onPause()385 public void onPause() { 386 super.onPause(); 387 LogUtil.i("VideoCallFragment.onPause", null); 388 inCallScreenDelegate.onInCallScreenPaused(); 389 } 390 391 @Override onStop()392 public void onStop() { 393 super.onStop(); 394 LogUtil.i("VideoCallFragment.onStop", null); 395 onVideoScreenStop(); 396 } 397 398 @Override onVideoScreenStop()399 public void onVideoScreenStop() { 400 getView().removeCallbacks(videoChargesAlertDialogRunnable); 401 getView().removeCallbacks(cameraPermissionDialogRunnable); 402 videoCallScreenDelegate.onVideoCallScreenUiUnready(); 403 } 404 exitFullscreenMode()405 private void exitFullscreenMode() { 406 LogUtil.i("VideoCallFragment.exitFullscreenMode", null); 407 408 if (!getView().isAttachedToWindow()) { 409 LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached"); 410 return; 411 } 412 413 showSystemUI(); 414 415 LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); 416 417 // Animate the controls to the shown state. 418 controls 419 .animate() 420 .translationX(0) 421 .translationY(0) 422 .setInterpolator(linearOutSlowInInterpolator) 423 .alpha(1) 424 .start(); 425 426 // Animate onHold to the shown state. 427 switchOnHoldButton 428 .animate() 429 .translationX(0) 430 .translationY(0) 431 .setInterpolator(linearOutSlowInInterpolator) 432 .alpha(1) 433 .withStartAction( 434 new Runnable() { 435 @Override 436 public void run() { 437 switchOnHoldCallController.setOnScreen(); 438 } 439 }); 440 441 View contactGridView = contactGridManager.getContainerView(); 442 // Animate contact grid to the shown state. 443 contactGridView 444 .animate() 445 .translationX(0) 446 .translationY(0) 447 .setInterpolator(linearOutSlowInInterpolator) 448 .alpha(1) 449 .withStartAction( 450 new Runnable() { 451 @Override 452 public void run() { 453 contactGridManager.show(); 454 } 455 }); 456 457 endCallButton 458 .animate() 459 .translationX(0) 460 .translationY(0) 461 .setInterpolator(linearOutSlowInInterpolator) 462 .alpha(1) 463 .withStartAction( 464 new Runnable() { 465 @Override 466 public void run() { 467 endCallButton.setVisibility(View.VISIBLE); 468 } 469 }) 470 .start(); 471 472 // Animate all the preview controls up to make room for the navigation bar. 473 // In green screen mode we don't need this because the preview takes up the whole screen and has 474 // a fixed position. 475 if (!isInGreenScreenMode) { 476 Point previewOffsetStartShown = getPreviewOffsetStartShown(); 477 for (View view : getAllPreviewRelatedViews()) { 478 // Animate up with the preview offset above the navigation bar. 479 view.animate() 480 .translationX(previewOffsetStartShown.x) 481 .translationY(previewOffsetStartShown.y) 482 .setInterpolator(new AccelerateDecelerateInterpolator()) 483 .start(); 484 } 485 } 486 487 updateOverlayBackground(); 488 } 489 showSystemUI()490 private void showSystemUI() { 491 View view = getView(); 492 if (view != null) { 493 // Code is more expressive with all flags present, even though some may be combined 494 // noinspection PointlessBitwiseExpression 495 view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 496 } 497 } 498 499 /** Set view flags to hide the system UI. System UI will return on any touch event */ hideSystemUI()500 private void hideSystemUI() { 501 View view = getView(); 502 if (view != null) { 503 view.setSystemUiVisibility( 504 View.SYSTEM_UI_FLAG_FULLSCREEN 505 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 506 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 507 } 508 } 509 getControlsOffsetEndHidden(View controls)510 private Point getControlsOffsetEndHidden(View controls) { 511 if (isLandscape()) { 512 return new Point(0, getOffsetBottom(controls)); 513 } else { 514 return new Point(getOffsetStart(controls), 0); 515 } 516 } 517 getSwitchOnHoldOffsetEndHidden(View swapCallButton)518 private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) { 519 if (isLandscape()) { 520 return new Point(0, getOffsetTop(swapCallButton)); 521 } else { 522 return new Point(getOffsetEnd(swapCallButton), 0); 523 } 524 } 525 getContactGridOffsetEndHidden(View view)526 private Point getContactGridOffsetEndHidden(View view) { 527 return new Point(0, getOffsetTop(view)); 528 } 529 getEndCallOffsetEndHidden(View endCallButton)530 private Point getEndCallOffsetEndHidden(View endCallButton) { 531 if (isLandscape()) { 532 return new Point(getOffsetEnd(endCallButton), 0); 533 } else { 534 return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin); 535 } 536 } 537 getPreviewOffsetStartShown()538 private Point getPreviewOffsetStartShown() { 539 // No insets in multiwindow mode, and rootWindowInsets will get the display's insets. 540 if (getActivity().isInMultiWindowMode()) { 541 return new Point(); 542 } 543 if (isLandscape()) { 544 int stableInsetEnd = 545 getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL 546 ? getView().getRootWindowInsets().getStableInsetLeft() 547 : -getView().getRootWindowInsets().getStableInsetRight(); 548 return new Point(stableInsetEnd, 0); 549 } else { 550 return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom()); 551 } 552 } 553 getAllPreviewRelatedViews()554 private View[] getAllPreviewRelatedViews() { 555 return new View[] { 556 previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay, 557 }; 558 } 559 getOffsetTop(View view)560 private int getOffsetTop(View view) { 561 return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin); 562 } 563 getOffsetBottom(View view)564 private int getOffsetBottom(View view) { 565 return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin; 566 } 567 getOffsetStart(View view)568 private int getOffsetStart(View view) { 569 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart(); 570 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 571 offset = -offset; 572 } 573 return -offset; 574 } 575 getOffsetEnd(View view)576 private int getOffsetEnd(View view) { 577 int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd(); 578 if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 579 offset = -offset; 580 } 581 return offset; 582 } 583 enterFullscreenMode()584 private void enterFullscreenMode() { 585 LogUtil.i("VideoCallFragment.enterFullscreenMode", null); 586 587 hideSystemUI(); 588 589 Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator(); 590 591 // Animate controls to the hidden state. 592 Point offset = getControlsOffsetEndHidden(controls); 593 controls 594 .animate() 595 .translationX(offset.x) 596 .translationY(offset.y) 597 .setInterpolator(fastOutLinearInInterpolator) 598 .alpha(0) 599 .start(); 600 601 // Animate onHold to the hidden state. 602 offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton); 603 switchOnHoldButton 604 .animate() 605 .translationX(offset.x) 606 .translationY(offset.y) 607 .setInterpolator(fastOutLinearInInterpolator) 608 .alpha(0); 609 610 View contactGridView = contactGridManager.getContainerView(); 611 // Animate contact grid to the hidden state. 612 offset = getContactGridOffsetEndHidden(contactGridView); 613 contactGridView 614 .animate() 615 .translationX(offset.x) 616 .translationY(offset.y) 617 .setInterpolator(fastOutLinearInInterpolator) 618 .alpha(0); 619 620 offset = getEndCallOffsetEndHidden(endCallButton); 621 // Use a fast out interpolator to quickly fade out the button. This is important because the 622 // button can't draw under the navigation bar which means that it'll look weird if it just 623 // abruptly disappears when it reaches the edge of the naivgation bar. 624 endCallButton 625 .animate() 626 .translationX(offset.x) 627 .translationY(offset.y) 628 .setInterpolator(fastOutLinearInInterpolator) 629 .alpha(0) 630 .withEndAction( 631 new Runnable() { 632 @Override 633 public void run() { 634 endCallButton.setVisibility(View.INVISIBLE); 635 } 636 }) 637 .setInterpolator(new FastOutLinearInInterpolator()) 638 .start(); 639 640 // Animate all the preview controls down now that the navigation bar is hidden. 641 // In green screen mode we don't need this because the preview takes up the whole screen and has 642 // a fixed position. 643 if (!isInGreenScreenMode) { 644 for (View view : getAllPreviewRelatedViews()) { 645 // Animate down with the navigation bar hidden. 646 view.animate() 647 .translationX(0) 648 .translationY(0) 649 .setInterpolator(new AccelerateDecelerateInterpolator()) 650 .start(); 651 } 652 } 653 updateOverlayBackground(); 654 } 655 656 @Override onClick(View v)657 public void onClick(View v) { 658 if (v == endCallButton) { 659 LogUtil.i("VideoCallFragment.onClick", "end call button clicked"); 660 inCallButtonUiDelegate.onEndCallClicked(); 661 videoCallScreenDelegate.resetAutoFullscreenTimer(); 662 } else if (v == swapCameraButton) { 663 if (swapCameraButton.getDrawable() instanceof Animatable) { 664 ((Animatable) swapCameraButton.getDrawable()).start(); 665 } 666 inCallButtonUiDelegate.toggleCameraClicked(); 667 videoCallScreenDelegate.resetAutoFullscreenTimer(); 668 } 669 } 670 671 @Override onCheckedChanged(CheckableImageButton button, boolean isChecked)672 public void onCheckedChanged(CheckableImageButton button, boolean isChecked) { 673 if (button == cameraOffButton) { 674 if (!isChecked && !VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) { 675 LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog"); 676 checkCameraPermission(); 677 } else { 678 inCallButtonUiDelegate.pauseVideoClicked(isChecked); 679 videoCallScreenDelegate.resetAutoFullscreenTimer(); 680 } 681 } else if (button == muteButton) { 682 inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */); 683 videoCallScreenDelegate.resetAutoFullscreenTimer(); 684 } 685 } 686 687 @Override showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)688 public void showVideoViews( 689 boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) { 690 LogUtil.i( 691 "VideoCallFragment.showVideoViews", 692 "showPreview: %b, shouldShowRemote: %b", 693 shouldShowPreview, 694 shouldShowRemote); 695 696 videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView); 697 videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView); 698 699 this.isRemotelyHeld = isRemotelyHeld; 700 if (this.shouldShowRemote != shouldShowRemote) { 701 this.shouldShowRemote = shouldShowRemote; 702 updateRemoteOffView(); 703 } 704 if (this.shouldShowPreview != shouldShowPreview) { 705 this.shouldShowPreview = shouldShowPreview; 706 updatePreviewOffView(); 707 } 708 } 709 710 @Override onLocalVideoDimensionsChanged()711 public void onLocalVideoDimensionsChanged() { 712 LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null); 713 updatePreviewVideoScaling(); 714 } 715 716 @Override onLocalVideoOrientationChanged()717 public void onLocalVideoOrientationChanged() { 718 LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null); 719 updatePreviewVideoScaling(); 720 } 721 722 /** Called when the remote video's dimensions change. */ 723 @Override onRemoteVideoDimensionsChanged()724 public void onRemoteVideoDimensionsChanged() { 725 LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null); 726 updateRemoteVideoScaling(); 727 } 728 729 @Override updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)730 public void updateFullscreenAndGreenScreenMode( 731 boolean shouldShowFullscreen, boolean shouldShowGreenScreen) { 732 LogUtil.i( 733 "VideoCallFragment.updateFullscreenAndGreenScreenMode", 734 "shouldShowFullscreen: %b, shouldShowGreenScreen: %b", 735 shouldShowFullscreen, 736 shouldShowGreenScreen); 737 738 if (getActivity() == null) { 739 LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity"); 740 return; 741 } 742 743 // Check if anything is actually going to change. The first time this function is called we 744 // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen 745 // and green screen modes to update even if only one has changed. That's because they both 746 // depend on each other. 747 if (hasInitializedScreenModes 748 && shouldShowGreenScreen == isInGreenScreenMode 749 && shouldShowFullscreen == isInFullscreenMode) { 750 LogUtil.i( 751 "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes"); 752 return; 753 } 754 hasInitializedScreenModes = true; 755 isInGreenScreenMode = shouldShowGreenScreen; 756 isInFullscreenMode = shouldShowFullscreen; 757 758 if (getView().isAttachedToWindow() && !getActivity().isInMultiWindowMode()) { 759 controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets()); 760 } 761 if (shouldShowGreenScreen) { 762 enterGreenScreenMode(); 763 } else { 764 exitGreenScreenMode(); 765 } 766 if (shouldShowFullscreen) { 767 enterFullscreenMode(); 768 } else { 769 exitFullscreenMode(); 770 } 771 772 OnHoldFragment onHoldFragment = 773 ((OnHoldFragment) 774 getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner)); 775 if (onHoldFragment != null) { 776 onHoldFragment.setPadTopInset(!isInFullscreenMode); 777 } 778 } 779 780 @Override getVideoCallScreenFragment()781 public Fragment getVideoCallScreenFragment() { 782 return this; 783 } 784 785 @Override 786 @NonNull getCallId()787 public String getCallId() { 788 return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); 789 } 790 791 @Override onHandoverFromWiFiToLte()792 public void onHandoverFromWiFiToLte() { 793 getView().post(videoChargesAlertDialogRunnable); 794 } 795 796 @Override showButton(@nCallButtonIds int buttonId, boolean show)797 public void showButton(@InCallButtonIds int buttonId, boolean show) { 798 LogUtil.v( 799 "VideoCallFragment.showButton", 800 "buttonId: %s, show: %b", 801 InCallButtonIdsExtension.toString(buttonId), 802 show); 803 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 804 speakerButtonController.setEnabled(show); 805 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 806 muteButton.setEnabled(show); 807 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 808 cameraOffButton.setEnabled(show); 809 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 810 switchOnHoldCallController.setVisible(show); 811 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) { 812 swapCameraButton.setEnabled(show); 813 } 814 } 815 816 @Override enableButton(@nCallButtonIds int buttonId, boolean enable)817 public void enableButton(@InCallButtonIds int buttonId, boolean enable) { 818 LogUtil.v( 819 "VideoCallFragment.setEnabled", 820 "buttonId: %s, enable: %b", 821 InCallButtonIdsExtension.toString(buttonId), 822 enable); 823 if (buttonId == InCallButtonIds.BUTTON_AUDIO) { 824 speakerButtonController.setEnabled(enable); 825 } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { 826 muteButton.setEnabled(enable); 827 } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { 828 cameraOffButton.setEnabled(enable); 829 } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { 830 switchOnHoldCallController.setEnabled(enable); 831 } 832 } 833 834 @Override setEnabled(boolean enabled)835 public void setEnabled(boolean enabled) { 836 LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled); 837 speakerButtonController.setEnabled(enabled); 838 muteButton.setEnabled(enabled); 839 cameraOffButton.setEnabled(enabled); 840 switchOnHoldCallController.setEnabled(enabled); 841 } 842 843 @Override setHold(boolean value)844 public void setHold(boolean value) { 845 LogUtil.i("VideoCallFragment.setHold", "value: " + value); 846 } 847 848 @Override setCameraSwitched(boolean isBackFacingCamera)849 public void setCameraSwitched(boolean isBackFacingCamera) { 850 LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera); 851 } 852 853 @Override setVideoPaused(boolean isPaused)854 public void setVideoPaused(boolean isPaused) { 855 LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused); 856 cameraOffButton.setChecked(isPaused); 857 } 858 859 @Override setAudioState(CallAudioState audioState)860 public void setAudioState(CallAudioState audioState) { 861 LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState); 862 speakerButtonController.setAudioState(audioState); 863 muteButton.setChecked(audioState.isMuted()); 864 updateMutePreviewOverlayVisibility(); 865 } 866 867 @Override updateButtonStates()868 public void updateButtonStates() { 869 LogUtil.i("VideoCallFragment.updateButtonState", null); 870 speakerButtonController.updateButtonState(); 871 switchOnHoldCallController.updateButtonState(); 872 } 873 874 @Override updateInCallButtonUiColors(@olorInt int color)875 public void updateInCallButtonUiColors(@ColorInt int color) {} 876 877 @Override getInCallButtonUiFragment()878 public Fragment getInCallButtonUiFragment() { 879 return this; 880 } 881 882 @Override showAudioRouteSelector()883 public void showAudioRouteSelector() { 884 LogUtil.i("VideoCallFragment.showAudioRouteSelector", null); 885 AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) 886 .show(getChildFragmentManager(), null); 887 } 888 889 @Override onAudioRouteSelected(int audioRoute)890 public void onAudioRouteSelected(int audioRoute) { 891 LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute); 892 inCallButtonUiDelegate.setAudioRoute(audioRoute); 893 } 894 895 @Override onAudioRouteSelectorDismiss()896 public void onAudioRouteSelectorDismiss() {} 897 898 @Override setPrimary(@onNull PrimaryInfo primaryInfo)899 public void setPrimary(@NonNull PrimaryInfo primaryInfo) { 900 LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString()); 901 contactGridManager.setPrimary(primaryInfo); 902 } 903 904 @Override setSecondary(@onNull SecondaryInfo secondaryInfo)905 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { 906 LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString()); 907 if (!isAdded()) { 908 savedSecondaryInfo = secondaryInfo; 909 return; 910 } 911 savedSecondaryInfo = null; 912 switchOnHoldCallController.setSecondaryInfo(secondaryInfo); 913 updateButtonStates(); 914 FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); 915 Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner); 916 if (secondaryInfo.shouldShow()) { 917 OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo); 918 onHoldFragment.setPadTopInset(!isInFullscreenMode); 919 transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment); 920 } else { 921 if (oldBanner != null) { 922 transaction.remove(oldBanner); 923 } 924 } 925 transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); 926 transaction.commitAllowingStateLoss(); 927 } 928 929 @Override setCallState(@onNull PrimaryCallState primaryCallState)930 public void setCallState(@NonNull PrimaryCallState primaryCallState) { 931 LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString()); 932 contactGridManager.setCallState(primaryCallState); 933 } 934 935 @Override setEndCallButtonEnabled(boolean enabled, boolean animate)936 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 937 LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled); 938 } 939 940 @Override showManageConferenceCallButton(boolean visible)941 public void showManageConferenceCallButton(boolean visible) { 942 LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible); 943 } 944 945 @Override isManageConferenceVisible()946 public boolean isManageConferenceVisible() { 947 LogUtil.i("VideoCallFragment.isManageConferenceVisible", null); 948 return false; 949 } 950 951 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)952 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 953 contactGridManager.dispatchPopulateAccessibilityEvent(event); 954 } 955 956 @Override showNoteSentToast()957 public void showNoteSentToast() { 958 LogUtil.i("VideoCallFragment.showNoteSentToast", null); 959 } 960 961 @Override updateInCallScreenColors()962 public void updateInCallScreenColors() { 963 LogUtil.i("VideoCallFragment.updateColors", null); 964 } 965 966 @Override onInCallScreenDialpadVisibilityChange(boolean isShowing)967 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { 968 LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null); 969 } 970 971 @Override getAnswerAndDialpadContainerResourceId()972 public int getAnswerAndDialpadContainerResourceId() { 973 return 0; 974 } 975 976 @Override getInCallScreenFragment()977 public Fragment getInCallScreenFragment() { 978 return this; 979 } 980 981 @Override isShowingLocationUi()982 public boolean isShowingLocationUi() { 983 return false; 984 } 985 986 @Override showLocationUi(Fragment locationUi)987 public void showLocationUi(Fragment locationUi) { 988 LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported"); 989 // Do nothing 990 } 991 updatePreviewVideoScaling()992 private void updatePreviewVideoScaling() { 993 if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) { 994 LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet"); 995 return; 996 } 997 VideoSurfaceTexture localVideoSurfaceTexture = 998 videoCallScreenDelegate.getLocalVideoSurfaceTexture(); 999 Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions(); 1000 if (cameraDimensions == null) { 1001 LogUtil.i( 1002 "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set"); 1003 return; 1004 } 1005 if (isLandscape()) { 1006 VideoSurfaceBindings.scaleVideoAndFillView( 1007 previewTextureView, 1008 cameraDimensions.x, 1009 cameraDimensions.y, 1010 videoCallScreenDelegate.getDeviceOrientation()); 1011 } else { 1012 VideoSurfaceBindings.scaleVideoAndFillView( 1013 previewTextureView, 1014 cameraDimensions.y, 1015 cameraDimensions.x, 1016 videoCallScreenDelegate.getDeviceOrientation()); 1017 } 1018 } 1019 updateRemoteVideoScaling()1020 private void updateRemoteVideoScaling() { 1021 VideoSurfaceTexture remoteVideoSurfaceTexture = 1022 videoCallScreenDelegate.getRemoteVideoSurfaceTexture(); 1023 Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions(); 1024 if (videoSize == null) { 1025 LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null"); 1026 return; 1027 } 1028 if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) { 1029 LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet"); 1030 return; 1031 } 1032 1033 // If the video and display aspect ratio's are close then scale video to fill display 1034 float videoAspectRatio = ((float) videoSize.x) / videoSize.y; 1035 float displayAspectRatio = 1036 ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight(); 1037 float delta = Math.abs(videoAspectRatio - displayAspectRatio); 1038 float sum = videoAspectRatio + displayAspectRatio; 1039 if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) { 1040 VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0); 1041 } else { 1042 VideoSurfaceBindings.scaleVideoMaintainingAspectRatio( 1043 remoteTextureView, videoSize.x, videoSize.y); 1044 } 1045 } 1046 isLandscape()1047 private boolean isLandscape() { 1048 // Choose orientation based on display orientation, not window orientation 1049 int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); 1050 return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; 1051 } 1052 enterGreenScreenMode()1053 private void enterGreenScreenMode() { 1054 LogUtil.i("VideoCallFragment.enterGreenScreenMode", null); 1055 RelativeLayout.LayoutParams params = 1056 new RelativeLayout.LayoutParams( 1057 RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); 1058 params.addRule(RelativeLayout.ALIGN_PARENT_START); 1059 params.addRule(RelativeLayout.ALIGN_PARENT_TOP); 1060 previewTextureView.setLayoutParams(params); 1061 previewTextureView.setOutlineProvider(null); 1062 updateOverlayBackground(); 1063 contactGridManager.setIsMiddleRowVisible(true); 1064 updateMutePreviewOverlayVisibility(); 1065 1066 previewOffBlurredImageView.setLayoutParams(params); 1067 previewOffBlurredImageView.setOutlineProvider(null); 1068 previewOffBlurredImageView.setClipToOutline(false); 1069 } 1070 exitGreenScreenMode()1071 private void exitGreenScreenMode() { 1072 LogUtil.i("VideoCallFragment.exitGreenScreenMode", null); 1073 Resources resources = getResources(); 1074 RelativeLayout.LayoutParams params = 1075 new RelativeLayout.LayoutParams( 1076 (int) resources.getDimension(R.dimen.videocall_preview_width), 1077 (int) resources.getDimension(R.dimen.videocall_preview_height)); 1078 params.setMargins( 1079 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom)); 1080 if (isLandscape()) { 1081 params.addRule(RelativeLayout.ALIGN_PARENT_END); 1082 params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end)); 1083 } else { 1084 params.addRule(RelativeLayout.ALIGN_PARENT_START); 1085 params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start)); 1086 } 1087 params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 1088 previewTextureView.setLayoutParams(params); 1089 previewTextureView.setOutlineProvider(circleOutlineProvider); 1090 updateOverlayBackground(); 1091 contactGridManager.setIsMiddleRowVisible(false); 1092 updateMutePreviewOverlayVisibility(); 1093 1094 previewOffBlurredImageView.setLayoutParams(params); 1095 previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider); 1096 previewOffBlurredImageView.setClipToOutline(true); 1097 } 1098 updatePreviewOffView()1099 private void updatePreviewOffView() { 1100 LogUtil.enterBlock("VideoCallFragment.updatePreviewOffView"); 1101 1102 // Always hide the preview off and remote off views in green screen mode. 1103 boolean previewEnabled = isInGreenScreenMode || shouldShowPreview; 1104 previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE); 1105 updateBlurredImageView( 1106 previewTextureView, 1107 previewOffBlurredImageView, 1108 shouldShowPreview, 1109 BLUR_PREVIEW_RADIUS, 1110 BLUR_PREVIEW_SCALE_FACTOR); 1111 } 1112 updateRemoteOffView()1113 private void updateRemoteOffView() { 1114 LogUtil.enterBlock("VideoCallFragment.updateRemoteOffView"); 1115 boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote; 1116 boolean isResumed = remoteEnabled && !isRemotelyHeld; 1117 if (isResumed) { 1118 boolean wasRemoteVideoOff = 1119 TextUtils.equals( 1120 remoteVideoOff.getText(), 1121 remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off)); 1122 // The text needs to be updated and hidden after enough delay in order to be announced by 1123 // talkback. 1124 remoteVideoOff.setText( 1125 wasRemoteVideoOff 1126 ? R.string.videocall_remote_video_on 1127 : R.string.videocall_remotely_resumed); 1128 remoteVideoOff.postDelayed( 1129 new Runnable() { 1130 @Override 1131 public void run() { 1132 remoteVideoOff.setVisibility(View.GONE); 1133 } 1134 }, 1135 VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS); 1136 } else { 1137 remoteVideoOff.setText( 1138 isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off); 1139 remoteVideoOff.setVisibility(View.VISIBLE); 1140 } 1141 updateBlurredImageView( 1142 remoteTextureView, 1143 remoteOffBlurredImageView, 1144 shouldShowRemote, 1145 BLUR_REMOTE_RADIUS, 1146 BLUR_REMOTE_SCALE_FACTOR); 1147 } 1148 1149 @VisibleForTesting updateBlurredImageView( TextureView textureView, ImageView blurredImageView, boolean isVideoEnabled, float blurRadius, float scaleFactor)1150 void updateBlurredImageView( 1151 TextureView textureView, 1152 ImageView blurredImageView, 1153 boolean isVideoEnabled, 1154 float blurRadius, 1155 float scaleFactor) { 1156 Context context = getContext(); 1157 1158 if (isVideoEnabled || context == null) { 1159 blurredImageView.setImageBitmap(null); 1160 blurredImageView.setVisibility(View.GONE); 1161 return; 1162 } 1163 1164 long startTimeMillis = SystemClock.elapsedRealtime(); 1165 int width = Math.round(textureView.getWidth() * scaleFactor); 1166 int height = Math.round(textureView.getHeight() * scaleFactor); 1167 1168 LogUtil.i("VideoCallFragment.updateBlurredImageView", "width: %d, height: %d", width, height); 1169 1170 // This call takes less than 10 milliseconds. 1171 Bitmap bitmap = textureView.getBitmap(width, height); 1172 1173 if (bitmap == null) { 1174 blurredImageView.setImageBitmap(null); 1175 blurredImageView.setVisibility(View.GONE); 1176 return; 1177 } 1178 1179 // TODO(mdooley): When the view is first displayed after a rotation the bitmap is empty 1180 // and thus this blur has no effect. 1181 // This call can take 100 milliseconds. 1182 blur(getContext(), bitmap, blurRadius); 1183 1184 // TODO(mdooley): Figure out why only have to apply the transform in landscape mode 1185 if (width > height) { 1186 bitmap = 1187 Bitmap.createBitmap( 1188 bitmap, 1189 0, 1190 0, 1191 bitmap.getWidth(), 1192 bitmap.getHeight(), 1193 textureView.getTransform(null), 1194 true); 1195 } 1196 1197 blurredImageView.setImageBitmap(bitmap); 1198 blurredImageView.setVisibility(View.VISIBLE); 1199 1200 LogUtil.i( 1201 "VideoCallFragment.updateBlurredImageView", 1202 "took %d millis", 1203 (SystemClock.elapsedRealtime() - startTimeMillis)); 1204 } 1205 updateOverlayBackground()1206 private void updateOverlayBackground() { 1207 if (isInGreenScreenMode) { 1208 // We want to darken the preview view to make text and buttons readable. The fullscreen 1209 // background is below the preview view so use the green screen background instead. 1210 animateSetVisibility(greenScreenBackgroundView, View.VISIBLE); 1211 animateSetVisibility(fullscreenBackgroundView, View.GONE); 1212 } else if (!isInFullscreenMode) { 1213 // We want to darken the remote view to make text and buttons readable. The green screen 1214 // background is above the preview view so it would darken the preview too. Use the fullscreen 1215 // background instead. 1216 animateSetVisibility(greenScreenBackgroundView, View.GONE); 1217 animateSetVisibility(fullscreenBackgroundView, View.VISIBLE); 1218 } else { 1219 animateSetVisibility(greenScreenBackgroundView, View.GONE); 1220 animateSetVisibility(fullscreenBackgroundView, View.GONE); 1221 } 1222 } 1223 updateMutePreviewOverlayVisibility()1224 private void updateMutePreviewOverlayVisibility() { 1225 // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen 1226 // mode the preview is fullscreen so there's no where to anchor it. 1227 mutePreviewOverlay.setVisibility( 1228 muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE); 1229 } 1230 animateSetVisibility(final View view, final int visibility)1231 private static void animateSetVisibility(final View view, final int visibility) { 1232 if (view.getVisibility() == visibility) { 1233 return; 1234 } 1235 1236 int startAlpha; 1237 int endAlpha; 1238 if (visibility == View.GONE) { 1239 startAlpha = 1; 1240 endAlpha = 0; 1241 } else if (visibility == View.VISIBLE) { 1242 startAlpha = 0; 1243 endAlpha = 1; 1244 } else { 1245 Assert.fail(); 1246 return; 1247 } 1248 1249 view.setAlpha(startAlpha); 1250 view.setVisibility(View.VISIBLE); 1251 view.animate() 1252 .alpha(endAlpha) 1253 .withEndAction( 1254 new Runnable() { 1255 @Override 1256 public void run() { 1257 view.setVisibility(visibility); 1258 } 1259 }) 1260 .start(); 1261 } 1262 blur(Context context, Bitmap image, float blurRadius)1263 private static void blur(Context context, Bitmap image, float blurRadius) { 1264 RenderScript renderScript = RenderScript.create(context); 1265 ScriptIntrinsicBlur blurScript = 1266 ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)); 1267 Allocation allocationIn = Allocation.createFromBitmap(renderScript, image); 1268 Allocation allocationOut = Allocation.createFromBitmap(renderScript, image); 1269 blurScript.setRadius(blurRadius); 1270 blurScript.setInput(allocationIn); 1271 blurScript.forEach(allocationOut); 1272 allocationOut.copyTo(image); 1273 blurScript.destroy(); 1274 allocationIn.destroy(); 1275 allocationOut.destroy(); 1276 } 1277 1278 @Override onSystemUiVisibilityChange(int visibility)1279 public void onSystemUiVisibilityChange(int visibility) { 1280 boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; 1281 videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible); 1282 } 1283 checkCameraPermission()1284 private void checkCameraPermission() { 1285 // Checks if user has consent of camera permission and the permission is granted. 1286 // If camera permission is revoked, shows system permission dialog. 1287 // If camera permission is granted but user doesn't have consent of camera permission 1288 // (which means it's first time making video call), shows custom dialog instead. This 1289 // will only be shown to user once. 1290 if (!VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) { 1291 videoCallScreenDelegate.onCameraPermissionDialogShown(); 1292 if (!VideoUtils.hasCameraPermission(getContext())) { 1293 requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE); 1294 } else { 1295 PermissionsUtil.showCameraPermissionToast(getContext()); 1296 videoCallScreenDelegate.onCameraPermissionGranted(); 1297 } 1298 } 1299 } 1300 } 1301 1302