That fuck shit the fascists are using
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}