From e7720640d1c33a81f8af13c18d681e86822da3fc Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 18 Jul 2024 10:35:18 -0300 Subject: [PATCH] Implement landscape calling. --- app/src/main/AndroidManifest.xml | 2 +- .../securesms/WebRtcCallActivity.java | 68 +++-- .../components/MaxHeightFrameLayout.java | 43 --- .../components/MaxSizeFrameLayout.java | 70 +++++ .../sensors/DeviceOrientationMonitor.java | 117 -------- .../components/webrtc/BroadcastVideoSink.java | 2 +- .../webrtc/CallParticipantsState.kt | 2 +- .../webrtc/CallStateUpdatePopupWindow.kt | 2 +- .../PictureInPictureExpansionHelper.java | 7 +- .../components/webrtc/WebRtcCallView.java | 45 +--- .../webrtc/WebRtcCallViewModel.java | 65 +---- .../components/webrtc/WebRtcControls.java | 12 +- .../controls/ControlsAndInfoController.kt | 252 ++++++++++-------- .../securesms/util/WindowUtil.java | 4 + .../res/layout-land/webrtc_call_controls.xml | 246 +++++++++++++++++ ...rtc_call_participant_overflow_recycler.xml | 26 ++ .../webrtc_call_participant_pager.xml | 26 ++ ...webrtc_call_view_incoming_call_buttons.xml | 100 +++++++ .../main/res/layout/webrtc_call_controls.xml | 33 +-- ...rtc_call_participant_overflow_recycler.xml | 26 ++ .../layout/webrtc_call_participant_pager.xml | 26 ++ .../webrtc_call_participant_recycler_item.xml | 5 +- app/src/main/res/layout/webrtc_call_view.xml | 126 +-------- .../layout/webrtc_call_view_header_large.xml | 34 +-- ...webrtc_call_view_incoming_call_buttons.xml | 96 +++++++ app/src/main/res/values-land/dimens.xml | 9 + app/src/main/res/values/dimens.xml | 7 + app/src/main/res/values/styles.xml | 8 +- app/src/main/res/values/themes.xml | 2 +- 29 files changed, 890 insertions(+), 571 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/MaxHeightFrameLayout.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/MaxSizeFrameLayout.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/sensors/DeviceOrientationMonitor.java create mode 100644 app/src/main/res/layout-land/webrtc_call_controls.xml create mode 100644 app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml create mode 100644 app/src/main/res/layout-land/webrtc_call_participant_pager.xml create mode 100644 app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml create mode 100644 app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml create mode 100644 app/src/main/res/layout/webrtc_call_participant_pager.xml create mode 100644 app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b53834d85..19d2ecd9ef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -121,7 +121,7 @@ android:excludeFromRecents="true" android:supportsPictureInPicture="true" android:windowSoftInputMode="stateAlwaysHidden" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:configChanges="screenSize|smallestScreenSize|screenLayout" android:taskAffinity=".calling" android:resizeableActivity="true" android:launchMode="singleTask" diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 7fe7a85eed..f545f62172 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -22,13 +22,16 @@ import android.annotation.SuppressLint; import android.app.PictureInPictureParams; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; 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.MotionEvent; +import android.view.Surface; import android.view.View; import android.view.Window; import android.view.WindowManager; @@ -39,7 +42,10 @@ 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; @@ -58,7 +64,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.thoughtcrime.securesms.components.TooltipPopup; -import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; +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; @@ -94,8 +100,8 @@ 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.RemoteConfig; 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; @@ -147,7 +153,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private CallStateUpdatePopupWindow callStateUpdatePopupWindow; private CallOverflowPopupWindow callOverflowPopupWindow; private WifiToCellularPopupWindow wifiToCellularPopupWindow; - private DeviceOrientationMonitor deviceOrientationMonitor; private FullscreenHelper fullscreenHelper; private WebRtcCallView callScreen; @@ -176,7 +181,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan super.attachBaseContext(newBase); } - @SuppressLint({ "SourceLockedOrientationActivity", "MissingInflatedId" }) + @SuppressLint({ "MissingInflatedId" }) @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate(" + getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")"); @@ -188,11 +193,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); - boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480; - if (!isLandscapeEnabled) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.webrtc_call_activity); @@ -201,7 +201,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); initializeResources(); - initializeViewModel(isLandscapeEnabled); + initializeViewModel(); initializePictureInPictureParams(); controlsAndInfo = new ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel); @@ -254,6 +254,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan }); } + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + controlsAndInfo.onStateRestored(); + } + @Override protected void onStart() { super.onStart(); @@ -307,7 +313,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan Log.i(TAG, "onPause"); super.onPause(); - if (!isAskingForPermission && !viewModel.isCallStarting()) { + if (!isAskingForPermission && !viewModel.isCallStarting() && !isChangingConfigurations()) { CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { finish(); @@ -329,7 +335,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan AppDependencies.getSignalCallManager().setEnableVideo(false); - if (!viewModel.isCallStarting()) { + if (!viewModel.isCallStarting() && !isChangingConfigurations()) { CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null) { if (state.getCallState().isPreJoinOrNetworkUnavailable()) { @@ -343,6 +349,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan @Override protected void onDestroy() { + Log.d(TAG, "onDestroy"); super.onDestroy(); windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer); EventBus.getDefault().unregister(this); @@ -486,14 +493,30 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan }); } - private void initializeViewModel(boolean isLandscapeEnabled) { - deviceOrientationMonitor = new DeviceOrientationMonitor(this); - getLifecycle().addObserver(deviceOrientationMonitor); + private @NonNull Orientation resolveOrientationFromContext() { + int displayOrientation = getResources().getConfiguration().orientation; + int displayRotation = ContextCompat.getDisplayOrDefault(this).getRotation(); - WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor); + 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; + } + } - viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class); - viewModel.setIsLandscapeEnabled(isLandscapeEnabled); + 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 -> { @@ -506,7 +529,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false); LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)), - viewModel.getOrientationAndLandscapeEnabled(), + orientationAndLandscapeEnabled, viewModel.getEphemeralState(), (s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink)) .observe(this, p -> callScreen.updateCallParticipants(p)); @@ -525,8 +548,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } }); - viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees())); - viewModel.getControlsRotation().observe(this, callScreen::rotateControls); + orientationAndLandscapeEnabled.observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees())); addOnPictureInPictureModeChangedListener(info -> { viewModel.setIsInPipMode(info.isInPictureInPictureMode()); @@ -1262,8 +1284,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString()); Optional feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst(); - viewModel.setIsLandscapeEnabled(feature.isPresent()); - setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); if (feature.isPresent()) { FoldingFeature foldingFeature = (FoldingFeature) feature.get(); Rect bounds = foldingFeature.getBounds(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MaxHeightFrameLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/MaxHeightFrameLayout.java deleted file mode 100644 index b9135e69ae..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/MaxHeightFrameLayout.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.R; - -public class MaxHeightFrameLayout extends FrameLayout { - - private final int maxHeight; - - public MaxHeightFrameLayout(@NonNull Context context) { - this(context, null); - } - - public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - if (attrs != null) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightFrameLayout); - - maxHeight = a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_mhfl_maxHeight, 0); - - a.recycle(); - } else { - maxHeight = 0; - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, Math.min(bottom, top + maxHeight)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MaxSizeFrameLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/MaxSizeFrameLayout.java new file mode 100644 index 0000000000..c819ff335f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/MaxSizeFrameLayout.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.R; + +/** + * FrameLayout which allows user to specify maximum dimensions of itself and therefore its children. + */ +public class MaxSizeFrameLayout extends FrameLayout { + + private final int maxHeight; + private final int maxWidth; + + public MaxSizeFrameLayout(@NonNull Context context) { + this(context, null); + } + + public MaxSizeFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public MaxSizeFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxSizeFrameLayout); + + maxHeight = a.getDimensionPixelSize(R.styleable.MaxSizeFrameLayout_msfl_maxHeight, 0); + maxWidth = a.getDimensionPixelSize(R.styleable.MaxSizeFrameLayout_msfl_maxWidth, 0); + a.recycle(); + } else { + maxHeight = 0; + maxWidth = 0; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int newWidthSpec = updateMeasureSpecWithMaxSize(widthMeasureSpec, maxWidth); + int newHeightSpec = updateMeasureSpecWithMaxSize(heightMeasureSpec, maxHeight); + + super.onMeasure(newWidthSpec, newHeightSpec); + } + + private int updateMeasureSpecWithMaxSize(int measureSpec, int maxSize) { + if (maxSize <= 0) { + return measureSpec; + } + + int mode = MeasureSpec.getMode(measureSpec); + int size = MeasureSpec.getSize(measureSpec); + + if (mode == MeasureSpec.UNSPECIFIED) { + return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST); + } else if (mode == MeasureSpec.EXACTLY && size > maxSize) { + return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); + } else if (mode == MeasureSpec.AT_MOST && size > maxSize) { + return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST); + } else { + return measureSpec; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/sensors/DeviceOrientationMonitor.java b/app/src/main/java/org/thoughtcrime/securesms/components/sensors/DeviceOrientationMonitor.java deleted file mode 100644 index 9774ea50ae..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/sensors/DeviceOrientationMonitor.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.thoughtcrime.securesms.components.sensors; - -import android.content.ContentResolver; -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.provider.Settings; - -import androidx.annotation.NonNull; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; - -import org.thoughtcrime.securesms.util.ServiceUtil; - -public final class DeviceOrientationMonitor implements DefaultLifecycleObserver { - - private static final float MAGNITUDE_MAXIMUM = 1.5f; - private static final float MAGNITUDE_MINIMUM = 0.75f; - private static final float LANDSCAPE_PITCH_MINIMUM = -0.5f; - private static final float LANDSCAPE_PITCH_MAXIMUM = 0.5f; - - private final SensorManager sensorManager; - private final ContentResolver contentResolver; - private final EventListener eventListener = new EventListener(); - - private final float[] accelerometerReading = new float[3]; - private final float[] magnetometerReading = new float[3]; - - private final float[] rotationMatrix = new float[9]; - private final float[] orientationAngles = new float[3]; - - private final MutableLiveData orientation = new MutableLiveData<>(Orientation.PORTRAIT_BOTTOM_EDGE); - - public DeviceOrientationMonitor(@NonNull Context context) { - this.sensorManager = ServiceUtil.getSensorManager(context); - this.contentResolver = context.getContentResolver(); - } - - @Override - public void onStart(@NonNull LifecycleOwner owner) { - Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - if (accelerometer != null) { - sensorManager.registerListener(eventListener, - accelerometer, - SensorManager.SENSOR_DELAY_NORMAL, - SensorManager.SENSOR_DELAY_UI); - } - Sensor magneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - if (magneticField != null) { - sensorManager.registerListener(eventListener, - magneticField, - SensorManager.SENSOR_DELAY_NORMAL, - SensorManager.SENSOR_DELAY_UI); - } - } - - @Override - public void onStop(@NonNull LifecycleOwner owner) { - sensorManager.unregisterListener(eventListener); - } - - public LiveData getOrientation() { - return Transformations.distinctUntilChanged(orientation); - } - - private void updateOrientationAngles() { - int rotationLocked = Settings.System.getInt(contentResolver, Settings.System.ACCELEROMETER_ROTATION, -1); - if (rotationLocked == 0) { - orientation.setValue(Orientation.PORTRAIT_BOTTOM_EDGE); - return; - } - - boolean success = SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading); - if (!success) { - SensorUtil.getRotationMatrixWithoutMagneticSensorData(rotationMatrix, accelerometerReading); - } - SensorManager.getOrientation(rotationMatrix, orientationAngles); - - float pitch = orientationAngles[1]; - float roll = orientationAngles[2]; - float mag = (float) Math.sqrt(Math.pow(pitch, 2) + Math.pow(roll, 2)); - - if (mag > MAGNITUDE_MAXIMUM || mag < MAGNITUDE_MINIMUM) { - return; - } - - if (pitch > LANDSCAPE_PITCH_MINIMUM && pitch < LANDSCAPE_PITCH_MAXIMUM) { - orientation.setValue(roll > 0 ? Orientation.LANDSCAPE_RIGHT_EDGE : Orientation.LANDSCAPE_LEFT_EDGE); - } else { - orientation.setValue(Orientation.PORTRAIT_BOTTOM_EDGE); - } - } - - private final class EventListener implements SensorEventListener { - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { - System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.length); - } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { - System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.length); - } - - updateOrientationAngles(); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java index 452d3b3467..903d50e635 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java @@ -47,7 +47,7 @@ public class BroadcastVideoSink implements VideoSink { this.deviceOrientationDegrees = deviceOrientationDegrees; this.rotateToRightSide = false; this.forceRotate = forceRotate; - this.rotateWithDevice = rotateWithDevice; + this.rotateWithDevice = false; } public @NonNull EglBaseWrapper getLockableEglBase() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt index 513ded5015..9c3554b16b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt @@ -194,7 +194,7 @@ data class CallParticipantsState( } companion object { - private const val SMALL_GROUP_MAX = 6 + const val SMALL_GROUP_MAX = 6 @JvmField val MAX_OUTGOING_GROUP_RING_DURATION = TimeUnit.MINUTES.toMillis(1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt index ffc8ab3907..84ff088bb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt @@ -78,7 +78,7 @@ class CallStateUpdatePopupWindow(private val parent: ViewGroup) : PopupWindow( } private fun show() { - if (!enabled) { + if (!enabled || parent.windowToken == null) { return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java index 593f5d1725..532165e7af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java @@ -31,10 +31,10 @@ final class PictureInPictureExpansionHelper { private final View selfPip; private final ViewGroup parent; - private final Point expandedDimensions; private State state = State.IS_SHRUNKEN; private Point defaultDimensions; + private Point expandedDimensions; public PictureInPictureExpansionHelper(@NonNull View selfPip) { this.selfPip = selfPip; @@ -62,6 +62,11 @@ final class PictureInPictureExpansionHelper { defaultDimensions = dimensions; + int x = (dimensions.x > dimensions.y) ? EXPANDED_PIP_HEIGHT_DP : EXPANDED_PIP_WIDTH_DP; + int y = (dimensions.x > dimensions.y) ? EXPANDED_PIP_WIDTH_DP : EXPANDED_PIP_HEIGHT_DP; + + expandedDimensions = new Point(ViewUtil.dpToPx(x), ViewUtil.dpToPx(y)); + if (isExpandedOrExpanding()) { return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index 9a13e20ccd..87d3eb18f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc; import android.Manifest; import android.content.Context; +import android.content.res.Configuration; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Point; @@ -79,9 +80,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private static final String TAG = Log.tag(WebRtcCallView.class); - private static final long TRANSITION_DURATION_MILLIS = 250; - private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8; - private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16; + private static final long TRANSITION_DURATION_MILLIS = 250; private WebRtcAudioOutputToggleButton audioToggle; private AccessibleToggleButton videoToggle; @@ -142,7 +141,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private final Set topViews = new HashSet<>(); private final Set visibleViewSet = new HashSet<>(); private final Set allTimeVisibleViews = new HashSet<>(); - private final Set rotatableControls = new HashSet<>(); private final ThrottledDebouncer throttledDebouncer = new ThrottledDebouncer(TRANSITION_DURATION_MILLIS); private WebRtcControls controls = WebRtcControls.NONE; @@ -360,18 +358,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { return false; }); - rotatableControls.add(overflow); - rotatableControls.add(hangup); - rotatableControls.add(answer); - rotatableControls.add(answerWithoutVideo); - rotatableControls.add(audioToggle); - rotatableControls.add(micToggle); - rotatableControls.add(videoToggle); - rotatableControls.add(cameraDirectionToggle); - rotatableControls.add(decline); - rotatableControls.add(smallLocalAudioIndicator); - rotatableControls.add(ringToggle); - missingPermissionContainer.setVisibility(hasCameraPermission() ? View.GONE : View.VISIBLE); allowAccessButton.setOnClickListener(v -> { @@ -379,10 +365,17 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { }); ConstraintLayout aboveControls = findViewById(R.id.call_controls_floating_parent); - SlideUpWithCallControlsBehavior behavior = (SlideUpWithCallControlsBehavior) ((CoordinatorLayout.LayoutParams) aboveControls.getLayoutParams()).getBehavior(); - Objects.requireNonNull(behavior).setOnTopOfControlsChangedListener(topOfControls -> { - pictureInPictureGestureHelper.setBottomVerticalBoundary(topOfControls); - }); + + if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + aboveControls.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + pictureInPictureGestureHelper.setBottomVerticalBoundary(bottom + ViewUtil.getStatusBarHeight(v)); + }); + } else { + SlideUpWithCallControlsBehavior behavior = (SlideUpWithCallControlsBehavior) ((CoordinatorLayout.LayoutParams) aboveControls.getLayoutParams()).getBehavior(); + Objects.requireNonNull(behavior).setOnTopOfControlsChangedListener(topOfControls -> { + pictureInPictureGestureHelper.setBottomVerticalBoundary(topOfControls); + }); + } } @Override @@ -408,12 +401,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { } } - public void rotateControls(int degrees) { - for (View view : rotatableControls) { - view.animate().rotation(degrees); - } - } - public void setControlsListener(@Nullable ControlsListener controlsListener) { this.controlsListener = controlsListener; } @@ -873,12 +860,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { constraintSet.setForceId(false); constraintSet.clone(this); - constraintSet.connect(R.id.call_screen_participants_parent, - ConstraintSet.BOTTOM, - layoutPositions.participantBottomViewId, - layoutPositions.participantBottomViewEndSide, - ViewUtil.dpToPx(layoutPositions.participantBottomMargin)); - constraintSet.connect(R.id.call_screen_reactions_feed, ConstraintSet.BOTTOM, layoutPositions.reactionBottomViewId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index 453a5d02ff..1f6a7038c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.webrtc; import android.os.Handler; import android.os.Looper; -import android.util.Pair; import androidx.annotation.MainThread; import androidx.annotation.NonNull; @@ -12,12 +11,10 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; import com.annimon.stream.Stream; import org.signal.core.util.ThreadUtil; -import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; import org.thoughtcrime.securesms.components.sensors.Orientation; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; @@ -41,7 +38,6 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Set; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -68,9 +64,7 @@ public class WebRtcCallViewModel extends ViewModel { private final LiveData> groupMembersChanged = LiveDataUtil.skip(groupMembers, 1); private final LiveData groupMemberCount = Transformations.map(groupMembers, List::size); private final Observable shouldShowSpeakerHint = participantsState.map(this::shouldShowSpeakerHint); - private final LiveData orientation; private final MutableLiveData isLandscapeEnabled = new MutableLiveData<>(); - private final LiveData controlsRotation; private final Observer> groupMemberStateUpdater = m -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), m)); private final MutableLiveData ephemeralState = new MutableLiveData<>(); @@ -95,27 +89,10 @@ public class WebRtcCallViewModel extends ViewModel { private final WebRtcCallRepository repository = new WebRtcCallRepository(AppDependencies.getApplication()); - private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) { - orientation = deviceOrientationMonitor.getOrientation(); - controlsRotation = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(isLandscapeEnabled), - Transformations.distinctUntilChanged(orientation), - this::resolveRotation); - + public WebRtcCallViewModel() { groupMembers.observeForever(groupMemberStateUpdater); } - public LiveData getControlsRotation() { - return controlsRotation; - } - - public LiveData getOrientation() { - return Transformations.distinctUntilChanged(orientation); - } - - public LiveData> getOrientationAndLandscapeEnabled() { - return LiveDataUtil.combineLatest(orientation, isLandscapeEnabled, Pair::new); - } - public LiveData getMicrophoneEnabled() { return Transformations.distinctUntilChanged(microphoneEnabled); } @@ -309,7 +286,8 @@ public class WebRtcCallViewModel extends ViewModel { webRtcViewModel.getAvailableDevices(), webRtcViewModel.getRemoteDevicesCount().orElse(0), webRtcViewModel.getParticipantLimit(), - webRtcViewModel.getRecipient().isCallLink()); + webRtcViewModel.getRecipient().isCallLink(), + webRtcViewModel.getRemoteParticipants().size() > CallParticipantsState.SMALL_GROUP_MAX); pendingParticipants.onNext(webRtcViewModel.getPendingParticipants()); @@ -364,23 +342,6 @@ public class WebRtcCallViewModel extends ViewModel { ephemeralState.setValue(state); } - private int resolveRotation(boolean isLandscapeEnabled, @NonNull Orientation orientation) { - if (isLandscapeEnabled) { - return 0; - } - - switch (orientation) { - case LANDSCAPE_LEFT_EDGE: - return 90; - case LANDSCAPE_RIGHT_EDGE: - return -90; - case PORTRAIT_BOTTOM_EDGE: - return 0; - default: - throw new AssertionError(); - } - } - private boolean containsPlaceholders(@NonNull List callParticipants) { return Stream.of(callParticipants).anyMatch(p -> p.getCallParticipantId().getDemuxId() == CallParticipantId.DEFAULT_ID); } @@ -396,7 +357,8 @@ public class WebRtcCallViewModel extends ViewModel { @NonNull Set availableDevices, long remoteDevicesCount, @Nullable Long participantLimit, - boolean isCallLink) + boolean isCallLink, + boolean hasParticipantOverflow) { final WebRtcControls.CallState callState; @@ -468,7 +430,8 @@ public class WebRtcCallViewModel extends ViewModel { WebRtcControls.FoldableState.flat(), activeDevice, availableDevices, - isCallLink)); + isCallLink, + hasParticipantOverflow)); } private @NonNull WebRtcControls updateControlsFoldableState(@NonNull WebRtcControls.FoldableState foldableState, @NonNull WebRtcControls controls) { @@ -608,18 +571,4 @@ public class WebRtcCallViewModel extends ViewModel { return recipientIds; } } - - public static class Factory implements ViewModelProvider.Factory { - - private final DeviceOrientationMonitor deviceOrientationMonitor; - - public Factory(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) { - this.deviceOrientationMonitor = deviceOrientationMonitor; - } - - @Override - public @NonNull T create(@NonNull Class modelClass) { - return Objects.requireNonNull(modelClass.cast(new WebRtcCallViewModel(deviceOrientationMonitor))); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index bf94fe21bf..617ab320c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -28,6 +28,7 @@ public final class WebRtcControls { FoldableState.flat(), SignalAudioManager.AudioDevice.NONE, emptySet(), + false, false); private final boolean isRemoteVideoEnabled; @@ -42,6 +43,7 @@ public final class WebRtcControls { private final SignalAudioManager.AudioDevice activeDevice; private final Set availableDevices; private final boolean isCallLink; + private final boolean hasParticipantOverflow; private WebRtcControls() { this(false, @@ -55,6 +57,7 @@ public final class WebRtcControls { FoldableState.flat(), SignalAudioManager.AudioDevice.NONE, emptySet(), + false, false); } @@ -69,7 +72,8 @@ public final class WebRtcControls { @NonNull FoldableState foldableState, @NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set availableDevices, - boolean isCallLink) + boolean isCallLink, + boolean hasParticipantOverflow) { this.isLocalVideoEnabled = isLocalVideoEnabled; this.isRemoteVideoEnabled = isRemoteVideoEnabled; @@ -83,6 +87,7 @@ public final class WebRtcControls { this.activeDevice = activeDevice; this.availableDevices = availableDevices; this.isCallLink = isCallLink; + this.hasParticipantOverflow = hasParticipantOverflow; } public @NonNull WebRtcControls withFoldableState(FoldableState foldableState) { @@ -97,7 +102,8 @@ public final class WebRtcControls { foldableState, activeDevice, availableDevices, - isCallLink); + isCallLink, + hasParticipantOverflow); } /** @@ -178,7 +184,7 @@ public final class WebRtcControls { } public boolean displayRemoteVideoRecycler() { - return isOngoing(); + return isOngoing() && hasParticipantOverflow; } public boolean displayAnswerWithoutVideo() { 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 6568ae5c58..8f122bb7df 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 @@ -8,7 +8,9 @@ package org.thoughtcrime.securesms.components.webrtc.controls import android.content.ActivityNotFoundException import android.content.Intent import android.content.res.ColorStateList +import android.content.res.Configuration import android.os.Handler +import android.os.Parcelable import android.view.View import android.widget.FrameLayout import android.widget.Toast @@ -40,6 +42,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.addTo 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.R @@ -61,70 +64,64 @@ import kotlin.math.max import kotlin.time.Duration.Companion.seconds /** - * Brain for rendering the call controls and info within a bottom sheet. + * Brain for rendering the call controls and info within a bottom sheet when we display the activity in portrait mode. */ -class ControlsAndInfoController( +class ControlsAndInfoController private constructor( private val webRtcCallActivity: WebRtcCallActivity, private val webRtcCallView: WebRtcCallView, private val overflowPopupWindow: CallOverflowPopupWindow, private val viewModel: WebRtcCallViewModel, - private val controlsAndInfoViewModel: ControlsAndInfoViewModel -) : CallInfoView.Callbacks, Disposable { + private val controlsAndInfoViewModel: ControlsAndInfoViewModel, + private val disposables: CompositeDisposable +) : CallInfoView.Callbacks, Disposable by disposables { + + constructor( + webRtcCallActivity: WebRtcCallActivity, + webRtcCallView: WebRtcCallView, + overflowPopupWindow: CallOverflowPopupWindow, + viewModel: WebRtcCallViewModel, + controlsAndInfoViewModel: ControlsAndInfoViewModel + ) : this( + webRtcCallActivity, + webRtcCallView, + overflowPopupWindow, + viewModel, + controlsAndInfoViewModel, + CompositeDisposable() + ) companion object { private val TAG = Log.tag(ControlsAndInfoController::class.java) + private const val CONTROL_TRANSITION_DURATION = 250L private const val CONTROL_FADE_OUT_START = 0f private const val CONTROL_FADE_OUT_DONE = 0.23f private const val INFO_FADE_IN_START = CONTROL_FADE_OUT_DONE private const val INFO_FADE_IN_DONE = 0.8f - private const val CONTROL_TRANSITION_DURATION = 250L - + private val INFO_TRANSLATION_DISTANCE = 24f.dp private val HIDE_CONTROL_DELAY = 5.seconds.inWholeMilliseconds } - private val disposables = CompositeDisposable() + private val coordinator: CoordinatorLayout = webRtcCallView.findViewById(R.id.call_controls_info_coordinator) + private val callInfoComposeView: ComposeView = webRtcCallView.findViewById(R.id.call_info_compose) + private val frame: FrameLayout = webRtcCallView.findViewById(R.id.call_controls_info_parent) + private val behavior = BottomSheetBehavior.from(frame) + private val raiseHandComposeView: ComposeView = webRtcCallView.findViewById(R.id.call_screen_raise_hand_view) + 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 coordinator: CoordinatorLayout - private val frame: FrameLayout - private val behavior: BottomSheetBehavior - private val callInfoComposeView: ComposeView - private val raiseHandComposeView: ComposeView - private val callControls: ConstraintLayout - private val aboveControlsGuideline: Guideline - private val bottomSheetVisibilityListeners = mutableSetOf() private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() } - private val toggleCameraDirectionView: View + private val bottomSheetVisibilityListeners = mutableSetOf() private val handler: Handler? get() = webRtcCallView.handler private var previousCallControlHeightData = HeightData() - private var controlPeakHeight = 0 private var controlState: WebRtcControls = WebRtcControls.NONE init { - val infoTranslationDistance = 24f.dp - coordinator = webRtcCallView.findViewById(R.id.call_controls_info_coordinator) - frame = webRtcCallView.findViewById(R.id.call_controls_info_parent) - behavior = BottomSheetBehavior.from(frame) - callInfoComposeView = webRtcCallView.findViewById(R.id.call_info_compose) - callControls = webRtcCallView.findViewById(R.id.call_controls_constraint_layout) - raiseHandComposeView = webRtcCallView.findViewById(R.id.call_screen_raise_hand_view) - aboveControlsGuideline = webRtcCallView.findViewById(R.id.call_screen_above_controls_guideline) - toggleCameraDirectionView = webRtcCallView.findViewById(R.id.call_screen_camera_direction_toggle) - - callInfoComposeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - val nestedScrollInterop = rememberNestedScrollInteropConnection() - CallInfoView.View(viewModel, controlsAndInfoViewModel, this@ControlsAndInfoController, Modifier.nestedScroll(nestedScrollInterop)) - } - } - - callInfoComposeView.alpha = 0f - callInfoComposeView.translationY = infoTranslationDistance - raiseHandComposeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -132,20 +129,6 @@ class ControlsAndInfoController( } } - frame.background = MaterialShapeDrawable( - ShapeAppearanceModel.builder() - .setTopLeftCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) - .setTopRightCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) - .build() - ).apply { - fillColor = ColorStateList.valueOf(ContextCompat.getColor(webRtcCallActivity, R.color.signal_colorSurface)) - } - - behavior.isHideable = true - behavior.peekHeight = 0 - behavior.state = BottomSheetBehavior.STATE_HIDDEN - BottomSheetBehaviorHack.setNestedScrollingChild(behavior, callInfoComposeView) - coordinator.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> webRtcCallView.post { onControlTopChanged() } } @@ -154,43 +137,22 @@ class ControlsAndInfoController( onControlTopChanged() } + val maxBehaviorHeightPercentage = if (isLandscape) 1f else 0.66f + val minFrameHeightDenominator = if (isLandscape) 1 else 2 + callControls.viewTreeObserver.addOnGlobalLayoutListener { if (callControls.height > 0 && previousCallControlHeightData.hasChanged(callControls.height, coordinator.height)) { previousCallControlHeightData = HeightData(callControls.height, coordinator.height) - controlPeakHeight = callControls.height + callControls.y.toInt() + val controlPeakHeight = callControls.height + callControls.y.toInt() + 16.dp behavior.peekHeight = controlPeakHeight - frame.minimumHeight = coordinator.height / 2 - behavior.maxHeight = (coordinator.height.toFloat() * 0.66f).toInt() + frame.minimumHeight = coordinator.height / minFrameHeightDenominator + behavior.maxHeight = (coordinator.height.toFloat() * maxBehaviorHeightPercentage).toInt() webRtcCallView.post { onControlTopChanged() } } } - behavior.addBottomSheetCallback(object : BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - overflowPopupWindow.dismiss() - if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - controlsAndInfoViewModel.resetScrollState() - if (controlState.isFadeOutEnabled) { - hide(delay = HIDE_CONTROL_DELAY) - } - } else if (newState == BottomSheetBehavior.STATE_EXPANDED || newState == BottomSheetBehavior.STATE_DRAGGING) { - cancelScheduledHide() - } else if (newState == BottomSheetBehavior.STATE_HIDDEN) { - controlsAndInfoViewModel.resetScrollState() - } - } - - override fun onSlide(bottomSheet: View, slideOffset: Float) { - callControls.alpha = alphaControls(slideOffset) - callControls.visible = callControls.alpha > 0f - - callInfoComposeView.alpha = alphaCallInfo(slideOffset) - callInfoComposeView.translationY = infoTranslationDistance - (infoTranslationDistance * callInfoComposeView.alpha) - } - }) - webRtcCallView.addWindowInsetsListener(object : InsetAwareConstraintLayout.WindowInsetsListener { override fun onApplyWindowInsets(statusBar: Int, navigationBar: Int, parentStart: Int, parentEnd: Int) { if (navigationBar > 0) { @@ -210,20 +172,77 @@ class ControlsAndInfoController( setName(bundle.getString(resultKey)!!) } } - } - fun onControlTopChanged() { - val guidelineTop = max(frame.top, coordinator.height - behavior.peekHeight) - aboveControlsGuideline.setGuidelineBegin(guidelineTop) - webRtcCallView.onControlTopChanged() + frame.background = MaterialShapeDrawable( + ShapeAppearanceModel.builder() + .setTopLeftCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) + .setTopRightCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) + .build() + ).apply { + fillColor = ColorStateList.valueOf(ContextCompat.getColor(webRtcCallActivity, R.color.signal_colorSurface)) + } + + behavior.isHideable = true + behavior.peekHeight = 0 + behavior.state = BottomSheetBehavior.STATE_HIDDEN + BottomSheetBehaviorHack.setNestedScrollingChild(behavior, callInfoComposeView) + + behavior.addBottomSheetCallback(object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + overflowPopupWindow.dismiss() + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + controlsAndInfoViewModel.resetScrollState() + if (controlState.isFadeOutEnabled) { + hide(delay = HIDE_CONTROL_DELAY) + } + } else if (newState == BottomSheetBehavior.STATE_EXPANDED || newState == BottomSheetBehavior.STATE_DRAGGING) { + cancelScheduledHide() + } else if (newState == BottomSheetBehavior.STATE_HIDDEN) { + controlsAndInfoViewModel.resetScrollState() + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + updateCallSheetVisibilities(slideOffset) + } + }) + + callInfoComposeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + CallInfoView.View(viewModel, controlsAndInfoViewModel, this@ControlsAndInfoController, Modifier.nestedScroll(nestedScrollInterop)) + } + } + + callInfoComposeView.alpha = 0f + callInfoComposeView.translationY = INFO_TRANSLATION_DISTANCE } fun addVisibilityListener(listener: BottomSheetVisibilityListener): Boolean { return bottomSheetVisibilityListeners.add(listener) } + fun onStateRestored() { + when (behavior.state) { + BottomSheetBehavior.STATE_EXPANDED -> { + showCallInfo() + updateCallSheetVisibilities(1f) + } + BottomSheetBehavior.STATE_HIDDEN -> { + hide() + updateCallSheetVisibilities(-1f) + } + else -> { + showControls() + updateCallSheetVisibilities(0f) + } + } + } + fun showCallInfo() { cancelScheduledHide() + behavior.isHideable = false behavior.state = BottomSheetBehavior.STATE_EXPANDED } @@ -282,6 +301,20 @@ class ControlsAndInfoController( hide(delay = HIDE_CONTROL_DELAY) } + private fun updateCallSheetVisibilities(slideOffset: Float) { + callControls.alpha = alphaControls(slideOffset) + callControls.visible = callControls.alpha > 0f + + callInfoComposeView.alpha = alphaCallInfo(slideOffset) + callInfoComposeView.translationY = INFO_TRANSLATION_DISTANCE - (INFO_TRANSLATION_DISTANCE * callInfoComposeView.alpha) + } + + private fun onControlTopChanged() { + val guidelineTop = max(frame.top, coordinator.height - behavior.peekHeight) + aboveControlsGuideline.setGuidelineBegin(guidelineTop) + webRtcCallView.onControlTopChanged() + } + private fun showOrHideControlsOnUpdate(previousState: WebRtcControls) { if (controlState == WebRtcControls.PIP || controlState.displayErrorControls()) { hide() @@ -346,34 +379,6 @@ class ControlsAndInfoController( handler?.removeCallbacks(scheduleHideControlsRunnable) } - private fun alphaControls(slideOffset: Float): Float { - return if (slideOffset <= CONTROL_FADE_OUT_START) { - 1f - } else if (slideOffset >= CONTROL_FADE_OUT_DONE) { - 0f - } else { - 1f - (1f * (slideOffset - CONTROL_FADE_OUT_START) / (CONTROL_FADE_OUT_DONE - CONTROL_FADE_OUT_START)) - } - } - - private fun alphaCallInfo(slideOffset: Float): Float { - return if (slideOffset >= INFO_FADE_IN_DONE) { - 1f - } else if (slideOffset <= INFO_FADE_IN_START) { - 0f - } else { - (1f * (slideOffset - INFO_FADE_IN_START) / (INFO_FADE_IN_DONE - INFO_FADE_IN_START)) - } - } - - override fun dispose() { - disposables.dispose() - } - - override fun isDisposed(): Boolean { - return disposables.isDisposed - } - override fun onShareLinkClicked() { val mimeType = Intent.normalizeMimeType("text/plain") val shareIntent = ShareCompat.IntentBuilder(webRtcCallActivity) @@ -461,10 +466,31 @@ class ControlsAndInfoController( displayEndCall() != previousState.displayEndCall() } + private fun alphaControls(slideOffset: Float): Float { + return if (slideOffset <= CONTROL_FADE_OUT_START) { + 1f + } else if (slideOffset >= CONTROL_FADE_OUT_DONE) { + 0f + } else { + 1f - (1f * (slideOffset - CONTROL_FADE_OUT_START) / (CONTROL_FADE_OUT_DONE - CONTROL_FADE_OUT_START)) + } + } + + private fun alphaCallInfo(slideOffset: Float): Float { + return if (slideOffset >= INFO_FADE_IN_DONE) { + 1f + } else if (slideOffset <= INFO_FADE_IN_START) { + 0f + } else { + (1f * (slideOffset - INFO_FADE_IN_START) / (INFO_FADE_IN_DONE - INFO_FADE_IN_START)) + } + } + + @Parcelize private data class HeightData( val controlHeight: Int = 0, val coordinatorHeight: Int = 0 - ) { + ) : Parcelable { fun hasChanged(controlHeight: Int, coordinatorHeight: Int): Boolean { return controlHeight != this.controlHeight || coordinatorHeight != this.coordinatorHeight } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java index a3885c5fb2..3c52d4f661 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java @@ -39,6 +39,10 @@ public final class WindowUtil { clearSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } + public static void clearTranslucentNavigationBar(@NonNull Window window) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + public static void setLightNavigationBar(@NonNull Window window) { if (Build.VERSION.SDK_INT < 27) return; diff --git a/app/src/main/res/layout-land/webrtc_call_controls.xml b/app/src/main/res/layout-land/webrtc_call_controls.xml new file mode 100644 index 0000000000..de0358c961 --- /dev/null +++ b/app/src/main/res/layout-land/webrtc_call_controls.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml b/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml new file mode 100644 index 0000000000..0970b424ee --- /dev/null +++ b/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/webrtc_call_participant_pager.xml b/app/src/main/res/layout-land/webrtc_call_participant_pager.xml new file mode 100644 index 0000000000..c9f69d400b --- /dev/null +++ b/app/src/main/res/layout-land/webrtc_call_participant_pager.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml b/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml new file mode 100644 index 0000000000..155cceaf4e --- /dev/null +++ b/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_controls.xml b/app/src/main/res/layout/webrtc_call_controls.xml index 2284811bcf..d60806f429 100644 --- a/app/src/main/res/layout/webrtc_call_controls.xml +++ b/app/src/main/res/layout/webrtc_call_controls.xml @@ -2,7 +2,6 @@ ~ Copyright 2023 Signal Messenger, LLC ~ SPDX-License-Identifier: AGPL-3.0-only --> - @@ -127,11 +127,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml b/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml new file mode 100644 index 0000000000..6b553ec026 --- /dev/null +++ b/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_pager.xml b/app/src/main/res/layout/webrtc_call_participant_pager.xml new file mode 100644 index 0000000000..fe90aa437f --- /dev/null +++ b/app/src/main/res/layout/webrtc_call_participant_pager.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml b/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml index dfda467441..4ac78f959d 100644 --- a/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml +++ b/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml @@ -1,15 +1,16 @@ - - - - - + - + - - - - - - - - - - - + + android:layout_gravity="center_horizontal" + tools:viewBindingIgnore="true"> - - + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/call_screen_status"> diff --git a/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml b/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml new file mode 100644 index 0000000000..6bacf2661e --- /dev/null +++ b/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index 7902f5c912..148bf0fdfa 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -5,4 +5,13 @@ 16dp 8dp 16dp + + + 10dp + 0dp + 80dp + 48dp + 0dp + 4dp + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7a05f27ee6..c493f10303 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -238,4 +238,11 @@ 140dp + 8dp + 30dp + 96dp + 106dp + 4dp + 0dp + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0f70240353..983cbbc1ac 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -231,6 +231,9 @@ @dimen/webrtc_button_size @null @null + @dimen/CallControls__button_horizontal_margin + @dimen/CallControls__button_horizontal_margin + @dimen/CallControls__button_bottom_margin