diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8995b88543..8c3a3028fe 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -128,7 +128,7 @@
- .
- */
-
-package org.thoughtcrime.securesms;
-
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.app.PictureInPictureParams;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.media.AudioManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.util.Pair;
-import android.util.Rational;
-import android.view.Surface;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.core.content.ContextCompat;
-import androidx.core.util.Consumer;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.LiveDataReactiveStreams;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Transformations;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
-import androidx.window.layout.DisplayFeature;
-import androidx.window.layout.FoldingFeature;
-import androidx.window.layout.WindowInfoTracker;
-import androidx.window.layout.WindowLayoutInfo;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.signal.core.util.ThreadUtil;
-import org.signal.core.util.concurrent.LifecycleDisposable;
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.logging.Log;
-import org.signal.libsignal.protocol.IdentityKey;
-import org.signal.ringrtc.GroupCall;
-import org.thoughtcrime.securesms.components.TooltipPopup;
-import org.thoughtcrime.securesms.components.sensors.Orientation;
-import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender;
-import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow;
-import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
-import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
-import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber;
-import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
-import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
-import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
-import org.thoughtcrime.securesms.components.webrtc.InCallStatus;
-import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsBottomSheet;
-import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView;
-import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice;
-import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
-import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
-import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
-import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
-import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow;
-import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoController;
-import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel;
-import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
-import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
-import org.thoughtcrime.securesms.components.webrtc.v2.CallControlsChange;
-import org.thoughtcrime.securesms.components.webrtc.v2.CallEvent;
-import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
-import org.thoughtcrime.securesms.components.webrtc.v2.CallPermissionsDialogController;
-import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
-import org.thoughtcrime.securesms.dependencies.AppDependencies;
-import org.thoughtcrime.securesms.events.WebRtcViewModel;
-import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
-import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
-import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
-import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason;
-import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
-import org.thoughtcrime.securesms.sms.MessageSender;
-import org.thoughtcrime.securesms.util.BottomSheetUtil;
-import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
-import org.thoughtcrime.securesms.util.FullscreenHelper;
-import org.thoughtcrime.securesms.util.RemoteConfig;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.ThrottledDebouncer;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.VibrateUtil;
-import org.thoughtcrime.securesms.util.WindowUtil;
-import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
-import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
-import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
-import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.core.BackpressureStrategy;
-import io.reactivex.rxjava3.disposables.Disposable;
-
-import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
-
-public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback, RecaptchaProofBottomSheetFragment.Callback {
-
- private static final String TAG = Log.tag(WebRtcCallActivity.class);
-
- private static final int STANDARD_DELAY_FINISH = 1000;
- private static final int VIBRATE_DURATION = 50;
-
- private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
- private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
- private CallOverflowPopupWindow callOverflowPopupWindow;
- private WifiToCellularPopupWindow wifiToCellularPopupWindow;
-
- private FullscreenHelper fullscreenHelper;
- private WebRtcCallView callScreen;
- private TooltipPopup videoTooltip;
- private TooltipPopup switchCameraTooltip;
- private WebRtcCallViewModel viewModel;
- private ControlsAndInfoViewModel controlsAndInfoViewModel;
- private boolean enableVideoIfAvailable;
- private boolean hasWarnedAboutBluetooth;
- private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
- private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter;
- private ThrottledDebouncer requestNewSizesThrottle;
- private PictureInPictureParams.Builder pipBuilderParams;
- private LifecycleDisposable lifecycleDisposable;
- private long lastCallLinkDisconnectDialogShowTime;
- private ControlsAndInfoController controlsAndInfo;
- private boolean enterPipOnResume;
- private long lastProcessedIntentTimestamp;
- private WebRtcViewModel previousEvent = null;
- private Disposable ephemeralStateDisposable = Disposable.empty();
- private CallPermissionsDialogController callPermissionsDialogController = new CallPermissionsDialogController();
-
- @Override
- protected void attachBaseContext(@NonNull Context newBase) {
- getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
- super.attachBaseContext(newBase);
- }
-
- @SuppressLint({ "MissingInflatedId" })
- @Override
- public void onCreate(Bundle savedInstanceState) {
- CallIntent callIntent = getCallIntent();
- Log.i(TAG, "onCreate(" + callIntent.isStartedFromFullScreen() + ")");
-
- lifecycleDisposable = new LifecycleDisposable();
- lifecycleDisposable.bindTo(this);
-
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- super.onCreate(savedInstanceState);
-
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.webrtc_call_activity);
-
- fullscreenHelper = new FullscreenHelper(this);
-
- setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
-
- initializeResources();
- initializeViewModel();
- initializePictureInPictureParams();
-
- controlsAndInfo = new ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel);
- controlsAndInfo.addVisibilityListener(new FadeCallback());
-
- fullscreenHelper.showAndHideWithSystemUI(getWindow(),
- findViewById(R.id.call_screen_header_gradient),
- findViewById(R.id.webrtc_call_view_toolbar_text),
- findViewById(R.id.webrtc_call_view_toolbar_no_text));
-
- lifecycleDisposable.add(controlsAndInfo);
-
- if (savedInstanceState == null) {
- logIntent(callIntent);
-
- if (callIntent.getAction() == CallIntent.Action.ANSWER_VIDEO) {
- enableVideoIfAvailable = true;
- } else if (callIntent.getAction() == CallIntent.Action.ANSWER_AUDIO || callIntent.isStartedFromFullScreen()) {
- enableVideoIfAvailable = false;
- } else {
- enableVideoIfAvailable = callIntent.shouldEnableVideoIfAvailable();
- callIntent.setShouldEnableVideoIfAvailable(false);
- }
-
- processIntent(callIntent);
- } else {
- Log.d(TAG, "Activity likely rotated, not processing intent");
- }
-
- registerSystemPipChangeListeners();
-
- windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
-
- windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
- windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
-
- requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
-
- initializePendingParticipantFragmentListener();
-
- WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface));
-
- if (!hasCameraPermission() & !hasAudioPermission()) {
- askCameraAudioPermissions(() -> {
- callScreen.setMicEnabled(viewModel.getMicrophoneEnabled().getValue());
- handleSetMuteVideo(false);
- });
- } else if (!hasAudioPermission()) {
- askAudioPermissions(() -> callScreen.setMicEnabled(viewModel.getMicrophoneEnabled().getValue()));
- }
- }
-
- private void registerSystemPipChangeListeners() {
- addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfo -> {
- CallParticipantsListDialog.dismiss(getSupportFragmentManager());
- CallReactionScrubber.dismissCustomEmojiBottomSheet(getSupportFragmentManager());
- });
- }
-
- @Override
- protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- controlsAndInfo.onStateRestored();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
-
- ephemeralStateDisposable = AppDependencies.getSignalCallManager()
- .ephemeralStates()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(viewModel::updateFromEphemeralState);
- }
-
- @Override
- public void onResume() {
- Log.i(TAG, "onResume()");
- super.onResume();
-
- initializeScreenshotSecurity();
-
- if (!EventBus.getDefault().isRegistered(this)) {
- EventBus.getDefault().register(this);
- }
-
- WebRtcViewModel rtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
- if (rtcViewModel == null) {
- Log.w(TAG, "Activity resumed without service event, perform delay destroy");
- ThreadUtil.runOnMainDelayed(() -> {
- WebRtcViewModel delayRtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
- if (delayRtcViewModel == null) {
- Log.w(TAG, "Activity still without service event, finishing activity");
- finish();
- } else {
- Log.i(TAG, "Event found after delay");
- }
- }, TimeUnit.SECONDS.toMillis(1));
- }
-
- if (enterPipOnResume) {
- enterPipOnResume = false;
- enterPipModeIfPossible();
- }
-
- if (SignalStore.rateLimit().needsRecaptcha()) {
- RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
- }
- }
-
- @Override
- public void onNewIntent(Intent intent) {
- CallIntent callIntent = getCallIntent();
- Log.i(TAG, "onNewIntent(" + callIntent.isStartedFromFullScreen() + ")");
- super.onNewIntent(intent);
- logIntent(callIntent);
- processIntent(callIntent);
- }
-
- @Override
- public void onPause() {
- Log.i(TAG, "onPause");
- super.onPause();
-
- if (!isInPipMode() || isFinishing()) {
- EventBus.getDefault().unregister(this);
- }
-
- if (!callPermissionsDialogController.isAskingForPermission() && !viewModel.isCallStarting() && !isChangingConfigurations()) {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
- if (state != null && (state.getCallState().isPreJoinOrNetworkUnavailable() || state.getCallState().isIncomingOrHandledElsewhere())) {
- finish();
- }
- }
- }
-
- @Override
- protected void onStop() {
- Log.i(TAG, "onStop");
- super.onStop();
-
- ephemeralStateDisposable.dispose();
-
- if (!isInPipMode() || isFinishing()) {
- EventBus.getDefault().unregister(this);
- requestNewSizesThrottle.clear();
- }
-
- AppDependencies.getSignalCallManager().setEnableVideo(false);
-
- if (!viewModel.isCallStarting() && !isChangingConfigurations()) {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
- if (state != null) {
- if (state.getCallState().isPreJoinOrNetworkUnavailable() || state.getCallState().isIncomingOrHandledElsewhere()) {
- AppDependencies.getSignalCallManager().cancelPreJoin();
- } else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
- AppDependencies.getSignalCallManager().relaunchPipOnForeground();
- }
- }
- }
- }
-
- @Override
- protected void onDestroy() {
- Log.d(TAG, "onDestroy");
- super.onDestroy();
- windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
- EventBus.getDefault().unregister(this);
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onRecaptchaRequiredEvent(RecaptchaRequiredEvent recaptchaRequiredEvent) {
- RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
- }
-
- @SuppressLint("MissingSuperCall")
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
- }
-
- @Override
- protected void onUserLeaveHint() {
- super.onUserLeaveHint();
- enterPipModeIfPossible();
- }
-
- @Override
- public void onBackPressed() {
- if (!enterPipModeIfPossible()) {
- super.onBackPressed();
- }
- }
-
- private @NonNull CallIntent getCallIntent() {
- return new CallIntent(getIntent());
- }
-
- private boolean enterPipModeIfPossible() {
- if (isSystemPipEnabledAndAvailable()) {
- if (Boolean.TRUE.equals(viewModel.canEnterPipMode().getValue())) {
- try {
- enterPictureInPictureMode(pipBuilderParams.build());
- } catch (Exception e) {
- Log.w(TAG, "Device lied to us about supporting PiP.", e);
- return false;
- }
-
- return true;
- }
-
- if (Build.VERSION.SDK_INT >= 31) {
- pipBuilderParams.setAutoEnterEnabled(false);
- }
- }
- return false;
- }
-
- private boolean isInPipMode() {
- return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
- }
-
- private void logIntent(@NonNull CallIntent intent) {
- Log.d(TAG, intent.toString());
- }
-
- private void processIntent(@NonNull CallIntent intent) {
- switch (intent.getAction()) {
- case ANSWER_AUDIO -> handleAnswerWithAudio();
- case ANSWER_VIDEO -> handleAnswerWithVideo();
- case DENY -> handleDenyCall();
- case END_CALL -> handleEndCall();
- }
-
- if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) {
- enterPipOnResume = intent.shouldLaunchInPip();
- }
-
- lastProcessedIntentTimestamp = System.currentTimeMillis();
- }
-
- private void initializePendingParticipantFragmentListener() {
- getSupportFragmentManager().setFragmentResultListener(
- PendingParticipantsBottomSheet.REQUEST_KEY,
- this,
- (requestKey, result) -> {
- PendingParticipantsBottomSheet.Action action = PendingParticipantsBottomSheet.getAction(result);
- List recipientIds = viewModel.getPendingParticipantsSnapshot()
- .getUnresolvedPendingParticipants()
- .stream()
- .map(r -> r.getRecipient().getId())
- .collect(Collectors.toList());
-
- switch (action) {
- case NONE:
- break;
- case APPROVE_ALL:
- new MaterialAlertDialogBuilder(this)
- .setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__approve_d_requests, recipientIds.size(), recipientIds.size()))
- .setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
- for (RecipientId id : recipientIds) {
- AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
- }
- })
- .show();
- break;
- case DENY_ALL:
- new MaterialAlertDialogBuilder(this)
- .setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__deny_d_requests, recipientIds.size(), recipientIds.size()))
- .setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_not_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
- for (RecipientId id : recipientIds) {
- AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
- }
- })
- .show();
- break;
- }
- }
- );
- }
-
- private void initializeScreenshotSecurity() {
- if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
- } else {
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
- }
- }
-
- private void initializeResources() {
- callScreen = findViewById(R.id.callScreen);
- callScreen.setControlsListener(new ControlsListener());
-
- participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
- callStateUpdatePopupWindow = new CallStateUpdatePopupWindow(callScreen);
- wifiToCellularPopupWindow = new WifiToCellularPopupWindow(callScreen);
- callOverflowPopupWindow = new CallOverflowPopupWindow(this, callScreen, () -> {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
- if (state == null) {
- return false;
- }
- return state.getLocalParticipant().isHandRaised();
- });
-
- getLifecycle().addObserver(participantUpdateWindow);
- }
-
- private @NonNull Orientation resolveOrientationFromContext() {
- int displayOrientation = getResources().getConfiguration().orientation;
- int displayRotation = ContextCompat.getDisplayOrDefault(this).getRotation();
-
- if (displayOrientation == Configuration.ORIENTATION_PORTRAIT) {
- return Orientation.PORTRAIT_BOTTOM_EDGE;
- } else if (displayRotation == Surface.ROTATION_270) {
- return Orientation.LANDSCAPE_RIGHT_EDGE;
- } else {
- return Orientation.LANDSCAPE_LEFT_EDGE;
- }
- }
-
- private void initializeViewModel() {
- final Orientation orientation = resolveOrientationFromContext();
- if (orientation == PORTRAIT_BOTTOM_EDGE) {
- WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface2));
- WindowUtil.clearTranslucentNavigationBar(getWindow());
- }
-
- LiveData> orientationAndLandscapeEnabled = Transformations.map(new MutableLiveData<>(orientation), o -> Pair.create(o, true));
-
- viewModel = new ViewModelProvider(this).get(WebRtcCallViewModel.class);
- viewModel.setIsLandscapeEnabled(true);
- viewModel.setIsInPipMode(isInPipMode());
- viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
- viewModel.getWebRtcControls().observe(this, controls -> {
- callScreen.setWebRtcControls(controls);
- controlsAndInfo.updateControls(controls);
- });
- viewModel.getEvents().observe(this, this::handleViewModelEvent);
-
- lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus));
- lifecycleDisposable.add(viewModel.getRecipientFlowable().subscribe(callScreen::setRecipient));
-
- boolean isStartedFromCallLink = getCallIntent().isStartedFromCallLink();
- LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)),
- orientationAndLandscapeEnabled,
- viewModel.getEphemeralState(),
- (s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink))
- .observe(this, p -> callScreen.updateCallParticipants(p));
- viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
- viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
- viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
- viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
- lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint));
-
- callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
- if (state != null) {
- if (state.needsNewRequestSizes()) {
- requestNewSizesThrottle.publish(() -> AppDependencies.getSignalCallManager().updateRenderedResolutions());
- }
- }
- });
-
- orientationAndLandscapeEnabled.observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
-
- addOnPictureInPictureModeChangedListener(info -> {
- viewModel.setIsInPipMode(info.isInPictureInPictureMode());
- participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode());
- callStateUpdatePopupWindow.setEnabled(!info.isInPictureInPictureMode());
- if (info.isInPictureInPictureMode()) {
- callScreen.maybeDismissAudioPicker();
- }
- viewModel.setIsLandscapeEnabled(info.isInPictureInPictureMode());
- });
-
- callScreen.setPendingParticipantsViewListener(new PendingParticipantsViewListener());
- Disposable disposable = viewModel.getPendingParticipants()
- .subscribe(callScreen::updatePendingParticipantsList);
-
- lifecycleDisposable.add(disposable);
-
- controlsAndInfoViewModel = new ViewModelProvider(this).get(ControlsAndInfoViewModel.class);
- }
-
- private void initializePictureInPictureParams() {
- if (isSystemPipEnabledAndAvailable()) {
- final Orientation orientation = resolveOrientationFromContext();
- final Rational aspectRatio;
-
- if (orientation == PORTRAIT_BOTTOM_EDGE) {
- aspectRatio = new Rational(9, 16);
- } else {
- aspectRatio = new Rational(16, 9);
- }
-
- pipBuilderParams = new PictureInPictureParams.Builder();
- pipBuilderParams.setAspectRatio(aspectRatio);
-
- if (Build.VERSION.SDK_INT >= 31) {
- viewModel.canEnterPipMode().observe(this, canEnterPipMode -> {
- pipBuilderParams.setAutoEnterEnabled(canEnterPipMode);
- tryToSetPictureInPictureParams();
- });
- } else {
- tryToSetPictureInPictureParams();
- }
- }
- }
-
- private void tryToSetPictureInPictureParams() {
- if (Build.VERSION.SDK_INT >= 26) {
- try {
- setPictureInPictureParams(pipBuilderParams.build());
- } catch (Exception e) {
- Log.w(TAG, "System lied about having PiP available.", e);
- }
- }
- }
-
- private void handleViewModelEvent(@NonNull CallEvent event) {
- if (event instanceof CallEvent.StartCall) {
- startCall(((CallEvent.StartCall) event).isVideoCall());
- return;
- } else if (event instanceof CallEvent.ShowGroupCallSafetyNumberChange) {
- SafetyNumberBottomSheet.forGroupCall(((CallEvent.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
- .show(getSupportFragmentManager());
- return;
- } else if (event instanceof CallEvent.SwitchToSpeaker) {
- callScreen.switchToSpeakerView();
- return;
- } else if (event instanceof CallEvent.ShowSwipeToSpeakerHint) {
- CallToastPopupWindow.show(callScreen);
- return;
- }
-
- if (isInPipMode()) {
- return;
- }
-
- if (event instanceof CallEvent.ShowVideoTooltip) {
- if (videoTooltip == null) {
- videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget())
- .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
- .setTextColor(ContextCompat.getColor(this, R.color.core_white))
- .setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
- .setOnDismissListener(() -> viewModel.onDismissedVideoTooltip())
- .show(TooltipPopup.POSITION_ABOVE);
- }
- } else if (event instanceof CallEvent.DismissVideoTooltip) {
- if (videoTooltip != null) {
- videoTooltip.dismiss();
- videoTooltip = null;
- }
- } else if (event instanceof CallEvent.ShowWifiToCellularPopup) {
- wifiToCellularPopupWindow.show();
- } else if (event instanceof CallEvent.ShowSwitchCameraTooltip) {
- if (switchCameraTooltip == null) {
- switchCameraTooltip = TooltipPopup.forTarget(callScreen.getSwitchCameraTooltipTarget())
- .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
- .setTextColor(ContextCompat.getColor(this, R.color.core_white))
- .setText(R.string.WebRtcCallActivity__flip_camera_tooltip)
- .setOnDismissListener(() -> viewModel.onDismissedSwitchCameraTooltip())
- .show(TooltipPopup.POSITION_ABOVE);
- }
- } else if (event instanceof CallEvent.DismissSwitchCameraTooltip) {
- if (switchCameraTooltip != null) {
- switchCameraTooltip.dismiss();
- switchCameraTooltip = null;
- }
- } else {
- throw new IllegalArgumentException("Unknown event: " + event);
- }
- }
-
- private void handleInCallStatus(@NonNull InCallStatus inCallStatus) {
- if (inCallStatus instanceof InCallStatus.ElapsedTime) {
-
- EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(((InCallStatus.ElapsedTime) inCallStatus).getElapsedTime());
-
- if (ellapsedTimeFormatter == null) {
- return;
- }
-
- callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
- } else if (inCallStatus instanceof InCallStatus.PendingCallLinkUsers) {
- int waiting = ((InCallStatus.PendingCallLinkUsers) inCallStatus).getPendingUserCount();
-
- callScreen.setStatus(getResources().getQuantityString(
- R.plurals.WebRtcCallActivity__d_people_waiting,
- waiting,
- waiting
- ));
- } else if (inCallStatus instanceof InCallStatus.JoinedCallLinkUsers) {
- int joined = ((InCallStatus.JoinedCallLinkUsers) inCallStatus).getJoinedUserCount();
-
- callScreen.setStatus(getResources().getQuantityString(
- R.plurals.WebRtcCallActivity__d_people,
- joined,
- joined
- ));
- }else {
- throw new AssertionError();
- }
- }
-
- private void handleSetAudioHandset() {
- AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
- }
-
- private void handleSetAudioSpeaker() {
- AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
- }
-
- private void handleSetAudioBluetooth() {
- AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
- }
-
- private void handleSetAudioWiredHeadset() {
- AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
- }
-
- private void handleSetMuteAudio(boolean enabled) {
- AppDependencies.getSignalCallManager().setMuteAudio(enabled);
- }
-
- private void handleSetMuteVideo(boolean muted) {
- Recipient recipient = viewModel.getRecipient().get();
-
- if (!recipient.equals(Recipient.UNKNOWN)) {
- Runnable onGranted = () -> AppDependencies.getSignalCallManager().setEnableVideo(!muted);
- askCameraPermissions(onGranted);
- }
- }
-
- private void handleFlipCamera() {
- AppDependencies.getSignalCallManager().flipCamera();
- }
-
- private void handleAnswerWithAudio() {
- Runnable onGranted = () -> {
- callScreen.setStatus(getString(R.string.RedPhone_answering));
- AppDependencies.getSignalCallManager().acceptCall(false);
- };
- askAudioPermissions(onGranted);
- }
-
- private void handleAnswerWithVideo() {
- Runnable onGranted = () -> {
- callScreen.setStatus(getString(R.string.RedPhone_answering));
- AppDependencies.getSignalCallManager().acceptCall(true);
- handleSetMuteVideo(false);
- };
- if (!hasCameraPermission() &!hasAudioPermission()) {
- askCameraAudioPermissions(onGranted);
- } else if (!hasAudioPermission()) {
- askAudioPermissions(onGranted);
- } else {
- askCameraPermissions(onGranted);
- }
- }
-
- private void handleDenyCall() {
- Recipient recipient = viewModel.getRecipient().get();
-
- if (!recipient.equals(Recipient.UNKNOWN)) {
- AppDependencies.getSignalCallManager().denyCall();
-
- callScreen.setRecipient(recipient);
- callScreen.setStatus(getString(R.string.RedPhone_ending_call));
- delayedFinish();
- }
- }
-
- private void handleEndCall() {
- Log.i(TAG, "Hangup pressed, handling termination now...");
- AppDependencies.getSignalCallManager().localHangup();
- }
-
- private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
- if (event.getGroupState().isNotIdle()) {
- callScreen.setStatusFromGroupCallState(event.getGroupState());
- } else {
- callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
- }
- }
-
- private void handleGroupCallHasMaxDevices(@NonNull Recipient recipient) {
- new MaterialAlertDialogBuilder(this)
- .setMessage(R.string.WebRtcCallView__call_is_full)
- .setPositiveButton(android.R.string.ok, (d, w) -> handleTerminate(recipient, HangupMessage.Type.NORMAL))
- .show();
- }
-
- private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
- Log.i(TAG, "handleTerminate called: " + hangupType.name());
-
- callScreen.setStatusFromHangupType(hangupType);
-
- EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
-
- if (hangupType == HangupMessage.Type.NEED_PERMISSION) {
- startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.getId()));
- }
- delayedFinish();
- }
-
- private void handleGlare(@NonNull Recipient recipient) {
- Log.i(TAG, "handleGlare: " + recipient.getId());
-
- callScreen.setStatus("");
- }
-
- private void handleCallRinging() {
- callScreen.setStatus(getString(R.string.RedPhone_ringing));
- }
-
- private void handleCallBusy() {
- EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setStatus(getString(R.string.RedPhone_busy));
- delayedFinish(SignalCallManager.BUSY_TONE_LENGTH);
- }
-
- private void handleCallConnected(@NonNull WebRtcViewModel event) {
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
- if (event.getGroupState().isNotIdleOrConnected()) {
- callScreen.setStatusFromGroupCallState(event.getGroupState());
- }
- }
-
- private void handleCallReconnecting() {
- callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting));
- VibrateUtil.vibrate(this, VIBRATE_DURATION);
- }
-
- private void handleRecipientUnavailable() {
- EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
- delayedFinish();
- }
-
- private void handleServerFailure() {
- EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setStatus(getString(R.string.RedPhone_network_failed));
- }
-
- private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
- if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.RedPhone_number_not_registered)
- .setIcon(R.drawable.symbol_error_triangle_fill_24)
- .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
- .setCancelable(true)
- .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
- .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
- .show();
- }
-
- private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
- final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
- final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
-
- if (theirKey == null) {
- Log.w(TAG, "Untrusted identity without an identity key.");
- }
-
- SafetyNumberBottomSheet.forCall(recipient.getId()).show(getSupportFragmentManager());
- }
-
- public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) {
- if (Util.hasItems(safetyNumberChangeEvent.getRecipientIds())) {
- if (safetyNumberChangeEvent.isInPipMode()) {
- GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get());
- } else {
- GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get());
- SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.getRecipientIds()).show(getSupportFragmentManager());
- }
- }
- }
-
- private void updateGroupMembersForGroupCall() {
- AppDependencies.getSignalCallManager().requestUpdateGroupMembers();
- }
-
- public void handleGroupMemberCountChange(int count) {
- boolean canRing = count <= RemoteConfig.maxGroupCallRingSize();
- callScreen.enableRingGroup(canRing);
- AppDependencies.getSignalCallManager().setRingGroup(canRing);
- }
-
- private void updateSpeakerHint(boolean showSpeakerHint) {
- if (showSpeakerHint) {
- callScreen.showSpeakerViewHint();
- } else {
- callScreen.hideSpeakerViewHint();
- }
- }
-
- @Override
- public void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients) {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
-
- if (state == null) {
- return;
- }
-
- if (state.isCallLink()) {
- CallLinkProfileKeySender.onSendAnyway(new HashSet<>(changedRecipients));
- }
-
- if (state.getGroupCallState().isConnected()) {
- AppDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
- } else {
- viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
- }
- }
-
- @Override
- public void onMessageResentAfterSafetyNumberChange() {}
-
- @Override
- public void onCanceled() {
- CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
- if (state != null && state.getGroupCallState().isNotIdle()) {
- if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
- AppDependencies.getSignalCallManager().cancelPreJoin();
- finish();
- } else {
- handleEndCall();
- }
- } else {
- handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
- }
- }
-
- private boolean isSystemPipEnabledAndAvailable() {
- return Build.VERSION.SDK_INT >= 26 && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
- }
-
- private void delayedFinish() {
- delayedFinish(STANDARD_DELAY_FINISH);
- }
-
- private void delayedFinish(int delayMillis) {
- callScreen.postDelayed(WebRtcCallActivity.this::finish, delayMillis);
- }
-
- @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
- public void onEventMainThread(@NonNull WebRtcViewModel event) {
- Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
-
- WebRtcViewModel.State previousCallState = previousEvent != null ? previousEvent.getState() : null;
-
- previousEvent = event;
-
- viewModel.setRecipient(event.getRecipient());
- controlsAndInfoViewModel.setRecipient(event.getRecipient());
-
- switch (event.getState()) {
- case CALL_INCOMING:
- if (previousCallState == WebRtcViewModel.State.NETWORK_FAILURE) {
- Log.d(TAG, "Incoming call directly from network failure state. Recreating activity.");
- recreate();
- return;
- }
-
- break;
- case CALL_PRE_JOIN:
- handleCallPreJoin(event); break;
- case CALL_CONNECTED:
- handleCallConnected(event); break;
- case CALL_RECONNECTING:
- handleCallReconnecting(); break;
- case NETWORK_FAILURE:
- handleServerFailure(); break;
- case CALL_RINGING:
- handleCallRinging(); break;
- case CALL_DISCONNECTED:
- if (event.getGroupCallEndReason() == GroupCall.GroupCallEndReason.HAS_MAX_DEVICES) {
- handleGroupCallHasMaxDevices(event.getRecipient());
- } else {
- handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
- }
-
- break;
- case CALL_DISCONNECTED_GLARE:
- handleGlare(event.getRecipient()); break;
- case CALL_ACCEPTED_ELSEWHERE:
- handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
- case CALL_DECLINED_ELSEWHERE:
- handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
- case CALL_ONGOING_ELSEWHERE:
- handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
- case CALL_NEEDS_PERMISSION:
- handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
- case NO_SUCH_USER:
- handleNoSuchUser(event); break;
- case RECIPIENT_UNAVAILABLE:
- handleRecipientUnavailable(); break;
- case CALL_OUTGOING:
- handleOutgoingCall(event); break;
- case CALL_BUSY:
- handleCallBusy(); break;
- case UNTRUSTED_IDENTITY:
- handleUntrustedIdentity(event); break;
- }
-
- if (event.getCallLinkDisconnectReason() != null && event.getCallLinkDisconnectReason().getPostedAt() > lastCallLinkDisconnectDialogShowTime) {
- lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis();
-
- if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.RemovedFromCall) {
- displayRemovedFromCallLinkDialog();
- } else if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.DeniedRequestToJoinCall) {
- displayDeniedRequestToJoinCallLinkDialog();
- } else {
- throw new AssertionError("Unexpected reason: " + event.getCallLinkDisconnectReason());
- }
- }
-
- boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
-
- viewModel.updateFromWebRtcViewModel(event, enableVideo);
-
- if (enableVideo) {
- enableVideoIfAvailable = false;
- handleSetMuteVideo(false);
- }
-
- if (event.getBluetoothPermissionDenied() && !hasWarnedAboutBluetooth && !isFinishing()) {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied)
- .setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
- .setPositiveButton(R.string.WebRtcCallActivity__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(this)))
- .setNegativeButton(R.string.WebRtcCallActivity__not_now, null)
- .show();
-
- hasWarnedAboutBluetooth = true;
- }
- }
-
- private void displayRemovedFromCallLinkDialog() {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.WebRtcCallActivity__removed_from_call)
- .setMessage(R.string.WebRtcCallActivity__someone_has_removed_you_from_the_call)
- .setPositiveButton(android.R.string.ok, null)
- .show();
- }
-
- private void displayDeniedRequestToJoinCallLinkDialog() {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.WebRtcCallActivity__join_request_denied)
- .setMessage(R.string.WebRtcCallActivity__your_request_to_join_this_call_has_been_denied)
- .setPositiveButton(android.R.string.ok, null)
- .show();
- }
-
- private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
- if (event.getGroupState().isNotIdle()) {
- callScreen.setRingGroup(event.shouldRingGroup());
-
- if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
- AppDependencies.getSignalCallManager().setRingGroup(false);
- }
- }
- }
-
- private boolean hasCameraPermission() {
- return Permissions.hasAll(this, Manifest.permission.CAMERA);
- }
-
- private boolean hasAudioPermission() {
- return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO);
- }
-
- private void askCameraPermissions(@NonNull Runnable onGranted) {
- callPermissionsDialogController.requestCameraPermission(
- this,
- () -> {
- onGranted.run();
- findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
- }
- );
- }
-
- private void askAudioPermissions(@NonNull Runnable onGranted) {
- callPermissionsDialogController.requestAudioPermission(
- this,
- onGranted,
- this::handleDenyCall
- );
- }
-
- public void askCameraAudioPermissions(@NonNull Runnable onGranted) {
- callPermissionsDialogController.requestCameraAndAudioPermission(
- this,
- onGranted,
- () -> findViewById(R.id.missing_permissions_container).setVisibility(View.GONE),
- this::handleDenyCall
- );
- }
-
- private void startCall(boolean isVideoCall) {
- enableVideoIfAvailable = isVideoCall;
-
- if (isVideoCall) {
- AppDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
- } else {
- AppDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
- }
-
- MessageSender.onMessageSent();
- }
-
- @Override
- public void onReactWithAnyEmojiDialogDismissed() { /* no-op */ }
-
- @Override
- public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
- AppDependencies.getSignalCallManager().react(emoji);
- callOverflowPopupWindow.dismiss();
- }
-
- @Override
- public void onProofCompleted() {
- AppDependencies.getSignalCallManager().resendMediaKeys();
- }
-
- private final class ControlsListener implements WebRtcCallView.ControlsListener {
-
- @Override
- public void onStartCall(boolean isVideoCall) {
- viewModel.startCall(isVideoCall);
- }
-
- @Override
- public void onCancelStartCall() {
- finish();
- }
-
- @Override
- public void toggleControls() {
- WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
- if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
- controlsAndInfo.toggleControls();
- }
- }
-
- @Override
- public void onAudioPermissionsRequested(Runnable onGranted) {
- askAudioPermissions(onGranted);
- }
-
- @Override
- public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
- maybeDisplaySpeakerphonePopup(audioOutput);
- switch (audioOutput) {
- case HANDSET:
- handleSetAudioHandset();
- break;
- case BLUETOOTH_HEADSET:
- handleSetAudioBluetooth();
- break;
- case SPEAKER:
- handleSetAudioSpeaker();
- break;
- case WIRED_HEADSET:
- handleSetAudioWiredHeadset();
- break;
- default:
- throw new IllegalStateException("Unknown output: " + audioOutput);
- }
- }
-
- @RequiresApi(31)
- @Override
- public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
- maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
- AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
- }
-
- @Override
- public void onVideoChanged(boolean isVideoEnabled) {
- handleSetMuteVideo(!isVideoEnabled);
- }
-
- @Override
- public void onMicChanged(boolean isMicEnabled) {
- Runnable onGranted = () -> {
- callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallControlsChange.MIC_ON
- : CallControlsChange.MIC_OFF);
- handleSetMuteAudio(!isMicEnabled);
- };
- askAudioPermissions(onGranted);
- }
-
- @Override
- public void onCameraDirectionChanged() {
- handleFlipCamera();
- }
-
- @Override
- public void onEndCallPressed() {
- handleEndCall();
- }
-
- @Override
- public void onDenyCallPressed() {
- handleDenyCall();
- }
-
- @Override
- public void onAcceptCallWithVoiceOnlyPressed() {
- handleAnswerWithAudio();
- }
-
- @Override
- public void onOverflowClicked() {
- controlsAndInfo.toggleOverflowPopup();
- }
-
- @Override
- public void onAcceptCallPressed() {
- if (viewModel.isAnswerWithVideoAvailable()) {
- handleAnswerWithVideo();
- } else {
- handleAnswerWithAudio();
- }
- }
-
- @Override
- public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
- viewModel.setIsViewingFocusedParticipant(page);
- }
-
- @Override
- public void onLocalPictureInPictureClicked() {
- viewModel.onLocalPictureInPictureClicked();
- controlsAndInfo.restartHideControlsTimer();
- }
-
- @Override
- public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
- if (ringingAllowed) {
- AppDependencies.getSignalCallManager().setRingGroup(ringGroup);
- callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallControlsChange.RINGING_ON
- : CallControlsChange.RINGING_OFF);
- } else {
- AppDependencies.getSignalCallManager().setRingGroup(false);
- callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.RINGING_DISABLED);
- }
- }
-
- @Override
- public void onCallInfoClicked() {
- controlsAndInfo.showCallInfo();
- }
-
- @Override
- public void onNavigateUpClicked() {
- onBackPressed();
- }
- }
-
- private void maybeDisplaySpeakerphonePopup(WebRtcAudioOutput nextOutput) {
- final WebRtcAudioOutput currentOutput = viewModel.getCurrentAudioOutput();
- if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
- callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_OFF);
- } else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
- callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_ON);
- }
- }
-
- private class PendingParticipantsViewListener implements PendingParticipantsView.Listener {
-
- @Override
- public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
- AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
- }
-
- @Override
- public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
- AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
- }
-
- @Override
- public void onLaunchPendingRequestsSheet() {
- new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
- }
-
- @Override
- public void onLaunchRecipientSheet(@NonNull Recipient pendingRecipient) {
- CallLinkIncomingRequestSheet.show(getSupportFragmentManager(), pendingRecipient.getId());
- }
- }
-
- private class WindowLayoutInfoConsumer implements Consumer {
-
- @Override
- public void accept(WindowLayoutInfo windowLayoutInfo) {
- Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString());
-
- Optional feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst();
- if (feature.isPresent()) {
- FoldingFeature foldingFeature = (FoldingFeature) feature.get();
- Rect bounds = foldingFeature.getBounds();
- if (foldingFeature.isSeparating()) {
- Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
- viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
- } else {
- Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode");
- viewModel.setFoldableState(WebRtcControls.FoldableState.flat());
- }
- }
- }
- }
-
- private class FadeCallback implements ControlsAndInfoController.BottomSheetVisibilityListener {
-
- @Override
- public void onShown() {
- fullscreenHelper.showSystemUI();
- }
-
- @Override
- public void onHidden() {
- WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
- if (controlState == null || !controlState.displayErrorControls()) {
- fullscreenHelper.hideSystemUI();
- if (videoTooltip != null) {
- videoTooltip.dismiss();
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt
index f79ded7a80..9fa67b1de1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt
@@ -68,7 +68,7 @@ class CallOverflowPopupWindow(private val activity: FragmentActivity, parentView
PopupWindowCompat.showAsDropDown(this, anchor, xOffset, yOffset, Gravity.NO_GRAVITY)
}
- interface RaisedHandDelegate {
+ fun interface RaisedHandDelegate {
fun isSelfHandRaised(): Boolean
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java
index 24236bc86e..a06909e90b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java
@@ -11,7 +11,7 @@ import androidx.core.app.NotificationManagerCompat;
import org.signal.core.util.PendingIntentFlags;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.WebRtcCallActivity;
+import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -26,7 +26,7 @@ public final class GroupCallSafetyNumberChangeNotificationUtil {
}
public static void showNotification(@NonNull Context context, @NonNull Recipient recipient) {
- Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
+ Intent contentIntent = new Intent(context, CallIntent.getActivityClass());
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentFlags.mutable());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt
index b9bc2391bd..3d587ad131 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt
@@ -46,8 +46,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.parcelize.Parcelize
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.WebRtcCallActivity
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
@@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds
* Brain for rendering the call controls and info within a bottom sheet when we display the activity in portrait mode.
*/
class ControlsAndInfoController private constructor(
- private val webRtcCallActivity: WebRtcCallActivity,
+ private val activity: BaseActivity,
private val webRtcCallView: WebRtcCallView,
private val overflowPopupWindow: CallOverflowPopupWindow,
private val viewModel: WebRtcCallViewModel,
@@ -74,13 +74,13 @@ class ControlsAndInfoController private constructor(
) : Disposable by disposables {
constructor(
- webRtcCallActivity: WebRtcCallActivity,
+ activity: BaseActivity,
webRtcCallView: WebRtcCallView,
overflowPopupWindow: CallOverflowPopupWindow,
viewModel: WebRtcCallViewModel,
controlsAndInfoViewModel: ControlsAndInfoViewModel
) : this(
- webRtcCallActivity,
+ activity,
webRtcCallView,
overflowPopupWindow,
viewModel,
@@ -108,15 +108,15 @@ class ControlsAndInfoController private constructor(
private val aboveControlsGuideline: Guideline = webRtcCallView.findViewById(R.id.call_screen_above_controls_guideline)
private val toggleCameraDirectionView: View = webRtcCallView.findViewById(R.id.call_screen_camera_direction_toggle)
private val callControls: ConstraintLayout = webRtcCallView.findViewById(R.id.call_controls_constraint_layout)
- private val isLandscape = webRtcCallActivity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ private val isLandscape = activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
private val waitingToBeLetInProgressDrawable = IndeterminateDrawable.createCircularDrawable(
- webRtcCallActivity,
- CircularProgressIndicatorSpec(webRtcCallActivity, null).apply {
+ activity,
+ CircularProgressIndicatorSpec(activity, null).apply {
indicatorSize = 20.dp
indicatorInset = 0.dp
trackThickness = 2.dp
trackCornerRadius = 1.dp
- indicatorColors = intArrayOf(ContextCompat.getColor(webRtcCallActivity, R.color.signal_colorOnBackground))
+ indicatorColors = intArrayOf(ContextCompat.getColor(activity, R.color.signal_colorOnBackground))
trackColor = Color.TRANSPARENT
}
)
@@ -133,7 +133,7 @@ class ControlsAndInfoController private constructor(
private var previousCallControlHeightData = HeightData()
private var controlState: WebRtcControls = WebRtcControls.NONE
- private val callInfoCallbacks = CallInfoCallbacks(webRtcCallActivity, controlsAndInfoViewModel)
+ private val callInfoCallbacks = CallInfoCallbacks(activity, controlsAndInfoViewModel)
init {
raiseHandComposeView.apply {
@@ -179,9 +179,9 @@ class ControlsAndInfoController private constructor(
hide(delay = HIDE_CONTROL_DELAY)
}
- webRtcCallActivity
+ activity
.supportFragmentManager
- .setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, webRtcCallActivity) { resultKey, bundle ->
+ .setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, activity) { resultKey, bundle ->
if (bundle.containsKey(resultKey)) {
setName(bundle.getString(resultKey)!!)
}
@@ -193,7 +193,7 @@ class ControlsAndInfoController private constructor(
.setTopRightCorner(CornerFamily.ROUNDED, 18.dp.toFloat())
.build()
).apply {
- fillColor = ColorStateList.valueOf(ContextCompat.getColor(webRtcCallActivity, R.color.signal_colorSurface))
+ fillColor = ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.signal_colorSurface))
}
behavior.isHideable = true
@@ -440,7 +440,7 @@ class ControlsAndInfoController private constructor(
}
private fun toastFailure() {
- Toast.makeText(webRtcCallActivity, R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
+ Toast.makeText(activity, R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
}
private fun ConstraintSet.setControlConstraints(@IdRes viewId: Int, visible: Boolean, @Px horizontalMargins: Int) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallIntent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallIntent.kt
index d79d1fc67c..09878b0e04 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallIntent.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallIntent.kt
@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.components.webrtc.v2
import android.app.Activity
import android.content.Context
import android.content.Intent
-import org.thoughtcrime.securesms.WebRtcCallActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
@@ -23,7 +22,8 @@ class CallIntent(
private const val CALL_INTENT_PREFIX = "CallIntent"
- private fun getActivityClass(): Class = if (RemoteConfig.newCallUi || SignalStore.internal.newCallingUi) {
+ @JvmStatic
+ fun getActivityClass(): Class = if (RemoteConfig.newCallUi || SignalStore.internal.newCallingUi) {
CallActivity::class.java
} else {
WebRtcCallActivity::class.java
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt
new file mode 100644
index 0000000000..3aa76a31e7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt
@@ -0,0 +1,1196 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.webrtc.v2
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.PictureInPictureParams
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.media.AudioManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Rational
+import android.view.Surface
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.lifecycle.toLiveData
+import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowInfoTracker
+import androidx.window.layout.WindowLayoutInfo
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.BackpressureStrategy
+import io.reactivex.rxjava3.disposables.Disposable
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.signal.core.util.ThreadUtil
+import org.signal.core.util.concurrent.LifecycleDisposable
+import org.signal.core.util.concurrent.SignalExecutors
+import org.signal.core.util.logging.Log
+import org.signal.ringrtc.GroupCall
+import org.thoughtcrime.securesms.BaseActivity
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.TooltipPopup
+import org.thoughtcrime.securesms.components.sensors.Orientation
+import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender
+import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
+import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow
+import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
+import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber
+import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow
+import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow
+import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil
+import org.thoughtcrime.securesms.components.webrtc.InCallStatus
+import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsBottomSheet
+import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView
+import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
+import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
+import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
+import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
+import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel.SafetyNumberChangeEvent
+import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
+import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow
+import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoController
+import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
+import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog
+import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet
+import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
+import org.thoughtcrime.securesms.dependencies.AppDependencies
+import org.thoughtcrime.securesms.events.WebRtcViewModel
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity
+import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
+import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent
+import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
+import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason
+import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
+import org.thoughtcrime.securesms.sms.MessageSender
+import org.thoughtcrime.securesms.util.BottomSheetUtil
+import org.thoughtcrime.securesms.util.EllapsedTimeFormatter
+import org.thoughtcrime.securesms.util.FullscreenHelper
+import org.thoughtcrime.securesms.util.RemoteConfig
+import org.thoughtcrime.securesms.util.TextSecurePreferences
+import org.thoughtcrime.securesms.util.ThrottledDebouncer
+import org.thoughtcrime.securesms.util.VibrateUtil
+import org.thoughtcrime.securesms.util.WindowUtil
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
+import org.thoughtcrime.securesms.util.visible
+import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
+import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
+import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.ChosenAudioDeviceIdentifier
+import org.whispersystems.signalservice.api.messages.calls.HangupMessage
+import kotlin.time.Duration.Companion.seconds
+
+/** Conversion */
+class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback, RecaptchaProofBottomSheetFragment.Callback {
+
+ companion object {
+ val TAG = Log.tag(WebRtcCallActivity::class)
+
+ private const val STANDARD_DELAY_FINISH = 1000L
+ private const val VIBRATE_DURATION = 50
+ }
+
+ private lateinit var participantUpdateWindow: CallParticipantsListUpdatePopupWindow
+ private lateinit var callStateUpdatePopupWindow: CallStateUpdatePopupWindow
+ private lateinit var callOverflowPopupWindow: CallOverflowPopupWindow
+ private lateinit var wifiToCellularPopupWindow: WifiToCellularPopupWindow
+
+ private lateinit var fullscreenHelper: FullscreenHelper
+ private lateinit var callScreen: WebRtcCallView
+ private var videoTooltip: TooltipPopup? = null
+ private var switchCameraTooltip: TooltipPopup? = null
+ private val viewModel: WebRtcCallViewModel by viewModels()
+ private val controlsAndInfoViewModel: ControlsAndInfoViewModel by viewModels()
+ private var enableVideoIfAvailable: Boolean = false
+ private var hasWarnedAboutBluetooth: Boolean = false
+ private lateinit var windowLayoutInfoConsumer: WindowLayoutInfoConsumer
+ private lateinit var windowInfoTrackerCallbackAdapter: WindowInfoTrackerCallbackAdapter
+ private lateinit var requestNewSizesThrottle: ThrottledDebouncer
+ private lateinit var pipBuilderParams: PictureInPictureParams.Builder
+ private val lifecycleDisposable = LifecycleDisposable()
+ private var lastCallLinkDisconnectDialogShowTime: Long = 0L
+ private lateinit var controlsAndInfo: ControlsAndInfoController
+ private var enterPipOnResume: Boolean = false
+ private var lastProcessedIntentTimestamp = 0L
+ private var previousEvent: WebRtcViewModel? = null
+ private var ephemeralStateDisposable = Disposable.empty()
+ private val callPermissionsDialogController = CallPermissionsDialogController()
+
+ override fun attachBaseContext(newBase: Context) {
+ delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
+ super.attachBaseContext(newBase)
+ }
+
+ @SuppressLint("MissingInflatedId")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val callIntent: CallIntent = getCallIntent()
+ Log.i(TAG, "onCreate(${callIntent.isStartedFromFullScreen})")
+
+ lifecycleDisposable.bindTo(this)
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ super.onCreate(savedInstanceState)
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setContentView(R.layout.webrtc_call_activity)
+
+ fullscreenHelper = FullscreenHelper(this)
+
+ volumeControlStream = AudioManager.STREAM_VOICE_CALL
+
+ initializeResources()
+ initializeViewModel()
+ initializePictureInPictureParams()
+
+ controlsAndInfo = ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel)
+ controlsAndInfo.addVisibilityListener(FadeCallback())
+
+ fullscreenHelper.showAndHideWithSystemUI(
+ window,
+ findViewById(R.id.call_screen_header_gradient),
+ findViewById(R.id.webrtc_call_view_toolbar_text),
+ findViewById(R.id.webrtc_call_view_toolbar_no_text)
+ )
+
+ lifecycleDisposable.add(controlsAndInfo)
+
+ if (savedInstanceState == null) {
+ logIntent(callIntent)
+
+ if (callIntent.action == CallIntent.Action.ANSWER_VIDEO) {
+ enableVideoIfAvailable = true
+ } else if (callIntent.action == CallIntent.Action.ANSWER_AUDIO || callIntent.isStartedFromFullScreen) {
+ enableVideoIfAvailable = false
+ } else {
+ enableVideoIfAvailable = callIntent.shouldEnableVideoIfAvailable
+ callIntent.shouldEnableVideoIfAvailable = false
+ }
+
+ processIntent(callIntent)
+ } else {
+ Log.d(TAG, "Activity likely rotated, not processing intent")
+ }
+
+ registerSystemPipChangeListeners()
+
+ windowLayoutInfoConsumer = WindowLayoutInfoConsumer()
+
+ windowInfoTrackerCallbackAdapter = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this))
+ windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer)
+
+ requestNewSizesThrottle = ThrottledDebouncer(1.seconds.inWholeMilliseconds)
+
+ initializePendingParticipantFragmentListener()
+
+ WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface))
+
+ if (!hasCameraPermission() && !hasAudioPermission()) {
+ askCameraAudioPermissions {
+ callScreen.setMicEnabled(viewModel.microphoneEnabled.value ?: false)
+ handleSetMuteVideo(false)
+ }
+ } else if (!hasAudioPermission()) {
+ askAudioPermissions {
+ callScreen.setMicEnabled(viewModel.microphoneEnabled.value ?: false)
+ }
+ }
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ controlsAndInfo.onStateRestored()
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ ephemeralStateDisposable = AppDependencies.signalCallManager
+ .ephemeralStates()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(viewModel::updateFromEphemeralState)
+ }
+
+ override fun onResume() {
+ Log.i(TAG, "onResume()")
+ super.onResume()
+
+ initializeScreenshotSecurity()
+
+ if (!EventBus.getDefault().isRegistered(this)) {
+ EventBus.getDefault().register(this)
+ }
+
+ val rtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel::class.java)
+ if (rtcViewModel == null) {
+ Log.w(TAG, "Activity resumed without service event, perform delay destroy")
+ ThreadUtil.runOnMainDelayed({
+ val delayedViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel::class.java)
+ if (delayedViewModel == null) {
+ Log.w(TAG, "Activity still without service event, finishing activity")
+ finish()
+ } else {
+ Log.i(TAG, "Event found after delay")
+ }
+ }, 1.seconds.inWholeMilliseconds)
+ }
+
+ if (enterPipOnResume) {
+ enterPipOnResume = false
+ enterPipModeIfPossible()
+ }
+
+ if (SignalStore.rateLimit.needsRecaptcha()) {
+ RecaptchaProofBottomSheetFragment.show(supportFragmentManager)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ val callIntent = getCallIntent()
+ Log.i(TAG, "onNewIntent(${callIntent.isStartedFromFullScreen})")
+ super.onNewIntent(intent)
+ logIntent(callIntent)
+ processIntent(callIntent)
+ }
+
+ override fun onPause() {
+ Log.i(TAG, "onPause")
+ super.onPause()
+
+ if (!isInPipMode() || isFinishing) {
+ EventBus.getDefault().unregister(this)
+ }
+
+ if (!callPermissionsDialogController.isAskingForPermission && !viewModel.isCallStarting && !isChangingConfigurations) {
+ val state = viewModel.callParticipantsStateSnapshot
+ if (state != null && (state.callState.isPreJoinOrNetworkUnavailable || state.callState.isIncomingOrHandledElsewhere)) {
+ finish()
+ }
+ }
+ }
+
+ override fun onStop() {
+ Log.i(TAG, "onStop")
+ super.onStop()
+
+ ephemeralStateDisposable.dispose()
+
+ if (!isInPipMode() || isFinishing) {
+ EventBus.getDefault().unregister(this)
+ requestNewSizesThrottle.clear()
+ }
+
+ AppDependencies.signalCallManager.setEnableVideo(false)
+
+ if (!viewModel.isCallStarting && !isChangingConfigurations) {
+ val state = viewModel.callParticipantsStateSnapshot
+ if (state != null) {
+ if (state.callState.isPreJoinOrNetworkUnavailable || state.callState.isIncomingOrHandledElsewhere) {
+ AppDependencies.signalCallManager.cancelPreJoin()
+ } else if (state.callState.inOngoingCall && isInPipMode()) {
+ AppDependencies.signalCallManager.relaunchPipOnForeground()
+ }
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy")
+ super.onDestroy()
+ windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer)
+ EventBus.getDefault().unregister(this)
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ enterPipModeIfPossible()
+ }
+
+ override fun onBackPressed() {
+ if (!enterPipModeIfPossible()) {
+ super.onBackPressed()
+ }
+ }
+
+ override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList) {
+ val state: CallParticipantsState = viewModel.callParticipantsStateSnapshot ?: return
+
+ if (state.isCallLink) {
+ CallLinkProfileKeySender.onSendAnyway(HashSet(changedRecipients))
+ }
+
+ if (state.groupCallState.isConnected) {
+ AppDependencies.signalCallManager.groupApproveSafetyChange(changedRecipients)
+ } else {
+ viewModel.startCall(state.localParticipant.isVideoEnabled)
+ }
+ }
+
+ override fun onMessageResentAfterSafetyNumberChange() = Unit
+
+ override fun onCanceled() {
+ val state: CallParticipantsState? = viewModel.callParticipantsStateSnapshot
+ if (state != null && state.groupCallState.isNotIdle) {
+ if (state.callState.isPreJoinOrNetworkUnavailable) {
+ AppDependencies.signalCallManager.cancelPreJoin()
+ finish()
+ } else {
+ handleEndCall()
+ }
+ } else {
+ handleTerminate(viewModel.recipient.get(), HangupMessage.Type.NORMAL)
+ }
+ }
+
+ override fun onReactWithAnyEmojiDialogDismissed() = Unit
+
+ override fun onReactWithAnyEmojiSelected(emoji: String) {
+ AppDependencies.signalCallManager.react(emoji)
+ callOverflowPopupWindow.dismiss()
+ }
+
+ override fun onProofCompleted() {
+ AppDependencies.signalCallManager.resendMediaKeys()
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onRecaptchaRequiredEvent(recaptchaRequiredEvent: RecaptchaRequiredEvent) {
+ RecaptchaProofBottomSheetFragment.show(supportFragmentManager)
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ fun onEventMainThread(event: WebRtcViewModel) {
+ Log.i(TAG, "Got message from service: ${event.describeDifference(previousEvent)}")
+
+ val previousCallState: WebRtcViewModel.State? = previousEvent?.state
+
+ previousEvent = event
+
+ viewModel.setRecipient(event.recipient)
+ controlsAndInfoViewModel.setRecipient(event.recipient)
+
+ when (event.state) {
+ WebRtcViewModel.State.IDLE -> Unit
+ WebRtcViewModel.State.CALL_PRE_JOIN -> handleCallPreJoin(event)
+ WebRtcViewModel.State.CALL_INCOMING -> {
+ if (previousCallState == WebRtcViewModel.State.NETWORK_FAILURE) {
+ Log.d(TAG, "Incoming call directly from network failure state. Recreating activity.")
+ recreate()
+ return
+ }
+ }
+ WebRtcViewModel.State.CALL_OUTGOING -> handleOutgoingCall(event)
+ WebRtcViewModel.State.CALL_CONNECTED -> handleCallConnected(event)
+ WebRtcViewModel.State.CALL_RINGING -> handleCallRinging()
+ WebRtcViewModel.State.CALL_BUSY -> handleCallBusy()
+ WebRtcViewModel.State.CALL_DISCONNECTED -> {
+ if (event.groupCallEndReason == GroupCall.GroupCallEndReason.HAS_MAX_DEVICES) {
+ handleGroupCallHasMaxDevices(event.recipient)
+ } else {
+ handleTerminate(event.recipient, HangupMessage.Type.NORMAL)
+ }
+ }
+ WebRtcViewModel.State.CALL_DISCONNECTED_GLARE -> handleGlare(event.recipient)
+ WebRtcViewModel.State.CALL_NEEDS_PERMISSION -> handleTerminate(event.recipient, HangupMessage.Type.NEED_PERMISSION)
+ WebRtcViewModel.State.CALL_RECONNECTING -> handleCallReconnecting()
+ WebRtcViewModel.State.NETWORK_FAILURE -> handleServerFailure()
+ WebRtcViewModel.State.RECIPIENT_UNAVAILABLE -> handleRecipientUnavailable()
+ WebRtcViewModel.State.NO_SUCH_USER -> handleNoSuchUser(event)
+ WebRtcViewModel.State.UNTRUSTED_IDENTITY -> handleUntrustedIdentity(event)
+ WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE -> handleTerminate(event.recipient, HangupMessage.Type.ACCEPTED)
+ WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE -> handleTerminate(event.recipient, HangupMessage.Type.DECLINED)
+ WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE -> handleTerminate(event.recipient, HangupMessage.Type.BUSY)
+ }
+
+ if (event.callLinkDisconnectReason != null && event.callLinkDisconnectReason.postedAt > lastCallLinkDisconnectDialogShowTime) {
+ lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis()
+
+ when (event.callLinkDisconnectReason) {
+ is CallLinkDisconnectReason.RemovedFromCall -> displayRemovedFromCallLinkDialog()
+ is CallLinkDisconnectReason.DeniedRequestToJoinCall -> displayDeniedRequestToJoinCallLinkDialog()
+ }
+ }
+
+ val enableVideo = event.localParticipant.cameraState.cameraCount > 0 && enableVideoIfAvailable
+ viewModel.updateFromWebRtcViewModel(event, enableVideo)
+
+ if (enableVideo) {
+ enableVideoIfAvailable = false
+ handleSetMuteVideo(false)
+ }
+
+ if (event.bluetoothPermissionDenied && !hasWarnedAboutBluetooth && !isFinishing) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied)
+ .setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
+ .setPositiveButton(R.string.WebRtcCallActivity__open_settings) { _, _ -> startActivity(Permissions.getApplicationSettingsIntent(this)) }
+ .setNegativeButton(R.string.WebRtcCallActivity__not_now, null)
+ .show()
+
+ hasWarnedAboutBluetooth = true
+ }
+ }
+
+ private fun getCallIntent(): CallIntent {
+ return CallIntent(intent)
+ }
+
+ private fun initializeResources() {
+ callScreen = findViewById(R.id.callScreen)
+ callScreen.setControlsListener(ControlsListener())
+
+ participantUpdateWindow = CallParticipantsListUpdatePopupWindow(callScreen)
+ callStateUpdatePopupWindow = CallStateUpdatePopupWindow(callScreen)
+ wifiToCellularPopupWindow = WifiToCellularPopupWindow(callScreen)
+ callOverflowPopupWindow = CallOverflowPopupWindow(this, callScreen) {
+ val state: CallParticipantsState? = viewModel.callParticipantsStateSnapshot
+ state?.localParticipant?.isHandRaised ?: false
+ }
+
+ lifecycle.addObserver(participantUpdateWindow)
+ }
+
+ private fun initializeViewModel() {
+ val orientation: Orientation = resolveOrientationFromContext()
+ if (orientation == Orientation.PORTRAIT_BOTTOM_EDGE) {
+ WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface2))
+ WindowUtil.clearTranslucentNavigationBar(window)
+ }
+
+ AppDependencies.signalCallManager.orientationChanged(true, orientation.degrees)
+
+ viewModel.setIsLandscapeEnabled(true)
+ viewModel.setIsInPipMode(isInPipMode())
+ viewModel.microphoneEnabled.observe(this, callScreen::setMicEnabled)
+ viewModel.webRtcControls.observe(this) { controls ->
+ callScreen.setWebRtcControls(controls)
+ controlsAndInfo.updateControls(controls)
+ }
+ viewModel.events.observe(this, this::handleViewModelEvent)
+
+ lifecycleDisposable.add(viewModel.inCallstatus.subscribe(this::handleInCallStatus))
+ lifecycleDisposable.add(viewModel.recipientFlowable.subscribe(callScreen::setRecipient))
+
+ val isStartedFromCallLink = getCallIntent().isStartedFromCallLink
+ LiveDataUtil.combineLatest(
+ viewModel.callParticipantsState.toFlowable(BackpressureStrategy.LATEST).toLiveData(),
+ viewModel.ephemeralState
+ ) { state, ephemeralState ->
+ CallParticipantsViewState(state, ephemeralState, orientation == Orientation.PORTRAIT_BOTTOM_EDGE, true, isStartedFromCallLink)
+ }.observe(this, callScreen::updateCallParticipants)
+
+ viewModel.callParticipantListUpdate.observe(this, participantUpdateWindow::addCallParticipantListUpdate)
+ viewModel.safetyNumberChangeEvent.observe(this, this::handleSafetyNumberChangeEvent)
+ viewModel.groupMembersChanged.observe(this) { updateGroupMembersForGroupCall() }
+ viewModel.groupMemberCount.observe(this, this::handleGroupMemberCountChange)
+ lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint))
+
+ callScreen.viewTreeObserver.addOnGlobalLayoutListener {
+ val state = viewModel.callParticipantsStateSnapshot
+ if (state != null) {
+ if (state.needsNewRequestSizes()) {
+ requestNewSizesThrottle.publish { AppDependencies.signalCallManager.updateRenderedResolutions() }
+ }
+ }
+ }
+
+ addOnPictureInPictureModeChangedListener { info ->
+ viewModel.setIsInPipMode(info.isInPictureInPictureMode)
+ participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode)
+ callStateUpdatePopupWindow.setEnabled(!info.isInPictureInPictureMode)
+ if (info.isInPictureInPictureMode) {
+ callScreen.maybeDismissAudioPicker()
+ }
+ viewModel.setIsLandscapeEnabled(info.isInPictureInPictureMode)
+ }
+
+ callScreen.setPendingParticipantsViewListener(PendingParticipantsViewListener())
+ lifecycleDisposable += viewModel.pendingParticipants.subscribe(callScreen::updatePendingParticipantsList)
+ }
+
+ private fun initializePictureInPictureParams() {
+ if (isSystemPipEnabledAndAvailable()) {
+ val orientation = resolveOrientationFromContext()
+ val aspectRatio = if (orientation == Orientation.PORTRAIT_BOTTOM_EDGE) {
+ Rational(9, 16)
+ } else {
+ Rational(16, 9)
+ }
+
+ pipBuilderParams = PictureInPictureParams.Builder()
+ pipBuilderParams.setAspectRatio(aspectRatio)
+
+ if (Build.VERSION.SDK_INT >= 31) {
+ viewModel.canEnterPipMode().observe(this) {
+ pipBuilderParams.setAutoEnterEnabled(it)
+ tryToSetPictureInPictureParams()
+ }
+ } else {
+ tryToSetPictureInPictureParams()
+ }
+ }
+ }
+
+ private fun logIntent(callIntent: CallIntent) {
+ Log.d(TAG, callIntent.toString())
+ }
+
+ private fun processIntent(callIntent: CallIntent) {
+ when (callIntent.action) {
+ CallIntent.Action.ANSWER_AUDIO -> handleAnswerWithAudio()
+ CallIntent.Action.ANSWER_VIDEO -> handleAnswerWithVideo()
+ CallIntent.Action.DENY -> handleDenyCall()
+ CallIntent.Action.END_CALL -> handleEndCall()
+ else -> Unit
+ }
+
+ if (System.currentTimeMillis() - lastProcessedIntentTimestamp > 1.seconds.inWholeMilliseconds) {
+ enterPipOnResume = callIntent.shouldLaunchInPip
+ }
+
+ lastProcessedIntentTimestamp = System.currentTimeMillis()
+ }
+
+ private fun registerSystemPipChangeListeners() {
+ addOnPictureInPictureModeChangedListener {
+ CallParticipantsListDialog.dismiss(supportFragmentManager)
+ CallReactionScrubber.dismissCustomEmojiBottomSheet(supportFragmentManager)
+ }
+ }
+
+ private fun initializePendingParticipantFragmentListener() {
+ supportFragmentManager.setFragmentResultListener(
+ PendingParticipantsBottomSheet.REQUEST_KEY,
+ this
+ ) { _, result ->
+ val action: PendingParticipantsBottomSheet.Action = PendingParticipantsBottomSheet.getAction(result)
+ val recipientIds = viewModel.pendingParticipantsSnapshot.getUnresolvedPendingParticipants().map { it.recipient.id }
+
+ when (action) {
+ PendingParticipantsBottomSheet.Action.NONE -> Unit
+ PendingParticipantsBottomSheet.Action.APPROVE_ALL -> {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(resources.getQuantityString(R.plurals.WebRtcCallActivity__approve_d_requests, recipientIds.size, recipientIds.size))
+ .setMessage(resources.getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size, recipientIds.size))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.WebRtcCallActivity__approve_all) { _, _ ->
+ for (id in recipientIds) {
+ AppDependencies.signalCallManager.setCallLinkJoinRequestAccepted(id)
+ }
+ }
+ .show()
+ }
+
+ PendingParticipantsBottomSheet.Action.DENY_ALL -> {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(resources.getQuantityString(R.plurals.WebRtcCallActivity__deny_d_requests, recipientIds.size, recipientIds.size))
+ .setMessage(resources.getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_not_be_added_to_the_call, recipientIds.size, recipientIds.size))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.WebRtcCallActivity__deny_all) { _, _ ->
+ for (id in recipientIds) {
+ AppDependencies.signalCallManager.setCallLinkJoinRequestRejected(id)
+ }
+ }
+ .show()
+ }
+ }
+ }
+ }
+
+ private fun hasCameraPermission(): Boolean {
+ return Permissions.hasAll(this, Manifest.permission.CAMERA)
+ }
+
+ private fun hasAudioPermission(): Boolean {
+ return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)
+ }
+
+ private fun askCameraAudioPermissions(onGranted: () -> Unit) {
+ callPermissionsDialogController.requestCameraAndAudioPermission(
+ activity = this,
+ onAllGranted = onGranted,
+ onCameraGranted = { findViewById(R.id.missing_permissions_container).visible = false },
+ onAudioDenied = this::handleDenyCall
+ )
+ }
+
+ private fun askCameraPermissions(onGranted: () -> Unit) {
+ callPermissionsDialogController.requestCameraPermission(this) {
+ onGranted()
+ findViewById(R.id.missing_permissions_container).visible = false
+ }
+ }
+
+ private fun askAudioPermissions(onGranted: () -> Unit) {
+ callPermissionsDialogController.requestAudioPermission(
+ activity = this,
+ onGranted = onGranted,
+ onDenied = this::handleDenyCall
+ )
+ }
+
+ private fun handleSetAudioHandset() {
+ AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE))
+ }
+
+ private fun handleSetAudioSpeaker() {
+ AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE))
+ }
+
+ private fun handleSetAudioBluetooth() {
+ AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH))
+ }
+
+ private fun handleSetAudioWiredHeadset() {
+ AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET))
+ }
+
+ private fun handleSetMuteAudio(enabled: Boolean) {
+ AppDependencies.signalCallManager.setMuteAudio(enabled)
+ }
+
+ private fun handleSetMuteVideo(muted: Boolean) {
+ val recipient = viewModel.recipient.get()
+
+ if (recipient != Recipient.UNKNOWN) {
+ askCameraPermissions {
+ AppDependencies.signalCallManager.setEnableVideo(!muted)
+ }
+ }
+ }
+
+ private fun handleFlipCamera() {
+ AppDependencies.signalCallManager.flipCamera()
+ }
+
+ private fun handleAnswerWithAudio() {
+ askAudioPermissions {
+ callScreen.setStatus(getString(R.string.RedPhone_answering))
+ AppDependencies.signalCallManager.acceptCall(false)
+ }
+ }
+
+ private fun handleAnswerWithVideo() {
+ val onGranted: () -> Unit = {
+ callScreen.setStatus(getString(R.string.RedPhone_answering))
+ AppDependencies.signalCallManager.acceptCall(true)
+ handleSetMuteVideo(false)
+ }
+
+ if (!hasCameraPermission() && !hasAudioPermission()) {
+ askCameraAudioPermissions(onGranted)
+ } else if (!hasAudioPermission()) {
+ askAudioPermissions(onGranted)
+ } else {
+ askCameraPermissions(onGranted)
+ }
+ }
+
+ private fun handleDenyCall() {
+ val recipient = viewModel.recipient.get()
+ if (recipient != Recipient.UNKNOWN) {
+ AppDependencies.signalCallManager.denyCall()
+
+ callScreen.setRecipient(recipient)
+ callScreen.setStatus(getString(R.string.RedPhone_ending_call))
+ delayedFinish()
+ }
+ }
+
+ private fun handleEndCall() {
+ Log.i(TAG, "Hangup pressed, handling termination now...")
+ AppDependencies.signalCallManager.localHangup()
+ }
+
+ private fun handleOutgoingCall(event: WebRtcViewModel) {
+ if (event.groupState.isNotIdle) {
+ callScreen.setStatusFromGroupCallState(event.groupState)
+ } else {
+ callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling))
+ }
+ }
+
+ private fun handleGroupCallHasMaxDevices(recipient: Recipient) {
+ MaterialAlertDialogBuilder(this)
+ .setMessage(R.string.WebRtcCallView__call_is_full)
+ .setPositiveButton(android.R.string.ok) { _, _ -> handleTerminate(recipient, HangupMessage.Type.NORMAL) }
+ .show()
+ }
+
+ private fun handleTerminate(recipient: Recipient, hangupType: HangupMessage.Type) {
+ Log.i(TAG, "handleTerminate called: $hangupType")
+
+ callScreen.setStatusFromHangupType(hangupType)
+
+ EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
+
+ if (hangupType == HangupMessage.Type.NEED_PERMISSION) {
+ startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.id))
+ }
+
+ delayedFinish()
+ }
+
+ private fun handleGlare(recipient: Recipient) {
+ Log.i(TAG, "handleGlare: ${recipient.id}")
+ callScreen.setStatus("")
+ }
+
+ private fun handleCallRinging() {
+ callScreen.setStatus(getString(R.string.RedPhone_ringing))
+ }
+
+ private fun handleCallBusy() {
+ EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
+ callScreen.setStatus(getString(R.string.RedPhone_busy))
+ delayedFinish(SignalCallManager.BUSY_TONE_LENGTH.toLong())
+ }
+
+ private fun handleCallPreJoin(event: WebRtcViewModel) {
+ if (event.groupState.isNotIdle) {
+ callScreen.setRingGroup(event.ringGroup)
+
+ if (event.ringGroup && event.areRemoteDevicesInCall()) {
+ AppDependencies.signalCallManager.setRingGroup(false)
+ }
+ }
+ }
+
+ private fun handleCallConnected(event: WebRtcViewModel) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES)
+ if (event.groupState.isNotIdleOrConnected) {
+ callScreen.setStatusFromGroupCallState(event.groupState)
+ }
+ }
+
+ private fun handleCallReconnecting() {
+ callScreen.setStatus(getString(R.string.WebRtcCallView__reconnecting))
+ VibrateUtil.vibrate(this, VIBRATE_DURATION)
+ }
+
+ private fun handleRecipientUnavailable() {
+ EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
+ callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable))
+ delayedFinish()
+ }
+
+ private fun handleServerFailure() {
+ EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
+ callScreen.setStatus(getString(R.string.RedPhone_network_failed))
+ }
+
+ private fun handleNoSuchUser(event: WebRtcViewModel) {
+ if (isFinishing) return
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.RedPhone_number_not_registered)
+ .setIcon(R.drawable.symbol_error_triangle_fill_24)
+ .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
+ .setCancelable(true)
+ .setPositiveButton(R.string.RedPhone_got_it) { _, _ -> handleTerminate(event.recipient, HangupMessage.Type.NORMAL) }
+ .setOnCancelListener { handleTerminate(event.recipient, HangupMessage.Type.NORMAL) }
+ .show()
+ }
+
+ private fun handleUntrustedIdentity(event: WebRtcViewModel) {
+ val theirKey = event.remoteParticipants[0].identityKey
+ val recipient = event.remoteParticipants[0].recipient
+
+ if (theirKey == null) {
+ Log.w(TAG, "Untrusted identity without an identity key.")
+ }
+
+ SafetyNumberBottomSheet.forCall(recipient.id).show(supportFragmentManager)
+ }
+
+ private fun handleViewModelEvent(event: CallEvent) {
+ when (event) {
+ is CallEvent.StartCall -> startCall(event.isVideoCall)
+ is CallEvent.ShowGroupCallSafetyNumberChange -> SafetyNumberBottomSheet.forGroupCall(event.identityRecords).show(supportFragmentManager)
+ is CallEvent.SwitchToSpeaker -> callScreen.switchToSpeakerView()
+ is CallEvent.ShowSwipeToSpeakerHint -> CallToastPopupWindow.show(callScreen)
+ is CallEvent.ShowVideoTooltip -> {
+ if (isInPipMode()) return
+
+ if (videoTooltip == null) {
+ videoTooltip = TooltipPopup.forTarget(callScreen.videoTooltipTarget)
+ .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
+ .setTextColor(ContextCompat.getColor(this, R.color.core_white))
+ .setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
+ .setOnDismissListener { viewModel.onDismissedVideoTooltip() }
+ .show(TooltipPopup.POSITION_ABOVE)
+ }
+ }
+
+ is CallEvent.DismissVideoTooltip -> {
+ if (isInPipMode()) return
+
+ videoTooltip?.dismiss()
+ videoTooltip = null
+ }
+
+ is CallEvent.ShowWifiToCellularPopup -> {
+ if (isInPipMode()) return
+ wifiToCellularPopupWindow.show()
+ }
+
+ is CallEvent.ShowSwitchCameraTooltip -> {
+ if (isInPipMode()) return
+
+ if (switchCameraTooltip == null) {
+ switchCameraTooltip = TooltipPopup.forTarget(callScreen.switchCameraTooltipTarget)
+ .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
+ .setTextColor(ContextCompat.getColor(this, R.color.core_white))
+ .setText(R.string.WebRtcCallActivity__flip_camera_tooltip)
+ .setOnDismissListener { viewModel.onDismissedSwitchCameraTooltip() }
+ .show(TooltipPopup.POSITION_ABOVE)
+ }
+ }
+
+ is CallEvent.DismissSwitchCameraTooltip -> {
+ if (isInPipMode()) return
+
+ switchCameraTooltip?.dismiss()
+ switchCameraTooltip = null
+ }
+ }
+ }
+
+ private fun handleInCallStatus(inCallStatus: InCallStatus) {
+ when (inCallStatus) {
+ is InCallStatus.ElapsedTime -> {
+ val formatter: EllapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(inCallStatus.elapsedTime) ?: return
+ callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, formatter.toString()))
+ }
+ is InCallStatus.PendingCallLinkUsers -> {
+ val waiting = inCallStatus.pendingUserCount
+
+ callScreen.setStatus(
+ resources.getQuantityString(
+ R.plurals.WebRtcCallActivity__d_people_waiting,
+ waiting,
+ waiting
+ )
+ )
+ }
+ is InCallStatus.JoinedCallLinkUsers -> {
+ val joined = inCallStatus.joinedUserCount
+
+ callScreen.setStatus(
+ resources.getQuantityString(
+ R.plurals.WebRtcCallActivity__d_people,
+ joined,
+ joined
+ )
+ )
+ }
+ }
+ }
+
+ private fun handleSafetyNumberChangeEvent(safetyNumberChangeEvent: SafetyNumberChangeEvent) {
+ if (safetyNumberChangeEvent.recipientIds.isNotEmpty()) {
+ if (safetyNumberChangeEvent.isInPipMode) {
+ GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.recipient.get())
+ } else {
+ GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.recipient.get())
+ SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.recipientIds).show(supportFragmentManager)
+ }
+ }
+ }
+
+ private fun updateGroupMembersForGroupCall() {
+ AppDependencies.signalCallManager.requestUpdateGroupMembers()
+ }
+
+ private fun handleGroupMemberCountChange(count: Int) {
+ val canRing = count <= RemoteConfig.maxGroupCallRingSize
+ callScreen.enableRingGroup(canRing)
+ AppDependencies.signalCallManager.setRingGroup(canRing)
+ }
+
+ private fun updateSpeakerHint(enabled: Boolean) {
+ if (enabled) {
+ callScreen.showSpeakerViewHint()
+ } else {
+ callScreen.hideSpeakerViewHint()
+ }
+ }
+
+ private fun initializeScreenshotSecurity() {
+ if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+ }
+
+ private fun enterPipModeIfPossible(): Boolean {
+ if (isSystemPipEnabledAndAvailable()) {
+ if (viewModel.canEnterPipMode().value == true) {
+ try {
+ enterPictureInPictureMode(pipBuilderParams.build())
+ } catch (e: Exception) {
+ Log.w(TAG, "Device lied to us about supporting PiP", e)
+ return false
+ }
+
+ return true
+ }
+
+ if (Build.VERSION.SDK_INT >= 31) {
+ pipBuilderParams.setAutoEnterEnabled(false)
+ }
+ }
+
+ return false
+ }
+
+ private fun isInPipMode(): Boolean {
+ return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode
+ }
+
+ private fun isSystemPipEnabledAndAvailable(): Boolean {
+ return Build.VERSION.SDK_INT >= 26 && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+ }
+
+ private fun resolveOrientationFromContext(): Orientation {
+ val displayOrientation = resources.configuration.orientation
+ val displayRotation = ContextCompat.getDisplayOrDefault(this).rotation
+
+ return if (displayOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ Orientation.PORTRAIT_BOTTOM_EDGE
+ } else if (displayRotation == Surface.ROTATION_270) {
+ Orientation.LANDSCAPE_RIGHT_EDGE
+ } else {
+ Orientation.LANDSCAPE_LEFT_EDGE
+ }
+ }
+
+ private fun tryToSetPictureInPictureParams() {
+ if (Build.VERSION.SDK_INT >= 26) {
+ try {
+ setPictureInPictureParams(pipBuilderParams.build())
+ } catch (e: Exception) {
+ Log.w(TAG, "System lied about having PiP available.", e)
+ }
+ }
+ }
+
+ private fun startCall(isVideoCall: Boolean) {
+ enableVideoIfAvailable = isVideoCall
+
+ if (isVideoCall) {
+ AppDependencies.signalCallManager.startOutgoingVideoCall(viewModel.recipient.get())
+ } else {
+ AppDependencies.signalCallManager.startOutgoingAudioCall(viewModel.recipient.get())
+ }
+
+ MessageSender.onMessageSent()
+ }
+
+ private fun delayedFinish(delayMillis: Long = STANDARD_DELAY_FINISH) {
+ callScreen.postDelayed(this::finish, delayMillis)
+ }
+
+ private fun displayRemovedFromCallLinkDialog() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.WebRtcCallActivity__removed_from_call)
+ .setMessage(R.string.WebRtcCallActivity__someone_has_removed_you_from_the_call)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
+
+ private fun displayDeniedRequestToJoinCallLinkDialog() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.WebRtcCallActivity__join_request_denied)
+ .setMessage(R.string.WebRtcCallActivity__your_request_to_join_this_call_has_been_denied)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
+
+ private fun maybeDisplaySpeakerphonePopup(nextOutput: WebRtcAudioOutput) {
+ val currentOutput = viewModel.currentAudioOutput
+ if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
+ callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_OFF)
+ } else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
+ callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_ON)
+ }
+ }
+
+ private inner class WindowLayoutInfoConsumer : Consumer {
+ override fun accept(value: WindowLayoutInfo) {
+ Log.d(TAG, "On WindowLayoutInfo accepted: $value")
+
+ val feature: FoldingFeature? = value.displayFeatures.filterIsInstance().firstOrNull()
+ if (feature != null) {
+ val bounds = feature.bounds
+ if (feature.isSeparating) {
+ Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top diplay mode")
+ viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top))
+ } else {
+ Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode")
+ viewModel.setFoldableState(WebRtcControls.FoldableState.flat())
+ }
+ }
+ }
+ }
+
+ private inner class FadeCallback : ControlsAndInfoController.BottomSheetVisibilityListener {
+ override fun onShown() {
+ fullscreenHelper.showSystemUI()
+ }
+
+ override fun onHidden() {
+ val controlState = viewModel.webRtcControls.value
+ if (controlState == null || !controlState.displayErrorControls()) {
+ fullscreenHelper.hideSystemUI()
+ videoTooltip?.dismiss()
+ }
+ }
+ }
+
+ private inner class ControlsListener : WebRtcCallView.ControlsListener {
+ override fun onStartCall(isVideoCall: Boolean) {
+ viewModel.startCall(isVideoCall)
+ }
+
+ override fun onCancelStartCall() {
+ finish()
+ }
+
+ override fun onAudioOutputChanged(audioOutput: WebRtcAudioOutput) {
+ maybeDisplaySpeakerphonePopup(audioOutput)
+ when (audioOutput) {
+ WebRtcAudioOutput.HANDSET -> handleSetAudioHandset()
+ WebRtcAudioOutput.BLUETOOTH_HEADSET -> handleSetAudioBluetooth()
+ WebRtcAudioOutput.SPEAKER -> handleSetAudioSpeaker()
+ WebRtcAudioOutput.WIRED_HEADSET -> handleSetAudioWiredHeadset()
+ }
+ }
+
+ @RequiresApi(31)
+ override fun onAudioOutputChanged31(audioOutput: WebRtcAudioDevice) {
+ maybeDisplaySpeakerphonePopup(audioOutput.webRtcAudioOutput)
+ AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(audioOutput.deviceId!!))
+ }
+
+ override fun onVideoChanged(isVideoEnabled: Boolean) {
+ handleSetMuteVideo(!isVideoEnabled)
+ }
+
+ override fun onMicChanged(isMicEnabled: Boolean) {
+ askAudioPermissions {
+ callStateUpdatePopupWindow.onCallStateUpdate(if (isMicEnabled) CallControlsChange.MIC_ON else CallControlsChange.MIC_OFF)
+ handleSetMuteAudio(!isMicEnabled)
+ }
+ }
+
+ override fun onOverflowClicked() {
+ controlsAndInfo.toggleOverflowPopup()
+ }
+
+ override fun onCameraDirectionChanged() {
+ handleFlipCamera()
+ }
+
+ override fun onEndCallPressed() {
+ handleEndCall()
+ }
+
+ override fun onDenyCallPressed() {
+ handleDenyCall()
+ }
+
+ override fun onAcceptCallWithVoiceOnlyPressed() {
+ handleAnswerWithAudio()
+ }
+
+ override fun onAcceptCallPressed() {
+ if (viewModel.isAnswerWithVideoAvailable) {
+ handleAnswerWithVideo()
+ } else {
+ handleAnswerWithAudio()
+ }
+ }
+
+ override fun onPageChanged(page: CallParticipantsState.SelectedPage) {
+ viewModel.setIsViewingFocusedParticipant(page)
+ }
+
+ override fun onLocalPictureInPictureClicked() {
+ viewModel.onLocalPictureInPictureClicked()
+ controlsAndInfo.restartHideControlsTimer()
+ }
+
+ override fun onRingGroupChanged(ringGroup: Boolean, ringingAllowed: Boolean) {
+ if (ringingAllowed) {
+ AppDependencies.signalCallManager.setRingGroup(ringGroup)
+ callStateUpdatePopupWindow.onCallStateUpdate(if (ringGroup) CallControlsChange.RINGING_ON else CallControlsChange.RINGING_OFF)
+ } else {
+ AppDependencies.signalCallManager.setRingGroup(false)
+ callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.RINGING_DISABLED)
+ }
+ }
+
+ override fun onCallInfoClicked() {
+ controlsAndInfo.showCallInfo()
+ }
+
+ override fun onNavigateUpClicked() {
+ onBackPressed()
+ }
+
+ override fun toggleControls() {
+ val controlState = viewModel.webRtcControls.value
+ if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
+ controlsAndInfo.toggleControls()
+ }
+ }
+
+ override fun onAudioPermissionsRequested(onGranted: Runnable?) {
+ askAudioPermissions { onGranted?.run() }
+ }
+ }
+
+ private inner class PendingParticipantsViewListener : PendingParticipantsView.Listener {
+ override fun onLaunchRecipientSheet(pendingRecipient: Recipient) {
+ CallLinkIncomingRequestSheet.show(supportFragmentManager, pendingRecipient.id)
+ }
+
+ override fun onAllowPendingRecipient(pendingRecipient: Recipient) {
+ AppDependencies.signalCallManager.setCallLinkJoinRequestAccepted(pendingRecipient.id)
+ }
+
+ override fun onRejectPendingRecipient(pendingRecipient: Recipient) {
+ AppDependencies.signalCallManager.setCallLinkJoinRequestRejected(pendingRecipient.id)
+ }
+
+ override fun onLaunchPendingRequestsSheet() {
+ PendingParticipantsBottomSheet().show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
index 26fca87010..2e5b7ca039 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
@@ -34,7 +34,6 @@ import org.signal.ringrtc.NetworkRoute;
import org.signal.ringrtc.PeekInfo;
import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
-import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.CallLinkTable;
@@ -538,7 +537,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
return false;
}
- context.startActivity(new Intent(context, WebRtcCallActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ context.startActivity(new Intent(context, CallIntent.getActivityClass()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
return true;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
index e98c6eb48d..f6b005dcc4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
@@ -29,9 +29,7 @@ import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.calls.links.CallLinks;
-import org.thoughtcrime.securesms.components.webrtc.v2.CallActivity;
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java
index 0547fabf58..821d9d5146 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java
@@ -7,11 +7,11 @@ import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
+import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.WebRtcCallActivity;
+import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.signal.core.util.concurrent.SimpleTask;
public class VoiceCallShare extends Activity {
@@ -40,7 +40,7 @@ public class VoiceCallShare extends Activity {
AppDependencies.getSignalCallManager().startOutgoingAudioCall(recipient);
}
- Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
+ Intent activityIntent = new Intent(this, CallIntent.getActivityClass());
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
}