diff --git a/app/build.gradle b/app/build.gradle index c54e65237..43fbff9ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -376,6 +376,7 @@ dependencies { implementation ('androidx.appcompat:appcompat:1.2.0') { force = true } + implementation "androidx.window:window:1.0.0-alpha09" implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0' diff --git a/app/proguard/proguard.cfg b/app/proguard/proguard.cfg index a7218672f..04d97b2cb 100644 --- a/app/proguard/proguard.cfg +++ b/app/proguard/proguard.cfg @@ -9,3 +9,5 @@ # Protobuf lite -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } + +-keep class androidx.window.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81126b7e8..e908bc83e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,6 +96,7 @@ android:label="@string/app_name" android:supportsRtl="true" tools:replace="android:allowBackup" + android:resizeableActivity="true" android:allowBackup="false" android:theme="@style/TextSecure.LightTheme" android:largeHeap="true"> @@ -104,6 +105,9 @@ android:name="com.google.android.geo.API_KEY" android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/> + + @@ -117,16 +121,15 @@ diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index dc343acde..d204cfd93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -22,8 +22,10 @@ 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; @@ -35,11 +37,16 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; +import androidx.core.util.Consumer; import androidx.lifecycle.ViewModelProviders; +import androidx.window.DisplayFeature; +import androidx.window.FoldingFeature; +import androidx.window.WindowLayoutInfo; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; @@ -50,6 +57,7 @@ import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeN 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.participantslist.CallParticipantsListDialog; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -70,6 +78,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import java.util.List; +import java.util.Optional; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; @@ -88,11 +97,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private CallParticipantsListUpdatePopupWindow participantUpdateWindow; private DeviceOrientationMonitor deviceOrientationMonitor; - private FullscreenHelper fullscreenHelper; - private WebRtcCallView callScreen; - private TooltipPopup videoTooltip; - private WebRtcCallViewModel viewModel; - private boolean enableVideoIfAvailable; + private FullscreenHelper fullscreenHelper; + private WebRtcCallView callScreen; + private TooltipPopup videoTooltip; + private WebRtcCallViewModel viewModel; + private boolean enableVideoIfAvailable; + private androidx.window.WindowManager windowManager; + private WindowLayoutInfoConsumer windowLayoutInfoConsumer; @Override protected void attachBaseContext(@NonNull Context newBase) { @@ -100,6 +111,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan super.attachBaseContext(newBase); } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if (newConfig.densityDpi >= 480 && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) { + viewModel.setIsLandscapeEnabled(true); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } else if (newConfig.densityDpi < 480){ + viewModel.setIsLandscapeEnabled(false); + } + } + + @SuppressLint("SourceLockedOrientationActivity") @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate()"); @@ -107,6 +131,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); + boolean isLandscapeEnabled = getResources().getConfiguration().densityDpi >= 480; + if (!isLandscapeEnabled) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.webrtc_call_activity); @@ -115,12 +144,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); initializeResources(); - initializeViewModel(); + initializeViewModel(isLandscapeEnabled); processIntent(getIntent()); enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false); getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE); + + windowManager = new androidx.window.WindowManager(this); + windowLayoutInfoConsumer = new WindowLayoutInfoConsumer(windowManager); + + windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer); } @Override @@ -178,6 +212,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan @Override protected void onDestroy() { super.onDestroy(); + windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer); EventBus.getDefault().unregister(this); } @@ -208,8 +243,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private boolean enterPipModeIfPossible() { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) { PictureInPictureParams params = new PictureInPictureParams.Builder() - .setAspectRatio(new Rational(9, 16)) - .build(); + .setAspectRatio(new Rational(9, 16)) + .build(); enterPictureInPictureMode(params); CallParticipantsListDialog.dismiss(getSupportFragmentManager()); @@ -248,13 +283,14 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen); } - private void initializeViewModel() { + private void initializeViewModel(boolean isLandscapeEnabled) { deviceOrientationMonitor = new DeviceOrientationMonitor(this); getLifecycle().addObserver(deviceOrientationMonitor); WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor); viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class); + viewModel.setIsLandscapeEnabled(isLandscapeEnabled); viewModel.setIsInPipMode(isInPipMode()); viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls); @@ -276,25 +312,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } }); - viewModel.getOrientation().observe(this, orientation -> { - ApplicationDependencies.getSignalCallManager().orientationChanged(orientation.getDegrees()); - - switch (orientation) { - case LANDSCAPE_LEFT_EDGE: - callScreen.rotateControls(90); - break; - case LANDSCAPE_RIGHT_EDGE: - callScreen.rotateControls(-90); - break; - case PORTRAIT_BOTTOM_EDGE: - callScreen.rotateControls(0); - } - }); + viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees())); + viewModel.getControlsRotation().observe(this, callScreen::rotateControls); } private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { if (event instanceof WebRtcCallViewModel.Event.StartCall) { - startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall()); + startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall()); return; } else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) { SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords()); @@ -406,7 +430,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) .ifNecessary() .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)), - R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted) + R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted) .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) .onAllGranted(() -> { callScreen.setRecipient(recipient); @@ -490,13 +514,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan 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 AlertDialog.Builder(this) - .setTitle(R.string.RedPhone_number_not_registered) - .setIcon(R.drawable.ic_warning) - .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(); + .setTitle(R.string.RedPhone_number_not_registered) + .setIcon(R.drawable.ic_warning) + .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) { @@ -588,20 +612,34 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan callScreen.setRecipient(event.getRecipient()); switch (event.getState()) { - case CALL_PRE_JOIN: handleCallPreJoin(event); break; - case CALL_CONNECTED: handleCallConnected(event); break; - case NETWORK_FAILURE: handleServerFailure(); break; - case CALL_RINGING: handleCallRinging(); break; - case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); 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; + case CALL_PRE_JOIN: + handleCallPreJoin(event); break; + case CALL_CONNECTED: + handleCallConnected(event); break; + case NETWORK_FAILURE: + handleServerFailure(); break; + case CALL_RINGING: + handleCallRinging(); break; + case CALL_DISCONNECTED: + handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); 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; } boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable; @@ -732,4 +770,31 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan viewModel.onLocalPictureInPictureClicked(); } } + + private class WindowLayoutInfoConsumer implements Consumer { + + private final androidx.window.WindowManager windowManager; + + private WindowLayoutInfoConsumer(androidx.window.WindowManager windowManager) { + this.windowManager = windowManager; + } + + @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.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) { + 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()); + } + } + } + } } 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 e68351d62..b7c45771b 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 @@ -20,6 +20,8 @@ import java.util.WeakHashMap; */ public class BroadcastVideoSink implements VideoSink { + public static final int DEVICE_ROTATION_IGNORE = -1; + private final EglBase eglBase; private final WeakHashMap sinks; private final WeakHashMap requestingSizes; @@ -85,7 +87,10 @@ public class BroadcastVideoSink implements VideoSink { @Override public synchronized void onFrame(@NonNull VideoFrame videoFrame) { - if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth() || forceRotate) { + boolean isDeviceRotationIgnored = deviceOrientationDegrees == DEVICE_ROTATION_IGNORE; + boolean isWideVideoFrame = videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth(); + + if (!isDeviceRotationIgnored && (isWideVideoFrame || forceRotate)) { int rotation = calculateRotation(); if (rotation > 0) { rotation += rotateWithDevice ? videoFrame.getRotation() : 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java index 459e5dcf1..648923258 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java @@ -34,6 +34,7 @@ public class CallParticipantsLayout extends FlexboxLayout { private CallParticipant focusedParticipant = null; private boolean shouldRenderInPip; private boolean isPortrait; + private LayoutStrategy layoutStrategy; public CallParticipantsLayout(@NonNull Context context) { super(context); @@ -47,11 +48,18 @@ public class CallParticipantsLayout extends FlexboxLayout { super(context, attrs, defStyleAttr); } - void update(@NonNull List callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip, boolean isPortrait) { + void update(@NonNull List callParticipants, + @NonNull CallParticipant focusedParticipant, + boolean shouldRenderInPip, + boolean isPortrait, + @NonNull LayoutStrategy layoutStrategy) + { this.callParticipants = callParticipants; this.focusedParticipant = focusedParticipant; this.shouldRenderInPip = shouldRenderInPip; this.isPortrait = isPortrait; + this.layoutStrategy = layoutStrategy; + setFlexDirection(layoutStrategy.getFlexDirection()); updateLayout(); } @@ -127,7 +135,7 @@ public class CallParticipantsLayout extends FlexboxLayout { callParticipantView.useLargeAvatar(); } - setChildLayoutParams(view, index, getChildCount()); + layoutStrategy.setChildLayoutParams(view, index, getChildCount()); } private void addCallParticipantView() { @@ -139,17 +147,9 @@ public class CallParticipantsLayout extends FlexboxLayout { addView(view); } - private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) { - FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams(); - if (childCount < 3) { - params.setFlexBasisPercent(1f); - } else { - if ((childCount % 2) != 0 && childPosition == childCount - 1) { - params.setFlexBasisPercent(1f); - } else { - params.setFlexBasisPercent(0.5f); - } - } - child.setLayoutParams(params); + public interface LayoutStrategy { + int getFlexDirection(); + + void setChildLayoutParams(@NonNull View child, int childPosition, int childCount); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt new file mode 100644 index 000000000..c234445c2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.components.webrtc + +import android.view.View +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayout + +object CallParticipantsLayoutStrategies { + + private object Portrait : CallParticipantsLayout.LayoutStrategy { + override fun getFlexDirection(): Int = FlexDirection.ROW + + override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) { + val params = child.layoutParams as FlexboxLayout.LayoutParams + if (childCount < 3) { + params.flexBasisPercent = 1f + } else { + if ((childCount % 2) != 0 && childPosition == childCount - 1) { + params.flexBasisPercent = 1f + } else { + params.flexBasisPercent = 0.5f + } + } + child.layoutParams = params + } + } + + private object Landscape : CallParticipantsLayout.LayoutStrategy { + override fun getFlexDirection() = FlexDirection.COLUMN + + override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) { + val params = child.layoutParams as FlexboxLayout.LayoutParams + if (childCount < 4) { + params.flexBasisPercent = 1f + } else { + if ((childCount % 2) != 0 && childPosition == childCount - 1) { + params.flexBasisPercent = 1f + } else { + params.flexBasisPercent = 0.5f + } + } + child.layoutParams = params + } + } + + @JvmStatic + fun getStrategy(isPortrait: Boolean): CallParticipantsLayout.LayoutStrategy { + return if (isPortrait) { + Portrait + } else { + Landscape + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java index c9741864f..d8b8fd784 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java @@ -36,7 +36,8 @@ public final class CallParticipantsState { false, false, false, - OptionalLong.empty()); + OptionalLong.empty(), + WebRtcControls.FoldableState.flat()); private final WebRtcViewModel.State callState; private final WebRtcViewModel.GroupCallState groupCallState; @@ -48,6 +49,7 @@ public final class CallParticipantsState { private final boolean showVideoForOutgoing; private final boolean isViewingFocusedParticipant; private final OptionalLong remoteDevicesCount; + private final WebRtcControls.FoldableState foldableState; public CallParticipantsState(@NonNull WebRtcViewModel.State callState, @NonNull WebRtcViewModel.GroupCallState groupCallState, @@ -58,7 +60,8 @@ public final class CallParticipantsState { boolean isInPipMode, boolean showVideoForOutgoing, boolean isViewingFocusedParticipant, - OptionalLong remoteDevicesCount) + OptionalLong remoteDevicesCount, + @NonNull WebRtcControls.FoldableState foldableState) { this.callState = callState; this.groupCallState = groupCallState; @@ -70,6 +73,7 @@ public final class CallParticipantsState { this.showVideoForOutgoing = showVideoForOutgoing; this.isViewingFocusedParticipant = isViewingFocusedParticipant; this.remoteDevicesCount = remoteDevicesCount; + this.foldableState = foldableState; } public @NonNull WebRtcViewModel.State getCallState() { @@ -94,7 +98,10 @@ public final class CallParticipantsState { listParticipants.addAll(remoteParticipants.getListParticipants()); } - listParticipants.add(CallParticipant.EMPTY); + if (foldableState.isFlat()) { + listParticipants.add(CallParticipant.EMPTY); + } + Collections.reverse(listParticipants); return listParticipants; @@ -155,6 +162,10 @@ public final class CallParticipantsState { return localRenderState; } + public boolean isFolded() { + return foldableState.isFolded(); + } + public boolean isLargeVideoGroup() { return getAllRemoteParticipants().size() > SMALL_GROUP_MAX; } @@ -215,7 +226,8 @@ public final class CallParticipantsState { oldState.isInPipMode, newShowVideoForOutgoing, oldState.isViewingFocusedParticipant, - webRtcViewModel.getRemoteDevicesCount()); + webRtcViewModel.getRemoteDevicesCount(), + oldState.foldableState); } public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) { @@ -237,7 +249,8 @@ public final class CallParticipantsState { isInPip, oldState.showVideoForOutgoing, oldState.isViewingFocusedParticipant, - oldState.remoteDevicesCount); + oldState.remoteDevicesCount, + oldState.foldableState); } public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) { @@ -259,7 +272,8 @@ public final class CallParticipantsState { oldState.isInPipMode, oldState.showVideoForOutgoing, oldState.isViewingFocusedParticipant, - oldState.remoteDevicesCount); + oldState.remoteDevicesCount, + oldState.foldableState); } public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) { @@ -281,7 +295,31 @@ public final class CallParticipantsState { oldState.isInPipMode, oldState.showVideoForOutgoing, selectedPage == SelectedPage.FOCUSED, - oldState.remoteDevicesCount); + oldState.remoteDevicesCount, + oldState.foldableState); + } + + public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull WebRtcControls.FoldableState foldableState) { + WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant, + oldState.isInPipMode, + oldState.showVideoForOutgoing, + oldState.getGroupCallState().isNotIdle(), + oldState.callState, + oldState.getAllRemoteParticipants().size(), + oldState.isViewingFocusedParticipant, + oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED); + + return new CallParticipantsState(oldState.callState, + oldState.groupCallState, + oldState.remoteParticipants, + oldState.localParticipant, + oldState.focusedParticipant, + localRenderState, + oldState.isInPipMode, + oldState.showVideoForOutgoing, + oldState.isViewingFocusedParticipant, + oldState.remoteDevicesCount, + foldableState); } private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java index d5dc2f7c2..28ddf3aef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java @@ -116,14 +116,16 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu } public void clearVerticalBoundaries() { - setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop()); + setTopVerticalBoundary(parent.getTop()); + setBottomVerticalBoundary(parent.getMeasuredHeight() + parent.getTop()); } - public void setVerticalBoundaries(int topBoundary, int bottomBoundary) { - extraPaddingTop = topBoundary - parent.getTop(); - extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; + public void setTopVerticalBoundary(int topBoundary) { + extraPaddingTop = topBoundary - parent.getTop(); + } - adjustPip(); + public void setBottomVerticalBoundary(int bottomBoundary) { + extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; } private boolean onGestureFinished(MotionEvent e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java index 6b040e0e1..1320dacbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java @@ -64,6 +64,10 @@ class WebRtcCallParticipantsPage { return isPortrait; } + public @NonNull CallParticipantsLayout.LayoutStrategy getLayoutStrategy() { + return CallParticipantsLayoutStrategies.getStrategy(isPortrait); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java index cb8e1843f..8dbcc9523 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java @@ -86,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter incomingCallViews = new HashSet<>(); private final Set topViews = new HashSet<>(); private final Set visibleViewSet = new HashSet<>(); @@ -161,6 +162,10 @@ public class WebRtcCallView extends FrameLayout { errorButton = findViewById(R.id.call_screen_error_cancel); groupCallSpeakerHint = new Stub<>(findViewById(R.id.call_screen_group_call_speaker_hint)); groupCallFullStub = new Stub<>(findViewById(R.id.group_call_call_full_view)); + topFoldGuideline = findViewById(R.id.fold_top_guideline); + callScreenTopFoldGuideline = findViewById(R.id.fold_top_call_screen_guideline); + foldParticipantCountWrapper = findViewById(R.id.fold_show_participants_menu_counter_wrapper); + foldParticipantCount = findViewById(R.id.fold_show_participants_menu_counter); View topGradient = findViewById(R.id.call_screen_header_gradient); View decline = findViewById(R.id.call_screen_decline_call); @@ -279,10 +284,18 @@ public class WebRtcCallView extends FrameLayout { @Override public void onWindowSystemUiVisibilityChanged(int visible) { if ((visible & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { - pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop()); + if (controls.adjustForFold()) { + pictureInPictureGestureHelper.clearVerticalBoundaries(); + pictureInPictureGestureHelper.setTopVerticalBoundary(toolbar.getBottom()); + } else { + pictureInPictureGestureHelper.setTopVerticalBoundary(toolbar.getBottom()); + pictureInPictureGestureHelper.setBottomVerticalBoundary(videoToggle.getTop()); + } } else { pictureInPictureGestureHelper.clearVerticalBoundaries(); } + + pictureInPictureGestureHelper.adjustPip(); } @Override @@ -323,16 +336,22 @@ public class WebRtcCallView extends FrameLayout { } if (state.getGroupCallState().isNotIdle() && participantCount != null) { - participantCount.setText(state.getParticipantCount() - .mapToObj(String::valueOf).orElse("\u2014")); - participantCount.setEnabled(state.getParticipantCount().isPresent()); + String text = state.getParticipantCount() + .mapToObj(String::valueOf).orElse("\u2014"); + boolean enabled = state.getParticipantCount().isPresent(); + + participantCount.setText(text); + participantCount.setEnabled(enabled); + + foldParticipantCount.setText(text); + foldParticipantCount.setEnabled(enabled); } pagerAdapter.submitList(pages); recyclerAdapter.submitList(state.getListParticipants()); updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant()); - if (state.isLargeVideoGroup() && !state.isInPipMode()) { + if (state.isLargeVideoGroup() && !state.isInPipMode() && !state.isFolded()) { layoutParticipantsForLargeCount(); } else { layoutParticipantsForSmallCount(); @@ -423,7 +442,9 @@ public class WebRtcCallView extends FrameLayout { toolbar.inflateMenu(R.menu.group_call); View showParticipants = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list).getActionView(); + showParticipants.setAlpha(0); showParticipants.setOnClickListener(unused -> showParticipantsList()); + foldParticipantCountWrapper.setOnClickListener(unused -> showParticipantsList()); participantCount = showParticipants.findViewById(R.id.show_participants_menu_counter); } @@ -480,6 +501,20 @@ public class WebRtcCallView extends FrameLayout { visibleViewSet.clear(); + if (webRtcControls.adjustForFold()) { + topFoldGuideline.setGuidelineEnd(webRtcControls.getFold()); + callScreenTopFoldGuideline.setGuidelineEnd(webRtcControls.getFold()); + + if (webRtcControls.displayGroupMembersButton()) { + visibleViewSet.add(foldParticipantCountWrapper); + } + } else { + topFoldGuideline.setGuidelineEnd(0); + callScreenTopFoldGuideline.setGuidelineEnd(0); + } + + setShowParticipantsState(webRtcControls.adjustForFold()); + if (webRtcControls.displayStartCallControls()) { visibleViewSet.add(footerGradient); visibleViewSet.add(startCallControls); @@ -577,10 +612,15 @@ public class WebRtcCallView extends FrameLayout { controls = webRtcControls; + if (!controls.isFadeOutEnabled()) { + controlsVisible = true; + } + if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) { fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons()); - post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop())); } + + onWindowSystemUiVisibilityChanged(getWindowSystemUiVisibility()); } public @NonNull View getVideoTooltipTarget() { @@ -597,6 +637,18 @@ public class WebRtcCallView extends FrameLayout { } } + private void setShowParticipantsState(boolean isFolded) { + MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list); + + if (item != null) { + View showParticipants = item.getActionView(); + showParticipants.animate().alpha(isFolded ? 0f : 1f); + showParticipants.setClickable(!isFolded); + foldParticipantCountWrapper.animate().alpha(isFolded ? 1f : 0f); + foldParticipantCount.setClickable(isFolded); + } + } + private void expandPip(@NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) { pictureInPictureExpansionHelper.expand(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() { @Override @@ -760,6 +812,8 @@ public class WebRtcCallView extends FrameLayout { constraintSet.setVisibility(view.getId(), visibility); } + adjustParticipantsRecycler(constraintSet); + constraintSet.applyTo(parent); layoutParticipants(); @@ -789,9 +843,21 @@ public class WebRtcCallView extends FrameLayout { } } + adjustParticipantsRecycler(constraintSet); + constraintSet.applyTo(parent); } + private void adjustParticipantsRecycler(@NonNull ConstraintSet constraintSet) { + if (controlsVisible) { + constraintSet.connect(R.id.call_screen_participants_recycler, ConstraintSet.BOTTOM, R.id.call_screen_video_toggle, ConstraintSet.TOP); + } else { + constraintSet.connect(R.id.call_screen_participants_recycler, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM); + } + + constraintSet.setHorizontalBias(R.id.call_screen_participants_recycler, controls.adjustForFold() ? 0.5f : 1f); + } + private void scheduleFadeOut() { cancelFadeOut(); 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 d3f63babe..7d9c19ac0 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,6 +2,7 @@ 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; @@ -14,6 +15,7 @@ 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.IdentityDatabase; @@ -41,7 +43,9 @@ public class WebRtcCallViewModel extends ViewModel { private final MutableLiveData microphoneEnabled = new MutableLiveData<>(true); private final MutableLiveData isInPipMode = new MutableLiveData<>(false); private final MutableLiveData webRtcControls = new MutableLiveData<>(WebRtcControls.NONE); - private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls); + private final MutableLiveData foldableState = new MutableLiveData<>(WebRtcControls.FoldableState.flat()); + private final LiveData controlsWithFoldableState = LiveDataUtil.combineLatest(foldableState, webRtcControls, this::updateControlsFoldableState); + private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls); private final SingleLiveEvent events = new SingleLiveEvent(); private final MutableLiveData elapsed = new MutableLiveData<>(-1L); private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live()); @@ -53,6 +57,8 @@ public class WebRtcCallViewModel extends ViewModel { private final LiveData> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1); private final LiveData shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint); private final LiveData orientation; + private final MutableLiveData isLandscapeEnabled = new MutableLiveData<>(); + private final LiveData controlsRotation; private boolean canDisplayTooltipIfNeeded = true; private boolean hasEnabledLocalVideo = false; @@ -69,13 +75,24 @@ public class WebRtcCallViewModel extends ViewModel { private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication()); private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) { - orientation = deviceOrientationMonitor.getOrientation(); + orientation = deviceOrientationMonitor.getOrientation(); + controlsRotation = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(isLandscapeEnabled), + Transformations.distinctUntilChanged(orientation), + this::resolveRotation); + } + + 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); } @@ -92,6 +109,12 @@ public class WebRtcCallViewModel extends ViewModel { liveRecipient.setValue(recipient.live()); } + public void setFoldableState(@NonNull WebRtcControls.FoldableState foldableState) { + this.foldableState.postValue(foldableState); + + ThreadUtil.runOnMain(() -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), foldableState))); + } + public LiveData getEvents() { return events; } @@ -140,6 +163,10 @@ public class WebRtcCallViewModel extends ViewModel { participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode)); } + public void setIsLandscapeEnabled(boolean isLandscapeEnabled) { + this.isLandscapeEnabled.postValue(isLandscapeEnabled); + } + @MainThread public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) { if (page == CallParticipantsState.SelectedPage.FOCUSED) { @@ -239,6 +266,23 @@ public class WebRtcCallViewModel extends ViewModel { } } + 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); } @@ -314,7 +358,12 @@ public class WebRtcCallViewModel extends ViewModel { callState, groupCallState, audioOutput, - participantLimit)); + participantLimit, + WebRtcControls.FoldableState.flat())); + } + + private @NonNull WebRtcControls updateControlsFoldableState(@NonNull WebRtcControls.FoldableState foldableState, @NonNull WebRtcControls controls) { + return controls.withFoldableState(foldableState); } private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) { 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 79478ac8f..6c1b0bd2d 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 @@ -4,6 +4,7 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Px; import androidx.annotation.StringRes; import org.thoughtcrime.securesms.R; @@ -11,7 +12,7 @@ import org.thoughtcrime.securesms.R; public final class WebRtcControls { public static final WebRtcControls NONE = new WebRtcControls(); - public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null); + public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat()); private final boolean isRemoteVideoEnabled; private final boolean isLocalVideoEnabled; @@ -23,9 +24,10 @@ public final class WebRtcControls { private final GroupCallState groupCallState; private final WebRtcAudioOutput audioOutput; private final Long participantLimit; + private final FoldableState foldableState; private WebRtcControls() { - this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null); + this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat()); } WebRtcControls(boolean isLocalVideoEnabled, @@ -37,7 +39,8 @@ public final class WebRtcControls { @NonNull CallState callState, @NonNull GroupCallState groupCallState, @NonNull WebRtcAudioOutput audioOutput, - @Nullable Long participantLimit) + @Nullable Long participantLimit, + @NonNull FoldableState foldableState) { this.isLocalVideoEnabled = isLocalVideoEnabled; this.isRemoteVideoEnabled = isRemoteVideoEnabled; @@ -49,6 +52,21 @@ public final class WebRtcControls { this.groupCallState = groupCallState; this.audioOutput = audioOutput; this.participantLimit = participantLimit; + this.foldableState = foldableState; + } + + public @NonNull WebRtcControls withFoldableState(FoldableState foldableState) { + return new WebRtcControls(isLocalVideoEnabled, + isRemoteVideoEnabled, + isMoreThanOneCameraAvailable, + isBluetoothAvailable, + isInPipMode, + hasAtLeastOneRemote, + callState, + groupCallState, + audioOutput, + participantLimit, + foldableState); } boolean displayErrorControls() { @@ -59,6 +77,14 @@ public final class WebRtcControls { return isPreJoin(); } + boolean adjustForFold() { + return foldableState.isFolded(); + } + + @Px int getFold() { + return foldableState.getFoldPoint(); + } + @StringRes int getStartCallButtonText() { if (isGroupCall()) { if (groupCallState == GroupCallState.FULL) { @@ -130,7 +156,7 @@ public final class WebRtcControls { } boolean isFadeOutEnabled() { - return isAtLeastOutgoing() && isRemoteVideoEnabled; + return isAtLeastOutgoing() && isRemoteVideoEnabled && foldableState.isFlat(); } boolean displaySmallOngoingCallButtons() { @@ -199,4 +225,35 @@ public final class WebRtcControls { return compareTo(other) >= 0; } } + + public static final class FoldableState { + + private static final int NOT_SET = -1; + + private final int foldPoint; + + public FoldableState(int foldPoint) { + this.foldPoint = foldPoint; + } + + public boolean isFolded() { + return foldPoint != NOT_SET; + } + + public boolean isFlat() { + return foldPoint == NOT_SET; + } + + public int getFoldPoint() { + return foldPoint; + } + + public static @NonNull FoldableState folded(int foldPoint) { + return new FoldableState(foldPoint); + } + + public static @NonNull FoldableState flat() { + return new FoldableState(NOT_SET); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/CalleeMustAcceptMessageRequestActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/CalleeMustAcceptMessageRequestActivity.java index f800d51eb..24259b78b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/CalleeMustAcceptMessageRequestActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/CalleeMustAcceptMessageRequestActivity.java @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.messagerequests; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -44,11 +46,17 @@ public class CalleeMustAcceptMessageRequestActivity extends BaseActivity { return intent; } + @SuppressLint("SourceLockedOrientationActivity") @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.callee_must_accept_message_request_dialog_fragment); + boolean callingFixedToPortrait = getResources().getConfiguration().densityDpi < 480; + if (callingFixedToPortrait) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + description = findViewById(R.id.description); avatar = findViewById(R.id.avatar); okay = findViewById(R.id.okay); 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 43af680b2..d6b414524 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 @@ -168,8 +168,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleUpdateRenderedResolutions(s)); } - public void orientationChanged(int degrees) { - process((s, p) -> p.handleOrientationChanged(s, degrees)); + public void orientationChanged(boolean isLandscapeEnabled, int degrees) { + process((s, p) -> p.handleOrientationChanged(s, isLandscapeEnabled, degrees)); } public void setAudioSpeaker(boolean isSpeaker) { @@ -649,7 +649,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. @Override public void onFullyInitialized() { - process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().getOrientation().getDegrees())); + process((s, p) -> p.handleOrientationChanged(s, false, s.getLocalDeviceState().getOrientation().getDegrees())); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 1fc769109..5cf83c048 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -11,6 +11,7 @@ import org.signal.ringrtc.CallException; import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallManager; import org.signal.ringrtc.GroupCall; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.sensors.Orientation; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; @@ -454,24 +455,27 @@ public abstract class WebRtcActionProcessor { return currentState; } - protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, int orientationDegrees) { + protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, boolean isLandscapeEnabled, int orientationDegrees) { Camera camera = currentState.getVideoState().getCamera(); if (camera != null) { camera.setOrientation(orientationDegrees); } + int sinkRotationDegrees = isLandscapeEnabled ? BroadcastVideoSink.DEVICE_ROTATION_IGNORE : orientationDegrees; + int stateRotationDegrees = isLandscapeEnabled ? 0 : orientationDegrees; + BroadcastVideoSink sink = currentState.getVideoState().getLocalSink(); if (sink != null) { - sink.setDeviceOrientationDegrees(orientationDegrees); + sink.setDeviceOrientationDegrees(sinkRotationDegrees); } for (CallParticipant callParticipant : currentState.getCallInfoState().getRemoteCallParticipants()) { - callParticipant.getVideoSink().setDeviceOrientationDegrees(orientationDegrees); + callParticipant.getVideoSink().setDeviceOrientationDegrees(sinkRotationDegrees); } return currentState.builder() .changeLocalDeviceState() - .setOrientation(Orientation.fromDegrees(orientationDegrees)) + .setOrientation(Orientation.fromDegrees(stateRotationDegrees)) .build(); } diff --git a/app/src/main/res/drawable/webrtc_call_screen_footer_gradient.xml b/app/src/main/res/drawable/webrtc_call_screen_footer_gradient.xml new file mode 100644 index 000000000..a55da7737 --- /dev/null +++ b/app/src/main/res/drawable/webrtc_call_screen_footer_gradient.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_view.xml b/app/src/main/res/layout/webrtc_call_view.xml index eb695c360..2b90c1471 100644 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ b/app/src/main/res/layout/webrtc_call_view.xml @@ -4,10 +4,21 @@ xmlns:tools="http://schemas.android.com/tools" tools:parentTag="org.thoughtcrime.securesms.components.webrtc.WebRtcCallView"> + + + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/fold_top_guideline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + android:layout_width="0dp" + android:layout_height="0dp" + android:layout="@layout/group_call_call_full" + app:layout_constraintBottom_toTopOf="@id/fold_top_guideline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + tools:itemCount="2" + tools:listitem="@layout/webrtc_call_participant_recycler_item" + tools:visibility="visible" /> @@ -358,6 +417,39 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + + + + + +