Apply proper rotation to buttons and video in landscape.

This commit is contained in:
Alex Hart
2021-01-29 16:22:01 -04:00
committed by Greyson Parrelli
parent e6e8786d86
commit 2678a00781
19 changed files with 326 additions and 35 deletions

View File

@@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.components.sensors;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
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 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<>();
public DeviceOrientationMonitor(@NonNull Context context) {
this.sensorManager = ServiceUtil.getSensorManager(context);
}
@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() {
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
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) {
}
}
}

View File

@@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.components.sensors;
import androidx.annotation.NonNull;
public enum Orientation {
PORTRAIT_BOTTOM_EDGE(0),
LANDSCAPE_LEFT_EDGE(90),
LANDSCAPE_RIGHT_EDGE(270);
private final int degrees;
Orientation(int degrees) {
this.degrees = degrees;
}
public int getDegrees() {
return degrees;
}
public static @NonNull Orientation fromDegrees(int degrees) {
for (Orientation orientation : Orientation.values()) {
if (orientation.degrees == degrees) {
return orientation;
}
}
return PORTRAIT_BOTTOM_EDGE;
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
public final class OrientationAwareVideoSink implements VideoSink {
private final VideoSink delegate;
public OrientationAwareVideoSink(@NonNull VideoSink delegate) {
this.delegate = delegate;
}
@Override
public void onFrame(VideoFrame videoFrame) {
if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth()) {
delegate.onFrame(new VideoFrame(videoFrame.getBuffer(), 270, videoFrame.getTimestampNs()));
} else {
delegate.onFrame(videoFrame);
}
}
}

View File

@@ -120,6 +120,7 @@ public class WebRtcCallView extends FrameLayout {
private final Set<View> topViews = new HashSet<>();
private final Set<View> visibleViewSet = new HashSet<>();
private final Set<View> adjustableMarginsSet = new HashSet<>();
private final Set<View> rotatableControls = new HashSet<>();
private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> {
@@ -251,6 +252,16 @@ public class WebRtcCallView extends FrameLayout {
controlsListener.onCancelStartCall();
}
});
rotatableControls.add(hangup);
rotatableControls.add(answer);
rotatableControls.add(answerWithAudio);
rotatableControls.add(audioToggle);
rotatableControls.add(micToggle);
rotatableControls.add(videoToggle);
rotatableControls.add(cameraDirectionToggle);
rotatableControls.add(decline);
rotatableControls.add(smallLocalRender.findViewById(R.id.call_participant_mic_muted));
}
@Override
@@ -288,6 +299,12 @@ public class WebRtcCallView extends FrameLayout {
cancelFadeOut();
}
public void rotateControls(int degrees) {
for (View view : rotatableControls) {
view.animate().rotation(degrees);
}
}
public void setControlsListener(@Nullable ControlsListener controlsListener) {
this.controlsListener = controlsListener;
}

View File

@@ -10,9 +10,12 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
@@ -31,23 +34,25 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
private final LiveData<Orientation> orientation;
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
@@ -61,6 +66,20 @@ public class WebRtcCallViewModel extends ViewModel {
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) {
orientation = LiveDataUtil.combineLatest(deviceOrientationMonitor.getOrientation(), webRtcControls, (deviceOrientation, controls) -> {
if (controls.canRotateControls()) {
return deviceOrientation;
} else {
return Orientation.PORTRAIT_BOTTOM_EDGE;
}
});
}
public LiveData<Orientation> getOrientation() {
return Transformations.distinctUntilChanged(orientation);
}
public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
}
@@ -394,4 +413,18 @@ 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)));
}
}
}

View File

@@ -51,6 +51,10 @@ public final class WebRtcControls {
this.participantLimit = participantLimit;
}
boolean canRotateControls() {
return !isGroupCall();
}
boolean displayErrorControls() {
return isError();
}