From 3c096559497cc69b7c721ff630d49eb968b3e9aa Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 15 Jul 2022 13:19:10 -0300 Subject: [PATCH] Fix camera rotation for newer API levels. --- .../camera/view/SignalCameraView.java | 34 +++++-- .../mediasend/AvatarSelectionActivity.java | 5 - .../securesms/mediasend/Camera1Fragment.java | 17 +++- .../securesms/mediasend/CameraFragment.java | 1 - .../securesms/mediasend/CameraXFragment.java | 10 +- .../mediasend/LegacyCameraModels.java | 92 ------------------- .../securesms/mediasend/RotationListener.kt | 49 ++++++++++ .../v2/capture/MediaCaptureFragment.kt | 10 -- 8 files changed, 95 insertions(+), 123 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/LegacyCameraModels.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt diff --git a/app/src/main/java/androidx/camera/view/SignalCameraView.java b/app/src/main/java/androidx/camera/view/SignalCameraView.java index 37c94e9b30..5d4ff28025 100644 --- a/app/src/main/java/androidx/camera/view/SignalCameraView.java +++ b/app/src/main/java/androidx/camera/view/SignalCameraView.java @@ -67,10 +67,13 @@ import androidx.lifecycle.LiveData; import com.google.common.util.concurrent.ListenableFuture; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.mediasend.RotationListener; import java.io.File; import java.util.concurrent.Executor; +import io.reactivex.rxjava3.disposables.Disposable; + /** * A {@link View} that displays a preview of the camera with methods {@link * #takePicture(Executor, OnImageCapturedCallback)}, @@ -134,6 +137,10 @@ public final class SignalCameraView extends FrameLayout { // BEGIN Custom Signal Code Block private Consumer errorConsumer; private Throwable pendingError; + + private RotationListener rotationListener; + private Disposable rotationDisposable; + private RotationListener.Rotation rotation; // END Custom Signal Code Block public SignalCameraView(@NonNull Context context) { @@ -190,6 +197,7 @@ public final class SignalCameraView extends FrameLayout { addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */); // Begin custom signal code block + rotationListener = new RotationListener(context); mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE); mCameraModule = new SignalCameraXModule(this, error -> { if (errorConsumer != null) { @@ -314,6 +322,9 @@ public final class SignalCameraView extends FrameLayout { DisplayManager dpyMgr = (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE); dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper())); + rotationDisposable = rotationListener.getObservable().distinctUntilChanged().subscribe(rotation -> { + this.rotation = rotation; + }); } @Override @@ -322,6 +333,7 @@ public final class SignalCameraView extends FrameLayout { DisplayManager dpyMgr = (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE); dpyMgr.unregisterDisplayListener(mDisplayListener); + rotationDisposable.dispose(); } /** @@ -368,22 +380,28 @@ public final class SignalCameraView extends FrameLayout { super.onLayout(changed, left, top, right, bottom); } + // BEGIN Custom Signal Code Block /** * @return One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, {@link * Surface#ROTATION_180}, {@link Surface#ROTATION_270}. */ int getDisplaySurfaceRotation() { - Display display = getDisplay(); + if (rotation == null) { + Display display = getDisplay(); - // Null when the View is detached. If we were in the middle of a background operation, - // better to not NPE. When the background operation finishes, it'll realize that the camera - // was closed. - if (display == null) { - return 0; + // Null when the View is detached. If we were in the middle of a background operation, + // better to not NPE. When the background operation finishes, it'll realize that the camera + // was closed. + if (display == null) { + return 0; + } + + return display.getRotation(); + } else { + return rotation.getSurfaceRotation(); } - - return display.getRotation(); } + // END Custom Signal Code Block /** * Returns the scale type used to scale the preview. diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index a3d1ed6f34..0c0bcd6a5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -125,11 +125,6 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera transaction.commit(); } - @Override - public int getDisplayRotation() { - return getWindowManager().getDefaultDisplay().getRotation(); - } - @Override public void onCameraCountButtonClicked() { throw new UnsupportedOperationException("Cannot select more than one photo"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java index 28e2507895..a17ab102a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java @@ -53,6 +53,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.io.ByteArrayOutputStream; import java.util.Optional; +import io.reactivex.rxjava3.disposables.Disposable; + /** * Camera capture implemented with the legacy camera API's. Should only be used if sdk < 21. */ @@ -71,6 +73,8 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment, private Controller controller; private OrderEnforcer orderEnforcer; private Camera1Controller.Properties properties; + private RotationListener rotationListener; + private Disposable rotationListenerDisposable; private final Observer> thumbObserver = this::presentRecentItemThumbnail; private boolean isThumbAvailable; @@ -116,6 +120,7 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment, super.onViewCreated(view, savedInstanceState); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); + rotationListener = new RotationListener(requireContext()); cameraPreview = view.findViewById(R.id.camera_preview); controlsContainer = view.findViewById(R.id.camera_controls_container); @@ -161,9 +166,17 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment, orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> { camera.linkSurface(cameraPreview.getSurfaceTexture()); - camera.setScreenRotation(controller.getDisplayRotation()); }); + rotationListenerDisposable = rotationListener.getObservable() + .distinctUntilChanged() + .filter(rotation -> rotation != RotationListener.Rotation.ROTATION_180) + .subscribe(rotation -> { + orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> { + camera.setScreenRotation(rotation.getSurfaceRotation()); + }); + }); + orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); } @@ -171,6 +184,7 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment, @Override public void onPause() { super.onPause(); + rotationListenerDisposable.dispose(); camera.release(); orderEnforcer.reset(); } @@ -232,7 +246,6 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment, @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> camera.setScreenRotation(controller.getDisplayRotation())); orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java index 6ba32eb96a..798764889f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java @@ -53,7 +53,6 @@ public interface CameraFragment { void onVideoCaptured(@NonNull FileDescriptor fd); void onVideoCaptureError(); void onGalleryClicked(); - int getDisplayRotation(); void onCameraCountButtonClicked(); @NonNull LiveData> getMostRecentMediaItem(); @NonNull MediaConstraints getMediaConstraints(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 919b21645d..631a867dfd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -70,11 +70,11 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { private static final String TAG = Log.tag(CameraXFragment.class); private static final String IS_VIDEO_ENABLED = "is_video_enabled"; - private SignalCameraView camera; - private ViewGroup controlsContainer; - private Controller controller; - private View selfieFlash; - private MemoryFileDescriptor videoFileDescriptor; + private SignalCameraView camera; + private ViewGroup controlsContainer; + private Controller controller; + private View selfieFlash; + private MemoryFileDescriptor videoFileDescriptor; private final Observer> thumbObserver = this::presentRecentItemThumbnail; private boolean isThumbAvailable; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/LegacyCameraModels.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/LegacyCameraModels.java deleted file mode 100644 index bae9f92167..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/LegacyCameraModels.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.os.Build; - -import java.util.HashSet; -import java.util.Set; - -public final class LegacyCameraModels { - private static final Set LEGACY_MODELS = new HashSet() {{ - // Pixel 4 - add("Pixel 4"); - add("Pixel 4 XL"); - - // Huawei Mate 10 - add("ALP-L29"); - add("ALP-L09"); - add("ALP-AL00"); - - // Huawei Mate 10 Pro - add("BLA-L29"); - add("BLA-L09"); - add("BLA-AL00"); - add("BLA-A09"); - - // Huawei Mate 20 - add("HMA-L29"); - add("HMA-L09"); - add("HMA-LX9"); - add("HMA-AL00"); - - // Huawei Mate 20 Pro - add("LYA-L09"); - add("LYA-L29"); - add("LYA-AL00"); - add("LYA-AL10"); - add("LYA-TL00"); - add("LYA-L0C"); - - // Huawei P20 - add("EML-L29C"); - add("EML-L09C"); - add("EML-AL00"); - add("EML-TL00"); - add("EML-L29"); - add("EML-L09"); - - // Huawei P20 Pro - add("CLT-L29C"); - add("CLT-L29"); - add("CLT-L09C"); - add("CLT-L09"); - add("CLT-AL00"); - add("CLT-AL01"); - add("CLT-TL01"); - add("CLT-AL00L"); - add("CLT-L04"); - add("HW-01K"); - - // Huawei P30 - add("ELE-L29"); - add("ELE-L09"); - add("ELE-AL00"); - add("ELE-TL00"); - add("ELE-L04"); - - // Huawei P30 Pro - add("VOG-L29"); - add("VOG-L09"); - add("VOG-AL00"); - add("VOG-TL00"); - add("VOG-L04"); - add("VOG-AL10"); - - // Huawei Honor 10 - add("COL-AL10"); - add("COL-L29"); - add("COL-L19"); - - // Samsung Galaxy S6 - add("SM-G920F"); - - // Honor View 10 - add("BLK-L09"); - }}; - - private LegacyCameraModels() { - } - - public static boolean isLegacyCameraModel() { - return LEGACY_MODELS.contains(Build.MODEL); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt new file mode 100644 index 0000000000..8d21f5de27 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.mediasend + +import android.content.Context +import android.view.OrientationEventListener +import android.view.Surface +import io.reactivex.rxjava3.subjects.BehaviorSubject +import io.reactivex.rxjava3.subjects.Subject + +/** + * Utilizes the OrientationEventListener to determine relative surface rotation. + * + * @param context A context, which will be held on to for the lifespan of the listener. + */ +class RotationListener( + context: Context +) : OrientationEventListener(context) { + + private val subject: Subject = BehaviorSubject.create() + + /** + * Observes the stream of orientation changes. This can emit a lot of data, as it does + * not perform any duplication. + */ + val observable = subject + .doOnSubscribe { enable() } + .doOnTerminate { disable() } + + override fun onOrientationChanged(orientation: Int) { + subject.onNext( + when { + orientation == ORIENTATION_UNKNOWN -> Rotation.ROTATION_0 + orientation > 315 || orientation < 45 -> Rotation.ROTATION_0 + orientation < 135 -> Rotation.ROTATION_270 + orientation < 225 -> Rotation.ROTATION_180 + else -> Rotation.ROTATION_90 + } + ) + } + + /** + * Expresses the rotation as a handy enum. + */ + enum class Rotation(val surfaceRotation: Int) { + ROTATION_0(Surface.ROTATION_0), + ROTATION_90(Surface.ROTATION_90), + ROTATION_180(Surface.ROTATION_180), + ROTATION_270(Surface.ROTATION_270) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt index be6cda0a9c..5d255ed71f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.mediasend.v2.capture -import android.os.Build import android.os.Bundle import android.view.View import android.widget.Toast @@ -143,15 +142,6 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme } } - override fun getDisplayRotation(): Int { - return if (Build.VERSION.SDK_INT >= 30) { - requireContext().display?.rotation ?: 0 - } else { - @Suppress("DEPRECATION") - requireActivity().windowManager.defaultDisplay.rotation - } - } - override fun onCameraCountButtonClicked() { val controller = findNavController() captureChildFragment.fadeOutControls {