diff --git a/app/proguard/proguard.cfg b/app/proguard/proguard.cfg index bef85c4817..6da53f2e30 100644 --- a/app/proguard/proguard.cfg +++ b/app/proguard/proguard.cfg @@ -16,6 +16,10 @@ -keep class androidx.window.** { *; } +-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key { + public (); +} + # AGP generated dont warns -dontwarn com.android.org.conscrypt.SSLParametersImpl -dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt new file mode 100644 index 0000000000..369599c1ab --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.constraintlayout.widget.Barrier +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.views.SlideUpWithDependencyBehavior +import kotlin.math.max + +/** + * Coordinator Layout Behavior which allows us to "pin" UI Elements to the top of the controls sheet. + */ +class SlideUpWithCallControlsBehavior( + context: Context, + attributeSet: AttributeSet? +) : SlideUpWithDependencyBehavior(context, attributeSet, offsetY = 0f) { + + private var minTranslationY: Float = 0f + + var onTopOfControlsChangedListener: OnTopOfControlsChangedListener? = null + + override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + super.onDependentViewChanged(parent, child, dependency) + + val bottomSheetBehavior = (dependency.layoutParams as CoordinatorLayout.LayoutParams).behavior as BottomSheetBehavior<*> + val slideOffset = bottomSheetBehavior.calculateSlideOffset() + if (slideOffset == 0f) { + minTranslationY = child.translationY + } else { + child.translationY = max(child.translationY, minTranslationY) + } + + emitViewChanged(child) + return true + } + + override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean { + emitViewChanged(child) + return false + } + + override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + return dependency.id == R.id.call_controls_info_parent + } + + private fun emitViewChanged(child: View) { + val barrier = child.findViewById(R.id.call_screen_above_controls_barrier) + onTopOfControlsChangedListener?.onTopOfControlsChanged(barrier.bottom + child.translationY.toInt()) + } + + interface OnTopOfControlsChangedListener { + fun onTopOfControlsChanged(topOfControls: Int) + } +} 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 be28cefbf0..0c9a714bf3 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 @@ -26,10 +26,10 @@ import androidx.annotation.StringRes; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.Toolbar; import androidx.compose.ui.platform.ComposeView; -import androidx.constraintlayout.widget.Barrier; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.Guideline; +import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.util.Consumer; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -72,6 +72,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; public class WebRtcCallView extends InsetAwareConstraintLayout { @@ -128,12 +129,9 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private RecyclerView groupReactionsFeed; private MultiReactionBurstLayout reactionViews; private ComposeView raiseHandSnackbar; - private Barrier pipBottomBoundaryBarrier; private View missingPermissionContainer; private MaterialButton allowAccessButton; - - private WebRtcCallParticipantsPagerAdapter pagerAdapter; private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter; private WebRtcReactionsRecyclerAdapter reactionsAdapter; @@ -210,7 +208,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { groupReactionsFeed = findViewById(R.id.call_screen_reactions_feed); reactionViews = findViewById(R.id.call_screen_reactions_container); raiseHandSnackbar = findViewById(R.id.call_screen_raise_hand_view); - pipBottomBoundaryBarrier = findViewById(R.id.pip_bottom_boundary_barrier); missingPermissionContainer = findViewById(R.id.missing_permissions_container); allowAccessButton = findViewById(R.id.allow_access_button); @@ -375,17 +372,17 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { rotatableControls.add(smallLocalAudioIndicator); rotatableControls.add(ringToggle); - pipBottomBoundaryBarrier.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (bottom != oldBottom) { - onBarrierBottomChanged(bottom); - } - }); - missingPermissionContainer.setVisibility(hasCameraPermission() ? View.GONE : View.VISIBLE); allowAccessButton.setOnClickListener(v -> { runIfNonNull(controlsListener, listener -> listener.onVideoChanged(videoToggle.isEnabled())); }); + + 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); + }); } @Override @@ -986,11 +983,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { } public void onControlTopChanged() { - onBarrierBottomChanged(pipBottomBoundaryBarrier.getBottom()); - } - - private void onBarrierBottomChanged(int barrierBottom) { - pictureInPictureGestureHelper.setBottomVerticalBoundary(barrierBottom); } public interface ControlsListener { 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 4a3fed6ee7..9821059ff7 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 @@ -94,6 +94,8 @@ class ControlsAndInfoController( private val aboveControlsGuideline: Guideline private val bottomSheetVisibilityListeners = mutableSetOf() private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() } + private val toggleCameraDirectionView: View + private val handler: Handler? get() = webRtcCallView.handler @@ -110,6 +112,7 @@ class ControlsAndInfoController( 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) @@ -321,7 +324,6 @@ class ControlsAndInfoController( val margin = if (controlState.displaySmallCallButtons()) 4.dp else 8.dp setControlConstraints(R.id.call_screen_speaker_toggle, controlState.displayAudioToggle(), margin) - setControlConstraints(R.id.call_screen_camera_direction_toggle, controlState.displayCameraToggle(), margin) setControlConstraints(R.id.call_screen_video_toggle, controlState.displayVideoToggle(), margin) setControlConstraints(R.id.call_screen_audio_mic_toggle, controlState.displayMuteAudio(), margin) setControlConstraints(R.id.call_screen_audio_ring_toggle, controlState.displayRingToggle(), margin) @@ -330,6 +332,8 @@ class ControlsAndInfoController( } constraints.applyTo(callControls) + + toggleCameraDirectionView.visible = controlState.displayCameraToggle() } private fun onScheduledHide() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithDependencyBehavior.kt b/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithDependencyBehavior.kt new file mode 100644 index 0000000000..2de6aa7ea9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithDependencyBehavior.kt @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.util.views + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.annotation.Px +import androidx.coordinatorlayout.widget.CoordinatorLayout +import kotlin.math.min + +/** + * @param offsetY - Extra padding between the dependency and child. + * @param maxTranslationY - The maximum offset to apply to child's translationY value. This should be a negative number. + */ +abstract class SlideUpWithDependencyBehavior( + context: Context, + attributeSet: AttributeSet?, + @field:Px @param:Px private val offsetY: Float = 0f +) : CoordinatorLayout.Behavior(context, attributeSet) { + + private val rect = Rect() + + override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + dependency.getLocalVisibleRect(rect) + + val height = if (rect.top < parent.bottom) { + rect.height() + } else { + 0 + } + + val translationY = min(0.0, (dependency.translationY - (height + offsetY)).toDouble()).toFloat() + child.translationY = translationY + + return true + } + + override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) { + child.translationY = 0f + } + + abstract override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.java deleted file mode 100644 index 11cc191ebb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.util.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Dimension; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.Px; -import androidx.coordinatorlayout.widget.CoordinatorLayout; - -import com.google.android.material.snackbar.Snackbar; - -import org.signal.core.util.DimensionUnit; - -public class SlideUpWithSnackbarBehavior extends CoordinatorLayout.Behavior { - - @Dimension(unit = Dimension.DP) - private static final float PAD_TOP_OF_SNACKBAR_DP = 16f; - - @Px - private final float padTopOfSnackbar = DimensionUnit.DP.toPixels(PAD_TOP_OF_SNACKBAR_DP); - - public SlideUpWithSnackbarBehavior(@NonNull Context context, @Nullable AttributeSet attributeSet) { - super(context, attributeSet); - } - - @Override - public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, - @NonNull View child, - @NonNull View dependency) - { - float translationY = Math.min(0, dependency.getTranslationY() - (dependency.getHeight() + padTopOfSnackbar)); - child.setTranslationY(translationY); - - return true; - } - - @Override - public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, - @NonNull View child, - @NonNull View dependency) - { - child.setTranslationY(0); - } - - @SuppressLint("RestrictedApi") - @Override - public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, - @NonNull View child, - @NonNull View dependency) - { - return dependency instanceof Snackbar.SnackbarLayout; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.kt b/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.kt new file mode 100644 index 0000000000..054f00d76b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.kt @@ -0,0 +1,20 @@ +package org.thoughtcrime.securesms.util.views + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.snackbar.Snackbar.SnackbarLayout +import org.signal.core.util.dp + +class SlideUpWithSnackbarBehavior(context: Context, attributeSet: AttributeSet?) : SlideUpWithDependencyBehavior(context, attributeSet, 16f.dp) { + @SuppressLint("RestrictedApi") + override fun layoutDependsOn( + parent: CoordinatorLayout, + child: View, + dependency: View + ): Boolean { + return dependency is SnackbarLayout + } +} diff --git a/app/src/main/res/layout/webrtc_call_controls.xml b/app/src/main/res/layout/webrtc_call_controls.xml index 1b62cad7e9..d6b55eb81b 100644 --- a/app/src/main/res/layout/webrtc_call_controls.xml +++ b/app/src/main/res/layout/webrtc_call_controls.xml @@ -16,6 +16,84 @@ tools:layout_height="match_parent" tools:layout_width="match_parent"> + + + + + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file