That fuck shit the fascists are using
at master 1207 lines 50 kB view raw
1/* 2 * Copyright (C) 2016 Open Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18package org.tm.archive; 19 20import android.Manifest; 21import android.annotation.SuppressLint; 22import android.app.PictureInPictureParams; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.ActivityInfo; 26import android.content.pm.PackageManager; 27import android.graphics.Rect; 28import android.media.AudioManager; 29import android.os.Build; 30import android.os.Bundle; 31import android.util.Rational; 32import android.view.Window; 33import android.view.WindowManager; 34 35import androidx.annotation.NonNull; 36import androidx.annotation.RequiresApi; 37import androidx.appcompat.app.AppCompatDelegate; 38import androidx.core.content.ContextCompat; 39import androidx.core.util.Consumer; 40import androidx.lifecycle.LiveDataReactiveStreams; 41import androidx.lifecycle.ViewModelProvider; 42import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter; 43import androidx.window.layout.DisplayFeature; 44import androidx.window.layout.FoldingFeature; 45import androidx.window.layout.WindowInfoTracker; 46import androidx.window.layout.WindowLayoutInfo; 47 48import com.google.android.material.dialog.MaterialAlertDialogBuilder; 49 50import org.greenrobot.eventbus.EventBus; 51import org.greenrobot.eventbus.Subscribe; 52import org.greenrobot.eventbus.ThreadMode; 53import org.signal.core.util.ThreadUtil; 54import org.signal.core.util.concurrent.LifecycleDisposable; 55import org.signal.core.util.concurrent.SignalExecutors; 56import org.signal.core.util.logging.Log; 57import org.signal.libsignal.protocol.IdentityKey; 58import org.tm.archive.components.TooltipPopup; 59import org.tm.archive.components.sensors.DeviceOrientationMonitor; 60import org.tm.archive.components.webrtc.CallLinkProfileKeySender; 61import org.tm.archive.components.webrtc.CallOverflowPopupWindow; 62import org.tm.archive.components.webrtc.CallParticipantsListUpdatePopupWindow; 63import org.tm.archive.components.webrtc.CallParticipantsState; 64import org.tm.archive.components.webrtc.CallStateUpdatePopupWindow; 65import org.tm.archive.components.webrtc.CallToastPopupWindow; 66import org.tm.archive.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil; 67import org.tm.archive.components.webrtc.InCallStatus; 68import org.tm.archive.components.webrtc.PendingParticipantsBottomSheet; 69import org.tm.archive.components.webrtc.PendingParticipantsView; 70import org.tm.archive.components.webrtc.WebRtcAudioDevice; 71import org.tm.archive.components.webrtc.WebRtcAudioOutput; 72import org.tm.archive.components.webrtc.WebRtcCallView; 73import org.tm.archive.components.webrtc.WebRtcCallViewModel; 74import org.tm.archive.components.webrtc.WebRtcControls; 75import org.tm.archive.components.webrtc.WifiToCellularPopupWindow; 76import org.tm.archive.components.webrtc.controls.ControlsAndInfoController; 77import org.tm.archive.components.webrtc.controls.ControlsAndInfoViewModel; 78import org.tm.archive.components.webrtc.participantslist.CallParticipantsListDialog; 79import org.tm.archive.components.webrtc.requests.CallLinkIncomingRequestSheet; 80import org.tm.archive.conversation.ui.error.SafetyNumberChangeDialog; 81import org.tm.archive.dependencies.ApplicationDependencies; 82import org.tm.archive.events.WebRtcViewModel; 83import org.tm.archive.messagerequests.CalleeMustAcceptMessageRequestActivity; 84import org.tm.archive.permissions.Permissions; 85import org.tm.archive.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; 86import org.tm.archive.recipients.Recipient; 87import org.tm.archive.recipients.RecipientId; 88import org.tm.archive.safety.SafetyNumberBottomSheet; 89import org.tm.archive.service.webrtc.CallLinkDisconnectReason; 90import org.tm.archive.service.webrtc.SignalCallManager; 91import org.tm.archive.sms.MessageSender; 92import org.tm.archive.util.BottomSheetUtil; 93import org.tm.archive.util.EllapsedTimeFormatter; 94import org.tm.archive.util.FeatureFlags; 95import org.tm.archive.util.FullscreenHelper; 96import org.tm.archive.util.TextSecurePreferences; 97import org.tm.archive.util.ThrottledDebouncer; 98import org.tm.archive.util.Util; 99import org.tm.archive.util.VibrateUtil; 100import org.tm.archive.util.WindowUtil; 101import org.tm.archive.util.livedata.LiveDataUtil; 102import org.tm.archive.webrtc.CallParticipantsViewState; 103import org.tm.archive.webrtc.audio.SignalAudioManager; 104import org.whispersystems.signalservice.api.messages.calls.HangupMessage; 105 106import java.util.HashSet; 107import java.util.List; 108import java.util.Optional; 109import java.util.concurrent.TimeUnit; 110import java.util.stream.Collectors; 111 112import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; 113import io.reactivex.rxjava3.core.BackpressureStrategy; 114import io.reactivex.rxjava3.disposables.Disposable; 115 116import static org.tm.archive.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; 117 118public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback { 119 120 private static final String TAG = Log.tag(WebRtcCallActivity.class); 121 122 private static final int STANDARD_DELAY_FINISH = 1000; 123 private static final int VIBRATE_DURATION = 50; 124 125 /** 126 * ANSWER the call via voice-only. 127 */ 128 public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION"; 129 130 /** 131 * ANSWER the call via video. 132 */ 133 public static final String ANSWER_VIDEO_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_VIDEO_ACTION"; 134 public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION"; 135 public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION"; 136 137 public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE"; 138 public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN"; 139 public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK"; 140 public static final String EXTRA_LAUNCH_IN_PIP = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK"; 141 142 private CallParticipantsListUpdatePopupWindow participantUpdateWindow; 143 private CallStateUpdatePopupWindow callStateUpdatePopupWindow; 144 private CallOverflowPopupWindow callOverflowPopupWindow; 145 private WifiToCellularPopupWindow wifiToCellularPopupWindow; 146 private DeviceOrientationMonitor deviceOrientationMonitor; 147 148 private FullscreenHelper fullscreenHelper; 149 private WebRtcCallView callScreen; 150 private TooltipPopup videoTooltip; 151 private TooltipPopup switchCameraTooltip; 152 private WebRtcCallViewModel viewModel; 153 private ControlsAndInfoViewModel controlsAndInfoViewModel; 154 private boolean enableVideoIfAvailable; 155 private boolean hasWarnedAboutBluetooth; 156 private WindowLayoutInfoConsumer windowLayoutInfoConsumer; 157 private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter; 158 private ThrottledDebouncer requestNewSizesThrottle; 159 private PictureInPictureParams.Builder pipBuilderParams; 160 private LifecycleDisposable lifecycleDisposable; 161 private long lastCallLinkDisconnectDialogShowTime; 162 private ControlsAndInfoController controlsAndInfo; 163 private boolean enterPipOnResume; 164 private long lastProcessedIntentTimestamp; 165 166 private Disposable ephemeralStateDisposable = Disposable.empty(); 167 168 @Override 169 protected void attachBaseContext(@NonNull Context newBase) { 170 getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); 171 super.attachBaseContext(newBase); 172 } 173 174 @SuppressLint({ "SourceLockedOrientationActivity", "MissingInflatedId" }) 175 @Override 176 public void onCreate(Bundle savedInstanceState) { 177 Log.i(TAG, "onCreate(" + getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")"); 178 179 lifecycleDisposable = new LifecycleDisposable(); 180 lifecycleDisposable.bindTo(this); 181 182 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); 183 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 184 super.onCreate(savedInstanceState); 185 186 boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480; 187 if (!isLandscapeEnabled) { 188 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 189 } 190 191 requestWindowFeature(Window.FEATURE_NO_TITLE); 192 setContentView(R.layout.webrtc_call_activity); 193 194 fullscreenHelper = new FullscreenHelper(this); 195 196 setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); 197 198 initializeResources(); 199 initializeViewModel(isLandscapeEnabled); 200 initializePictureInPictureParams(); 201 202 controlsAndInfo = new ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel); 203 controlsAndInfo.addVisibilityListener(new FadeCallback()); 204 205 fullscreenHelper.showAndHideWithSystemUI(getWindow(), 206 findViewById(R.id.call_screen_header_gradient), 207 findViewById(R.id.webrtc_call_view_toolbar_text), 208 findViewById(R.id.webrtc_call_view_toolbar_no_text)); 209 210 lifecycleDisposable.add(controlsAndInfo); 211 212 logIntent(getIntent()); 213 214 if (ANSWER_VIDEO_ACTION.equals(getIntent().getAction())) { 215 enableVideoIfAvailable = true; 216 } else if (ANSWER_ACTION.equals(getIntent().getAction()) || getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false)) { 217 enableVideoIfAvailable = false; 218 } else { 219 enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false); 220 getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE); 221 } 222 223 processIntent(getIntent()); 224 225 windowLayoutInfoConsumer = new WindowLayoutInfoConsumer(); 226 227 windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this)); 228 windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer); 229 230 requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1)); 231 232 initializePendingParticipantFragmentListener(); 233 234 WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface)); 235 } 236 237 @Override 238 protected void onStart() { 239 super.onStart(); 240 241 ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager() 242 .ephemeralStates() 243 .observeOn(AndroidSchedulers.mainThread()) 244 .subscribe(viewModel::updateFromEphemeralState); 245 } 246 247 @Override 248 public void onResume() { 249 Log.i(TAG, "onResume()"); 250 super.onResume(); 251 initializeScreenshotSecurity(); 252 253 if (!EventBus.getDefault().isRegistered(this)) { 254 EventBus.getDefault().register(this); 255 } 256 257 WebRtcViewModel rtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class); 258 if (rtcViewModel == null) { 259 Log.w(TAG, "Activity resumed without service event, perform delay destroy"); 260 ThreadUtil.runOnMainDelayed(() -> { 261 WebRtcViewModel delayRtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class); 262 if (delayRtcViewModel == null) { 263 Log.w(TAG, "Activity still without service event, finishing activity"); 264 finish(); 265 } else { 266 Log.i(TAG, "Event found after delay"); 267 } 268 }, TimeUnit.SECONDS.toMillis(1)); 269 } 270 271 if (enterPipOnResume) { 272 enterPipOnResume = false; 273 enterPipModeIfPossible(); 274 } 275 } 276 277 @Override 278 public void onNewIntent(Intent intent) { 279 Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")"); 280 super.onNewIntent(intent); 281 logIntent(intent); 282 processIntent(intent); 283 } 284 285 @Override 286 public void onPause() { 287 Log.i(TAG, "onPause"); 288 super.onPause(); 289 290 if (!viewModel.isCallStarting()) { 291 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 292 if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { 293 finish(); 294 } 295 } 296 } 297 298 @Override 299 protected void onStop() { 300 Log.i(TAG, "onStop"); 301 super.onStop(); 302 303 ephemeralStateDisposable.dispose(); 304 305 if (!isInPipMode() || isFinishing()) { 306 EventBus.getDefault().unregister(this); 307 requestNewSizesThrottle.clear(); 308 } 309 310 ApplicationDependencies.getSignalCallManager().setEnableVideo(false); 311 312 if (!viewModel.isCallStarting()) { 313 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 314 if (state != null) { 315 if (state.getCallState().isPreJoinOrNetworkUnavailable()) { 316 ApplicationDependencies.getSignalCallManager().cancelPreJoin(); 317 } else if (state.getCallState().getInOngoingCall() && isInPipMode()) { 318 ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground(); 319 } 320 } 321 } 322 } 323 324 @Override 325 protected void onDestroy() { 326 super.onDestroy(); 327 windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer); 328 EventBus.getDefault().unregister(this); 329 } 330 331 @SuppressLint("MissingSuperCall") 332 @Override 333 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 334 Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); 335 } 336 337 @Override 338 protected void onUserLeaveHint() { 339 enterPipModeIfPossible(); 340 } 341 342 @Override 343 public void onBackPressed() { 344 if (!enterPipModeIfPossible()) { 345 super.onBackPressed(); 346 } 347 } 348 349 private boolean enterPipModeIfPossible() { 350 if (isSystemPipEnabledAndAvailable()) { 351 if (viewModel.canEnterPipMode()) { 352 try { 353 enterPictureInPictureMode(pipBuilderParams.build()); 354 } catch (Exception e) { 355 Log.w(TAG, "Device lied to us about supporting PiP.", e); 356 return false; 357 } 358 359 CallParticipantsListDialog.dismiss(getSupportFragmentManager()); 360 361 return true; 362 } 363 if (Build.VERSION.SDK_INT >= 31) { 364 pipBuilderParams.setAutoEnterEnabled(false); 365 } 366 } 367 return false; 368 } 369 370 private boolean isInPipMode() { 371 return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode(); 372 } 373 374 private void logIntent(@NonNull Intent intent) { 375 Log.d(TAG, "Intent: Action: " + intent.getAction()); 376 Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false)); 377 Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false)); 378 Log.d(TAG, "Intent: EXTRA_LAUNCH_IN_PIP: " + intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false)); 379 } 380 381 private void processIntent(@NonNull Intent intent) { 382 if (ANSWER_ACTION.equals(intent.getAction())) { 383 handleAnswerWithAudio(); 384 } else if (ANSWER_VIDEO_ACTION.equals(intent.getAction())) { 385 handleAnswerWithVideo(); 386 } else if (DENY_ACTION.equals(intent.getAction())) { 387 handleDenyCall(); 388 } else if (END_CALL_ACTION.equals(intent.getAction())) { 389 handleEndCall(); 390 } 391 392 if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) { 393 enterPipOnResume = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false); 394 } 395 396 lastProcessedIntentTimestamp = System.currentTimeMillis(); 397 } 398 399 private void initializePendingParticipantFragmentListener() { 400 if (!FeatureFlags.adHocCalling()) { 401 return; 402 } 403 404 getSupportFragmentManager().setFragmentResultListener( 405 PendingParticipantsBottomSheet.REQUEST_KEY, 406 this, 407 (requestKey, result) -> { 408 PendingParticipantsBottomSheet.Action action = PendingParticipantsBottomSheet.getAction(result); 409 List<RecipientId> recipientIds = viewModel.getPendingParticipantsSnapshot() 410 .getUnresolvedPendingParticipants() 411 .stream() 412 .map(r -> r.getRecipient().getId()) 413 .collect(Collectors.toList()); 414 415 switch (action) { 416 case NONE: 417 break; 418 case APPROVE_ALL: 419 new MaterialAlertDialogBuilder(this) 420 .setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__approve_d_requests, recipientIds.size(), recipientIds.size())) 421 .setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size())) 422 .setNegativeButton(android.R.string.cancel, null) 423 .setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> { 424 for (RecipientId id : recipientIds) { 425 ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id); 426 } 427 }) 428 .show(); 429 break; 430 case DENY_ALL: 431 new MaterialAlertDialogBuilder(this) 432 .setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__deny_d_requests, recipientIds.size(), recipientIds.size())) 433 .setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size())) 434 .setNegativeButton(android.R.string.cancel, null) 435 .setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> { 436 for (RecipientId id : recipientIds) { 437 ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id); 438 } 439 }) 440 .show(); 441 break; 442 } 443 } 444 ); 445 } 446 447 private void initializeScreenshotSecurity() { 448 if (TextSecurePreferences.isScreenSecurityEnabled(this)) { 449 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); 450 } else { 451 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); 452 } 453 } 454 455 private void initializeResources() { 456 callScreen = findViewById(R.id.callScreen); 457 callScreen.setControlsListener(new ControlsListener()); 458 459 participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen); 460 callStateUpdatePopupWindow = new CallStateUpdatePopupWindow(callScreen); 461 wifiToCellularPopupWindow = new WifiToCellularPopupWindow(callScreen); 462 callOverflowPopupWindow = new CallOverflowPopupWindow(this, callScreen, () -> { 463 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 464 if (state == null) { 465 return false; 466 } 467 return state.getLocalParticipant().isHandRaised(); 468 }); 469 } 470 471 private void initializeViewModel(boolean isLandscapeEnabled) { 472 deviceOrientationMonitor = new DeviceOrientationMonitor(this); 473 getLifecycle().addObserver(deviceOrientationMonitor); 474 475 WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor); 476 477 viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class); 478 viewModel.setIsLandscapeEnabled(isLandscapeEnabled); 479 viewModel.setIsInPipMode(isInPipMode()); 480 viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); 481 viewModel.getWebRtcControls().observe(this, controls -> { 482 callScreen.setWebRtcControls(controls); 483 controlsAndInfo.updateControls(controls); 484 }); 485 viewModel.getEvents().observe(this, this::handleViewModelEvent); 486 487 lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus)); 488 489 boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false); 490 LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)), 491 viewModel.getOrientationAndLandscapeEnabled(), 492 viewModel.getEphemeralState(), 493 (s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink)) 494 .observe(this, p -> callScreen.updateCallParticipants(p)); 495 viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); 496 viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); 497 viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall()); 498 viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange); 499 lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint)); 500 501 callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> { 502 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 503 if (state != null) { 504 if (state.needsNewRequestSizes()) { 505 requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions()); 506 } 507 } 508 }); 509 510 viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees())); 511 viewModel.getControlsRotation().observe(this, callScreen::rotateControls); 512 513 addOnPictureInPictureModeChangedListener(info -> { 514 viewModel.setIsInPipMode(info.isInPictureInPictureMode()); 515 participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode()); 516 callStateUpdatePopupWindow.setEnabled(!info.isInPictureInPictureMode()); 517 if (info.isInPictureInPictureMode()) { 518 callScreen.maybeDismissAudioPicker(); 519 } 520 viewModel.setIsLandscapeEnabled(info.isInPictureInPictureMode()); 521 }); 522 523 callScreen.setPendingParticipantsViewListener(new PendingParticipantsViewListener()); 524 Disposable disposable = viewModel.getPendingParticipants() 525 .subscribe(callScreen::updatePendingParticipantsList); 526 527 lifecycleDisposable.add(disposable); 528 529 controlsAndInfoViewModel = new ViewModelProvider(this).get(ControlsAndInfoViewModel.class); 530 } 531 532 private void initializePictureInPictureParams() { 533 if (isSystemPipEnabledAndAvailable()) { 534 pipBuilderParams = new PictureInPictureParams.Builder(); 535 pipBuilderParams.setAspectRatio(new Rational(9, 16)); 536 if (Build.VERSION.SDK_INT >= 31) { 537 pipBuilderParams.setAutoEnterEnabled(true); 538 } 539 if (Build.VERSION.SDK_INT >= 26) { 540 try { 541 setPictureInPictureParams(pipBuilderParams.build()); 542 } catch (Exception e) { 543 Log.w(TAG, "System lied about having PiP available.", e); 544 } 545 } 546 } 547 } 548 549 private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { 550 if (event instanceof WebRtcCallViewModel.Event.StartCall) { 551 startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall()); 552 return; 553 } else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) { 554 SafetyNumberBottomSheet.forGroupCall(((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords()) 555 .show(getSupportFragmentManager()); 556 return; 557 } else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) { 558 callScreen.switchToSpeakerView(); 559 return; 560 } else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) { 561 CallToastPopupWindow.show(callScreen); 562 return; 563 } 564 565 if (isInPipMode()) { 566 return; 567 } 568 569 if (event instanceof WebRtcCallViewModel.Event.ShowVideoTooltip) { 570 if (videoTooltip == null) { 571 videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget()) 572 .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine)) 573 .setTextColor(ContextCompat.getColor(this, R.color.core_white)) 574 .setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video) 575 .setOnDismissListener(() -> viewModel.onDismissedVideoTooltip()) 576 .show(TooltipPopup.POSITION_ABOVE); 577 } 578 } else if (event instanceof WebRtcCallViewModel.Event.DismissVideoTooltip) { 579 if (videoTooltip != null) { 580 videoTooltip.dismiss(); 581 videoTooltip = null; 582 } 583 } else if (event instanceof WebRtcCallViewModel.Event.ShowWifiToCellularPopup) { 584 wifiToCellularPopupWindow.show(); 585 } else if (event instanceof WebRtcCallViewModel.Event.ShowSwitchCameraTooltip) { 586 if (switchCameraTooltip == null) { 587 switchCameraTooltip = TooltipPopup.forTarget(callScreen.getSwitchCameraTooltipTarget()) 588 .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine)) 589 .setTextColor(ContextCompat.getColor(this, R.color.core_white)) 590 .setText(R.string.WebRtcCallActivity__flip_camera_tooltip) 591 .setOnDismissListener(() -> viewModel.onDismissedSwitchCameraTooltip()) 592 .show(TooltipPopup.POSITION_ABOVE); 593 } 594 } else if (event instanceof WebRtcCallViewModel.Event.DismissSwitchCameraTooltip) { 595 if (switchCameraTooltip != null) { 596 switchCameraTooltip.dismiss(); 597 switchCameraTooltip = null; 598 } 599 } else { 600 throw new IllegalArgumentException("Unknown event: " + event); 601 } 602 } 603 604 private void handleInCallStatus(@NonNull InCallStatus inCallStatus) { 605 if (inCallStatus instanceof InCallStatus.ElapsedTime) { 606 607 EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(((InCallStatus.ElapsedTime) inCallStatus).getElapsedTime()); 608 609 if (ellapsedTimeFormatter == null) { 610 return; 611 } 612 613 callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString())); 614 } else if (inCallStatus instanceof InCallStatus.PendingCallLinkUsers) { 615 int waiting = ((InCallStatus.PendingCallLinkUsers) inCallStatus).getPendingUserCount(); 616 617 callScreen.setStatus(getResources().getQuantityString( 618 R.plurals.WebRtcCallActivity__d_people_waiting, 619 waiting, 620 waiting 621 )); 622 } else if (inCallStatus instanceof InCallStatus.JoinedCallLinkUsers) { 623 int joined = ((InCallStatus.JoinedCallLinkUsers) inCallStatus).getJoinedUserCount(); 624 625 callScreen.setStatus(getResources().getQuantityString( 626 R.plurals.WebRtcCallActivity__d_people, 627 joined, 628 joined 629 )); 630 }else { 631 throw new AssertionError(); 632 } 633 } 634 635 private void handleSetAudioHandset() { 636 ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE)); 637 } 638 639 private void handleSetAudioSpeaker() { 640 ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE)); 641 } 642 643 private void handleSetAudioBluetooth() { 644 ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH)); 645 } 646 647 private void handleSetAudioWiredHeadset() { 648 ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET)); 649 } 650 651 private void handleSetMuteAudio(boolean enabled) { 652 ApplicationDependencies.getSignalCallManager().setMuteAudio(enabled); 653 } 654 655 private void handleSetMuteVideo(boolean muted) { 656 Recipient recipient = viewModel.getRecipient().get(); 657 658 if (!recipient.equals(Recipient.UNKNOWN)) { 659 String recipientDisplayName = recipient.getDisplayName(this); 660 661 Permissions.with(this) 662 .request(Manifest.permission.CAMERA) 663 .ifNecessary() 664 .withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted) 665 .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName)) 666 .onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted)) 667 .execute(); 668 } 669 } 670 671 private void handleFlipCamera() { 672 ApplicationDependencies.getSignalCallManager().flipCamera(); 673 } 674 675 private void handleAnswerWithAudio() { 676 Permissions.with(this) 677 .request(Manifest.permission.RECORD_AUDIO) 678 .ifNecessary() 679 .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone), 680 R.drawable.ic_mic_solid_24) 681 .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) 682 .onAllGranted(() -> { 683 callScreen.setStatus(getString(R.string.RedPhone_answering)); 684 685 ApplicationDependencies.getSignalCallManager().acceptCall(false); 686 }) 687 .onAnyDenied(this::handleDenyCall) 688 .execute(); 689 } 690 691 private void handleAnswerWithVideo() { 692 Permissions.with(this) 693 .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) 694 .ifNecessary() 695 .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted) 696 .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) 697 .onAllGranted(() -> { 698 callScreen.setStatus(getString(R.string.RedPhone_answering)); 699 700 ApplicationDependencies.getSignalCallManager().acceptCall(true); 701 702 handleSetMuteVideo(false); 703 }) 704 .onAnyDenied(this::handleDenyCall) 705 .execute(); 706 } 707 708 private void handleDenyCall() { 709 Recipient recipient = viewModel.getRecipient().get(); 710 711 if (!recipient.equals(Recipient.UNKNOWN)) { 712 ApplicationDependencies.getSignalCallManager().denyCall(); 713 714 callScreen.setRecipient(recipient); 715 callScreen.setStatus(getString(R.string.RedPhone_ending_call)); 716 delayedFinish(); 717 } 718 } 719 720 private void handleEndCall() { 721 Log.i(TAG, "Hangup pressed, handling termination now..."); 722 ApplicationDependencies.getSignalCallManager().localHangup(); 723 } 724 725 private void handleOutgoingCall(@NonNull WebRtcViewModel event) { 726 if (event.getGroupState().isNotIdle()) { 727 callScreen.setStatusFromGroupCallState(event.getGroupState()); 728 } else { 729 callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling)); 730 } 731 } 732 733 private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) { 734 Log.i(TAG, "handleTerminate called: " + hangupType.name()); 735 736 callScreen.setStatusFromHangupType(hangupType); 737 738 EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); 739 740 if (hangupType == HangupMessage.Type.NEED_PERMISSION) { 741 startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.getId())); 742 } 743 delayedFinish(); 744 } 745 746 private void handleGlare(@NonNull Recipient recipient) { 747 Log.i(TAG, "handleGlare: " + recipient.getId()); 748 749 callScreen.setStatus(""); 750 } 751 752 private void handleCallRinging() { 753 callScreen.setStatus(getString(R.string.RedPhone_ringing)); 754 } 755 756 private void handleCallBusy() { 757 EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); 758 callScreen.setStatus(getString(R.string.RedPhone_busy)); 759 delayedFinish(SignalCallManager.BUSY_TONE_LENGTH); 760 } 761 762 private void handleCallConnected(@NonNull WebRtcViewModel event) { 763 getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); 764 if (event.getGroupState().isNotIdleOrConnected()) { 765 callScreen.setStatusFromGroupCallState(event.getGroupState()); 766 } 767 } 768 769 private void handleCallReconnecting() { 770 callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting)); 771 VibrateUtil.vibrate(this, VIBRATE_DURATION); 772 } 773 774 private void handleRecipientUnavailable() { 775 EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); 776 callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable)); 777 delayedFinish(); 778 } 779 780 private void handleServerFailure() { 781 EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); 782 callScreen.setStatus(getString(R.string.RedPhone_network_failed)); 783 } 784 785 private void handleNoSuchUser(final @NonNull WebRtcViewModel event) { 786 if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie 787 new MaterialAlertDialogBuilder(this) 788 .setTitle(R.string.RedPhone_number_not_registered) 789 .setIcon(R.drawable.symbol_error_triangle_fill_24) 790 .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice) 791 .setCancelable(true) 792 .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL)) 793 .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL)) 794 .show(); 795 } 796 797 private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { 798 final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey(); 799 final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient(); 800 801 if (theirKey == null) { 802 Log.w(TAG, "Untrusted identity without an identity key."); 803 } 804 805 SafetyNumberBottomSheet.forCall(recipient.getId()).show(getSupportFragmentManager()); 806 } 807 808 public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) { 809 if (Util.hasItems(safetyNumberChangeEvent.getRecipientIds())) { 810 if (safetyNumberChangeEvent.isInPipMode()) { 811 GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get()); 812 } else { 813 GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get()); 814 SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.getRecipientIds()).show(getSupportFragmentManager()); 815 } 816 } 817 } 818 819 private void updateGroupMembersForGroupCall() { 820 ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers(); 821 } 822 823 public void handleGroupMemberCountChange(int count) { 824 boolean canRing = count <= FeatureFlags.maxGroupCallRingSize(); 825 callScreen.enableRingGroup(canRing); 826 ApplicationDependencies.getSignalCallManager().setRingGroup(canRing); 827 } 828 829 private void updateSpeakerHint(boolean showSpeakerHint) { 830 if (showSpeakerHint) { 831 callScreen.showSpeakerViewHint(); 832 } else { 833 callScreen.hideSpeakerViewHint(); 834 } 835 } 836 837 @Override 838 public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) { 839 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 840 841 if (state == null) { 842 return; 843 } 844 845 if (state.isCallLink()) { 846 CallLinkProfileKeySender.onSendAnyway(new HashSet<>(changedRecipients)); 847 } 848 849 if (state.getGroupCallState().isConnected()) { 850 ApplicationDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients); 851 } else { 852 viewModel.startCall(state.getLocalParticipant().isVideoEnabled()); 853 } 854 } 855 856 @Override 857 public void onMessageResentAfterSafetyNumberChange() {} 858 859 @Override 860 public void onCanceled() { 861 CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); 862 if (state != null && state.getGroupCallState().isNotIdle()) { 863 if (state.getCallState().isPreJoinOrNetworkUnavailable()) { 864 ApplicationDependencies.getSignalCallManager().cancelPreJoin(); 865 finish(); 866 } else { 867 handleEndCall(); 868 } 869 } else { 870 handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL); 871 } 872 } 873 874 private boolean isSystemPipEnabledAndAvailable() { 875 return Build.VERSION.SDK_INT >= 26 && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); 876 } 877 878 private void delayedFinish() { 879 delayedFinish(STANDARD_DELAY_FINISH); 880 } 881 882 private void delayedFinish(int delayMillis) { 883 callScreen.postDelayed(WebRtcCallActivity.this::finish, delayMillis); 884 } 885 886 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) 887 public void onEventMainThread(@NonNull WebRtcViewModel event) { 888 Log.i(TAG, "Got message from service: " + event); 889 890 viewModel.setRecipient(event.getRecipient()); 891 callScreen.setRecipient(event.getRecipient()); 892 controlsAndInfoViewModel.setRecipient(event.getRecipient()); 893 894 switch (event.getState()) { 895 case CALL_PRE_JOIN: 896 handleCallPreJoin(event); break; 897 case CALL_CONNECTED: 898 handleCallConnected(event); break; 899 case CALL_RECONNECTING: 900 handleCallReconnecting(); break; 901 case NETWORK_FAILURE: 902 handleServerFailure(); break; 903 case CALL_RINGING: 904 handleCallRinging(); break; 905 case CALL_DISCONNECTED: 906 handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break; 907 case CALL_DISCONNECTED_GLARE: 908 handleGlare(event.getRecipient()); break; 909 case CALL_ACCEPTED_ELSEWHERE: 910 handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break; 911 case CALL_DECLINED_ELSEWHERE: 912 handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break; 913 case CALL_ONGOING_ELSEWHERE: 914 handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break; 915 case CALL_NEEDS_PERMISSION: 916 handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break; 917 case NO_SUCH_USER: 918 handleNoSuchUser(event); break; 919 case RECIPIENT_UNAVAILABLE: 920 handleRecipientUnavailable(); break; 921 case CALL_OUTGOING: 922 handleOutgoingCall(event); break; 923 case CALL_BUSY: 924 handleCallBusy(); break; 925 case UNTRUSTED_IDENTITY: 926 handleUntrustedIdentity(event); break; 927 } 928 929 if (event.getCallLinkDisconnectReason() != null && event.getCallLinkDisconnectReason().getPostedAt() > lastCallLinkDisconnectDialogShowTime) { 930 lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis(); 931 932 if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.RemovedFromCall) { 933 displayRemovedFromCallLinkDialog(); 934 } else if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.DeniedRequestToJoinCall) { 935 displayDeniedRequestToJoinCallLinkDialog(); 936 } else { 937 throw new AssertionError("Unexpected reason: " + event.getCallLinkDisconnectReason()); 938 } 939 } 940 941 boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable; 942 943 viewModel.updateFromWebRtcViewModel(event, enableVideo); 944 945 if (enableVideo) { 946 enableVideoIfAvailable = false; 947 handleSetMuteVideo(false); 948 } 949 950 if (event.getBluetoothPermissionDenied() && !hasWarnedAboutBluetooth && !isFinishing()) { 951 new MaterialAlertDialogBuilder(this) 952 .setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied) 953 .setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call) 954 .setPositiveButton(R.string.WebRtcCallActivity__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(this))) 955 .setNegativeButton(R.string.WebRtcCallActivity__not_now, null) 956 .show(); 957 958 hasWarnedAboutBluetooth = true; 959 } 960 } 961 962 private void displayRemovedFromCallLinkDialog() { 963 new MaterialAlertDialogBuilder(this) 964 .setTitle(R.string.WebRtcCallActivity__removed_from_call) 965 .setMessage(R.string.WebRtcCallActivity__someone_has_removed_you_from_the_call) 966 .setPositiveButton(android.R.string.ok, null) 967 .show(); 968 } 969 970 private void displayDeniedRequestToJoinCallLinkDialog() { 971 new MaterialAlertDialogBuilder(this) 972 .setTitle(R.string.WebRtcCallActivity__join_request_denied) 973 .setMessage(R.string.WebRtcCallActivity__your_request_to_join_this_call_has_been_denied) 974 .setPositiveButton(android.R.string.ok, null) 975 .show(); 976 } 977 978 private void handleCallPreJoin(@NonNull WebRtcViewModel event) { 979 if (event.getGroupState().isNotIdle()) { 980 callScreen.setRingGroup(event.shouldRingGroup()); 981 982 if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) { 983 ApplicationDependencies.getSignalCallManager().setRingGroup(false); 984 } 985 } 986 } 987 988 private void startCall(boolean isVideoCall) { 989 enableVideoIfAvailable = isVideoCall; 990 991 if (isVideoCall) { 992 ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get()); 993 } else { 994 ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get()); 995 } 996 997 MessageSender.onMessageSent(); 998 } 999 1000 @Override 1001 public void onReactWithAnyEmojiDialogDismissed() { /* no-op */ } 1002 1003 @Override 1004 public void onReactWithAnyEmojiSelected(@NonNull String emoji) { 1005 ApplicationDependencies.getSignalCallManager().react(emoji); 1006 callOverflowPopupWindow.dismiss(); 1007 } 1008 1009 private final class ControlsListener implements WebRtcCallView.ControlsListener { 1010 1011 @Override 1012 public void onStartCall(boolean isVideoCall) { 1013 viewModel.startCall(isVideoCall); 1014 } 1015 1016 @Override 1017 public void onCancelStartCall() { 1018 finish(); 1019 } 1020 1021 @Override 1022 public void toggleControls() { 1023 WebRtcControls controlState = viewModel.getWebRtcControls().getValue(); 1024 if (controlState != null && !controlState.displayIncomingCallButtons()) { 1025 controlsAndInfo.toggleControls(); 1026 } 1027 } 1028 1029 @Override 1030 public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) { 1031 maybeDisplaySpeakerphonePopup(audioOutput); 1032 switch (audioOutput) { 1033 case HANDSET: 1034 handleSetAudioHandset(); 1035 break; 1036 case BLUETOOTH_HEADSET: 1037 handleSetAudioBluetooth(); 1038 break; 1039 case SPEAKER: 1040 handleSetAudioSpeaker(); 1041 break; 1042 case WIRED_HEADSET: 1043 handleSetAudioWiredHeadset(); 1044 break; 1045 default: 1046 throw new IllegalStateException("Unknown output: " + audioOutput); 1047 } 1048 } 1049 1050 @RequiresApi(31) 1051 @Override 1052 public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) { 1053 maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput()); 1054 ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId())); 1055 } 1056 1057 @Override 1058 public void onVideoChanged(boolean isVideoEnabled) { 1059 handleSetMuteVideo(!isVideoEnabled); 1060 } 1061 1062 @Override 1063 public void onMicChanged(boolean isMicEnabled) { 1064 callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON 1065 : CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF); 1066 handleSetMuteAudio(!isMicEnabled); 1067 } 1068 1069 @Override 1070 public void onCameraDirectionChanged() { 1071 handleFlipCamera(); 1072 } 1073 1074 @Override 1075 public void onEndCallPressed() { 1076 handleEndCall(); 1077 } 1078 1079 @Override 1080 public void onDenyCallPressed() { 1081 handleDenyCall(); 1082 } 1083 1084 @Override 1085 public void onAcceptCallWithVoiceOnlyPressed() { 1086 handleAnswerWithAudio(); 1087 } 1088 1089 @Override 1090 public void onOverflowClicked() { 1091 controlsAndInfo.toggleOverflowPopup(); 1092 } 1093 1094 @Override 1095 public void onAcceptCallPressed() { 1096 if (viewModel.isAnswerWithVideoAvailable()) { 1097 handleAnswerWithVideo(); 1098 } else { 1099 handleAnswerWithAudio(); 1100 } 1101 } 1102 1103 @Override 1104 public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) { 1105 viewModel.setIsViewingFocusedParticipant(page); 1106 } 1107 1108 @Override 1109 public void onLocalPictureInPictureClicked() { 1110 viewModel.onLocalPictureInPictureClicked(); 1111 controlsAndInfo.restartHideControlsTimer(); 1112 } 1113 1114 @Override 1115 public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) { 1116 if (ringingAllowed) { 1117 ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup); 1118 callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallStateUpdatePopupWindow.CallStateUpdate.RINGING_ON 1119 : CallStateUpdatePopupWindow.CallStateUpdate.RINGING_OFF); 1120 } else { 1121 ApplicationDependencies.getSignalCallManager().setRingGroup(false); 1122 callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.RINGING_DISABLED); 1123 } 1124 } 1125 1126 @Override 1127 public void onCallInfoClicked() { 1128 controlsAndInfo.showCallInfo(); 1129 } 1130 1131 @Override 1132 public void onNavigateUpClicked() { 1133 onBackPressed(); 1134 } 1135 } 1136 1137 private void maybeDisplaySpeakerphonePopup(WebRtcAudioOutput nextOutput) { 1138 final WebRtcAudioOutput currentOutput = viewModel.getCurrentAudioOutput(); 1139 if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) { 1140 callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_OFF); 1141 } else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) { 1142 callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_ON); 1143 } 1144 } 1145 1146 private class PendingParticipantsViewListener implements PendingParticipantsView.Listener { 1147 1148 @Override 1149 public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) { 1150 ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId()); 1151 } 1152 1153 @Override 1154 public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) { 1155 ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId()); 1156 } 1157 1158 @Override 1159 public void onLaunchPendingRequestsSheet() { 1160 new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); 1161 } 1162 1163 @Override 1164 public void onLaunchRecipientSheet(@NonNull Recipient pendingRecipient) { 1165 CallLinkIncomingRequestSheet.show(getSupportFragmentManager(), pendingRecipient.getId()); 1166 } 1167 } 1168 1169 private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> { 1170 1171 @Override 1172 public void accept(WindowLayoutInfo windowLayoutInfo) { 1173 Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString()); 1174 1175 Optional<DisplayFeature> feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst(); 1176 viewModel.setIsLandscapeEnabled(feature.isPresent()); 1177 setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 1178 if (feature.isPresent()) { 1179 FoldingFeature foldingFeature = (FoldingFeature) feature.get(); 1180 Rect bounds = foldingFeature.getBounds(); 1181 if (foldingFeature.isSeparating()) { 1182 Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode"); 1183 viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top)); 1184 } else { 1185 Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode"); 1186 viewModel.setFoldableState(WebRtcControls.FoldableState.flat()); 1187 } 1188 } 1189 } 1190 } 1191 1192 private class FadeCallback implements ControlsAndInfoController.BottomSheetVisibilityListener { 1193 1194 @Override 1195 public void onShown() { 1196 fullscreenHelper.showSystemUI(); 1197 } 1198 1199 @Override 1200 public void onHidden() { 1201 fullscreenHelper.hideSystemUI(); 1202 if (videoTooltip != null) { 1203 videoTooltip.dismiss(); 1204 } 1205 } 1206 } 1207}