mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 05:03:28 +00:00
Implement landscape calling.
This commit is contained in:
committed by
Greyson Parrelli
parent
6e55bc04ab
commit
e7720640d1
@@ -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<Pair<Orientation, Boolean>> 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<DisplayFeature> 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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> 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<Orientation> 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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -78,7 +78,7 @@ class CallStateUpdatePopupWindow(private val parent: ViewGroup) : PopupWindow(
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
if (!enabled) {
|
||||
if (!enabled || parent.windowToken == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<View> topViews = new HashSet<>();
|
||||
private final Set<View> visibleViewSet = new HashSet<>();
|
||||
private final Set<View> allTimeVisibleViews = new HashSet<>();
|
||||
private final Set<View> 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,
|
||||
|
||||
@@ -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<List<GroupMemberEntry.FullMember>> groupMembersChanged = LiveDataUtil.skip(groupMembers, 1);
|
||||
private final LiveData<Integer> groupMemberCount = Transformations.map(groupMembers, List::size);
|
||||
private final Observable<Boolean> shouldShowSpeakerHint = participantsState.map(this::shouldShowSpeakerHint);
|
||||
private final LiveData<Orientation> orientation;
|
||||
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
|
||||
private final LiveData<Integer> controlsRotation;
|
||||
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), m));
|
||||
private final MutableLiveData<WebRtcEphemeralState> 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<Integer> getControlsRotation() {
|
||||
return controlsRotation;
|
||||
}
|
||||
|
||||
public LiveData<Orientation> getOrientation() {
|
||||
return Transformations.distinctUntilChanged(orientation);
|
||||
}
|
||||
|
||||
public LiveData<Pair<Orientation, Boolean>> getOrientationAndLandscapeEnabled() {
|
||||
return LiveDataUtil.combineLatest(orientation, isLandscapeEnabled, Pair::new);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> 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<CallParticipant> callParticipants) {
|
||||
return Stream.of(callParticipants).anyMatch(p -> p.getCallParticipantId().getDemuxId() == CallParticipantId.DEFAULT_ID);
|
||||
}
|
||||
@@ -396,7 +357,8 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
@NonNull Set<SignalAudioManager.AudioDevice> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new WebRtcCallViewModel(deviceOrientationMonitor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SignalAudioManager.AudioDevice> 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<SignalAudioManager.AudioDevice> 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() {
|
||||
|
||||
@@ -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<View>
|
||||
private val callInfoComposeView: ComposeView
|
||||
private val raiseHandComposeView: ComposeView
|
||||
private val callControls: ConstraintLayout
|
||||
private val aboveControlsGuideline: Guideline
|
||||
private val bottomSheetVisibilityListeners = mutableSetOf<BottomSheetVisibilityListener>()
|
||||
private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() }
|
||||
private val toggleCameraDirectionView: View
|
||||
private val bottomSheetVisibilityListeners = mutableSetOf<BottomSheetVisibilityListener>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user