Rotate webcam stream images to correct orientation
Add new parameter to rotate images when converting to MJpeg format.
Only support to rotate the webcam stream images 180 degrees.
Created a RotationProvider to monitor the orientation changes via
OrientationEventListener and then convert it to the corresponding
rotation degrees value to rotate the images to upright orientation.
Added a CameraInfo class to provide necessary characteristics values of
the working camera.
Bug: 269644311
Test: manual test
Change-Id: I80c274d132d80399fafaeecfbac6a2fd22609768
diff --git a/jni/DeviceAsWebcamNative.cpp b/jni/DeviceAsWebcamNative.cpp
index be9714b..0211d9a 100644
--- a/jni/DeviceAsWebcamNative.cpp
+++ b/jni/DeviceAsWebcamNative.cpp
@@ -64,7 +64,7 @@
(void*)com_android_DeviceAsWebcam_setupServicesAndStartListening},
{"nativeOnDestroy", "()V", (void*)com_android_DeviceAsWebcam_onDestroy},
{"shouldStartServiceNative", "()Z", (void*)com_android_DeviceAsWebcam_shouldStartService},
- {"nativeEncodeImage", "(Landroid/hardware/HardwareBuffer;J)I",
+ {"nativeEncodeImage", "(Landroid/hardware/HardwareBuffer;JI)I",
(void*)com_android_DeviceAsWebcam_encodeImage},
};
@@ -88,8 +88,10 @@
jint DeviceAsWebcamNative::com_android_DeviceAsWebcam_encodeImage(JNIEnv* env, jobject,
jobject hardwareBuffer,
- jlong timestamp) {
- return DeviceAsWebcamServiceManager::kInstance->encodeImage(env, hardwareBuffer, timestamp);
+ jlong timestamp,
+ jint rotation) {
+ return DeviceAsWebcamServiceManager::kInstance->encodeImage(env, hardwareBuffer, timestamp,
+ rotation);
}
jint DeviceAsWebcamNative::com_android_DeviceAsWebcam_setupServicesAndStartListening(JNIEnv* env,
diff --git a/jni/DeviceAsWebcamNative.h b/jni/DeviceAsWebcamNative.h
index 7d0f5a0..5db2932 100644
--- a/jni/DeviceAsWebcamNative.h
+++ b/jni/DeviceAsWebcamNative.h
@@ -42,7 +42,8 @@
// Native implementations of Java Methods.
static jint com_android_DeviceAsWebcam_encodeImage(JNIEnv* env, jobject thiz,
- jobject hardwareBuffer, jlong timestamp);
+ jobject hardwareBuffer, jlong timestamp,
+ jint rotation);
static jint com_android_DeviceAsWebcam_setupServicesAndStartListening(JNIEnv* env,
jobject thiz);
static jboolean com_android_DeviceAsWebcam_shouldStartService(JNIEnv*, jclass);
diff --git a/jni/DeviceAsWebcamServiceManager.cpp b/jni/DeviceAsWebcamServiceManager.cpp
index e0a9a91..97972d3 100644
--- a/jni/DeviceAsWebcamServiceManager.cpp
+++ b/jni/DeviceAsWebcamServiceManager.cpp
@@ -54,7 +54,7 @@
}
int DeviceAsWebcamServiceManager::encodeImage(JNIEnv* env, jobject hardwareBuffer,
- jlong timestamp) {
+ jlong timestamp, jint rotation) {
ALOGV("%s", __FUNCTION__);
std::lock_guard<std::mutex> l(mSerializationLock);
if (!mServiceRunning) {
@@ -62,7 +62,7 @@
return -1;
}
AHardwareBuffer* buffer = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer);
- return mUVCProvider->encodeImage(buffer, timestamp);
+ return mUVCProvider->encodeImage(buffer, timestamp, rotation);
}
void DeviceAsWebcamServiceManager::setStreamConfig(bool mjpeg, uint32_t width, uint32_t height,
diff --git a/jni/DeviceAsWebcamServiceManager.h b/jni/DeviceAsWebcamServiceManager.h
index da26e56..97c281e 100644
--- a/jni/DeviceAsWebcamServiceManager.h
+++ b/jni/DeviceAsWebcamServiceManager.h
@@ -39,7 +39,7 @@
// before any of the functions below it
int setupServicesAndStartListening(JNIEnv* env, jobject javaService);
// Called by Java to encode a frame
- int encodeImage(JNIEnv* env, jobject hardwareBuffer, jlong timestamp);
+ int encodeImage(JNIEnv* env, jobject hardwareBuffer, jlong timestamp, jint rotation);
// Called by native service to set the stream configuration in the Java Service.
void setStreamConfig(bool mjpeg, uint32_t width, uint32_t height, uint32_t fps);
// Called by native service to notify the Java service to start streaming the camera.
diff --git a/jni/Encoder.cpp b/jni/Encoder.cpp
index fefb75f..6bf50fa 100644
--- a/jni/Encoder.cpp
+++ b/jni/Encoder.cpp
@@ -22,6 +22,7 @@
#include <condition_variable>
#include <libyuv/convert.h>
#include <libyuv/convert_from.h>
+#include <libyuv/rotate.h>
#include <log/log.h>
#include <queue>
#include <sched.h>
@@ -292,11 +293,13 @@
int32_t dstYRowStride = mConfig.width;
int32_t dstURowStride = mConfig.width / 2;
int32_t dstVRowStride = mConfig.width / 2;
+ libyuv::RotationMode rotationMode = request.rotationDegrees == 180 ?
+ libyuv::kRotate180 : libyuv::kRotate0;
- return libyuv::Android420ToI420(src.yData, src.yRowStride, src.uData, src.uRowStride, src.vData,
- src.vRowStride, src.uvPixelStride, dstY, dstYRowStride, dstU,
- dstURowStride, dstV, dstVRowStride, mConfig.width,
- mConfig.height);
+ return libyuv::Android420ToI420Rotate(src.yData, src.yRowStride, src.uData, src.uRowStride,
+ src.vData, src.vRowStride, src.uvPixelStride, dstY,
+ dstYRowStride, dstU, dstURowStride, dstV, dstVRowStride,
+ mConfig.width, mConfig.height, rotationMode);
}
void Encoder::encodeToYUYV(EncodeRequest& r) {
diff --git a/jni/Encoder.h b/jni/Encoder.h
index 70cd4f6..fe0d868 100644
--- a/jni/Encoder.h
+++ b/jni/Encoder.h
@@ -36,10 +36,11 @@
struct EncodeRequest {
EncodeRequest() = default;
- EncodeRequest(HardwareBufferDesc& buffer, Buffer* producerBuffer)
- : srcBuffer(buffer), dstBuffer(producerBuffer) {}
+ EncodeRequest(HardwareBufferDesc& buffer, Buffer* producerBuffer, uint32_t rotation)
+ : srcBuffer(buffer), dstBuffer(producerBuffer), rotationDegrees(rotation) {}
HardwareBufferDesc srcBuffer;
Buffer* dstBuffer = nullptr;
+ uint32_t rotationDegrees = 0;
};
struct I420 {
diff --git a/jni/FrameProvider.h b/jni/FrameProvider.h
index 13c83ce..5e50ffe 100644
--- a/jni/FrameProvider.h
+++ b/jni/FrameProvider.h
@@ -42,7 +42,7 @@
virtual void setStreamConfig() = 0;
virtual Status startStreaming() = 0;
virtual Status stopStreaming() = 0;
- virtual Status encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp) = 0;
+ virtual Status encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp, int rotation) = 0;
[[nodiscard]] virtual bool isInited() const { return mInited; }
protected:
diff --git a/jni/SdkFrameProvider.cpp b/jni/SdkFrameProvider.cpp
index c3ef386..4993b0c 100644
--- a/jni/SdkFrameProvider.cpp
+++ b/jni/SdkFrameProvider.cpp
@@ -55,13 +55,14 @@
return Status::OK;
}
-Status SdkFrameProvider::encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp) {
+Status SdkFrameProvider::encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp,
+ int rotation) {
HardwareBufferDesc desc;
if (getHardwareBufferDescFromHardwareBuffer(hardwareBuffer, desc) != Status::OK) {
ALOGE("%s Couldn't get hardware buffer descriptor", __FUNCTION__);
return Status::ERROR;
}
- return encodeImage(desc, timestamp);
+ return encodeImage(desc, timestamp, rotation);
}
Status SdkFrameProvider::getHardwareBufferDescFromHardwareBuffer(AHardwareBuffer* hardwareBuffer,
@@ -112,7 +113,7 @@
return Status::OK;
}
-Status SdkFrameProvider::encodeImage(HardwareBufferDesc desc, jlong timestamp) {
+Status SdkFrameProvider::encodeImage(HardwareBufferDesc desc, jlong timestamp, jint rotation) {
Buffer* producerBuffer = mBufferProducer->getFreeBufferIfAvailable();
if (producerBuffer == nullptr) {
// Not available so don't compress
@@ -123,7 +124,7 @@
producerBuffer->setTimestamp(static_cast<uint64_t>(timestamp));
// send to the Encoder.
- EncodeRequest encodeRequest(desc, producerBuffer);
+ EncodeRequest encodeRequest(desc, producerBuffer, rotation);
mEncoder->queueRequest(encodeRequest);
return Status::OK;
}
diff --git a/jni/SdkFrameProvider.h b/jni/SdkFrameProvider.h
index 900811a..27c1842 100644
--- a/jni/SdkFrameProvider.h
+++ b/jni/SdkFrameProvider.h
@@ -36,7 +36,7 @@
Status startStreaming() override;
Status stopStreaming() final ;
- Status encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp) override;
+ Status encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp, int rotation) override;
// EncoderCallback overrides
void onEncoded(Buffer* producerBuffer, HardwareBufferDesc& hardwareBufferDesc,
@@ -45,7 +45,7 @@
private:
Status getHardwareBufferDescFromHardwareBuffer(AHardwareBuffer* hardwareBuffer,
HardwareBufferDesc& ret);
- Status encodeImage(HardwareBufferDesc desc, jlong timestamp);
+ Status encodeImage(HardwareBufferDesc desc, jlong timestamp, jint rotation);
void releaseHardwareBuffer(const HardwareBufferDesc& desc);
std::mutex mMapLock;
diff --git a/jni/UVCProvider.cpp b/jni/UVCProvider.cpp
index 872dd10..f9e4509 100644
--- a/jni/UVCProvider.cpp
+++ b/jni/UVCProvider.cpp
@@ -737,12 +737,12 @@
return;
}
}
-Status UVCProvider::UVCDevice::encodeImage(AHardwareBuffer* buffer, long timestamp) {
+Status UVCProvider::UVCDevice::encodeImage(AHardwareBuffer* buffer, long timestamp, int rotation) {
if (mFrameProvider == nullptr) {
ALOGE("%s: encodeImage called but there is no frame provider active", __FUNCTION__);
return Status::ERROR;
}
- return mFrameProvider->encodeImage(buffer, timestamp);
+ return mFrameProvider->encodeImage(buffer, timestamp, rotation);
}
void UVCProvider::processUVCEvent() {
@@ -821,12 +821,12 @@
}
}
-int UVCProvider::encodeImage(AHardwareBuffer* buffer, long timestamp) {
+int UVCProvider::encodeImage(AHardwareBuffer* buffer, long timestamp, int rotation) {
if (mUVCDevice == nullptr) {
ALOGE("%s: Request to encode Image without UVCDevice Running.", __FUNCTION__);
return -1;
}
- return mUVCDevice->encodeImage(buffer, timestamp) == Status::OK ? 0 : -1;
+ return mUVCDevice->encodeImage(buffer, timestamp, rotation) == Status::OK ? 0 : -1;
}
void UVCProvider::startUVCListenerThread() {
diff --git a/jni/UVCProvider.h b/jni/UVCProvider.h
index 9002554..f398aac 100644
--- a/jni/UVCProvider.h
+++ b/jni/UVCProvider.h
@@ -106,7 +106,7 @@
void stopService();
- int encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp);
+ int encodeImage(AHardwareBuffer* hardwareBuffer, long timestamp, jint rotation);
void watchStreamEvent();
@@ -134,7 +134,7 @@
void processStreamOnEvent();
void processStreamOffEvent();
void processStreamEvent();
- Status encodeImage(AHardwareBuffer* buffer, long timestamp);
+ Status encodeImage(AHardwareBuffer* buffer, long timestamp, int rotation);
// BufferCreatorAndDestroyer overrides
Status allocateAndMapBuffers(
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6ec6008..b4f9d57 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -23,5 +23,5 @@
<string name="zoom_ratio_description">Zoom ratio value</string>
<string name="zoom_ratio">%.2fx</string>
<!-- Accessibility description for the toggle camera image button -->
- <string name="toggle_camera_button_description">Toggle camera button</string>
+ <string name="toggle_camera_button_description">Toggle camera direction button</string>
</resources>
diff --git a/src/com/android/DeviceAsWebcam/CameraController.java b/src/com/android/DeviceAsWebcam/CameraController.java
index 263fb73..6afbdea 100644
--- a/src/com/android/DeviceAsWebcam/CameraController.java
+++ b/src/com/android/DeviceAsWebcam/CameraController.java
@@ -176,8 +176,8 @@
HardwareBuffer hardwareBuffer = image.getHardwareBuffer();
mImageMap.put(ts, new ImageAndBuffer(image, hardwareBuffer));
// Callback into DeviceAsWebcamFgService to encode image
- if ((!mStartCaptureWebcamStream.get()) ||
- (service.nativeEncodeImage(hardwareBuffer, ts) != 0)) {
+ if ((!mStartCaptureWebcamStream.get()) || (service.nativeEncodeImage(
+ hardwareBuffer, ts, getCurrentRotation()) != 0)) {
if (VERBOSE) {
Log.v(TAG,
"Couldn't get buffer immediately, returning image images. "
@@ -191,6 +191,8 @@
};
private volatile float mZoomRatio = 1.0f;
+ private RotationProvider mRotationProvider;
+ private CameraInfo mCameraInfo = null;
public CameraController(Context context, WeakReference<DeviceAsWebcamFgService> serviceWeak) {
mContext = context;
@@ -203,6 +205,12 @@
mCameraManager = mContext.getSystemService(CameraManager.class);
refreshLensFacingCameraIds();
mCameraId = mBackCameraId != null ? mBackCameraId : mFrontCameraId;
+ mCameraInfo = createCameraInfo(mCameraId);
+ mRotationProvider = new RotationProvider(context.getApplicationContext(),
+ mCameraInfo.getSensorOrientation());
+ // Adds an empty listener to enable the RotationProvider so that we can get the rotation
+ // degrees info to rotate the webcam stream images.
+ mRotationProvider.addListener(mThreadPoolExecutor, rotation -> {});
}
private void refreshLensFacingCameraIds() {
@@ -212,13 +220,12 @@
return;
}
for (String cameraId : cameraIdList) {
- CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(
- cameraId);
- if (mBackCameraId == null && characteristics.get(CameraCharacteristics.LENS_FACING)
- == CameraMetadata.LENS_FACING_BACK) {
+ int lensFacing = getCameraCharacteristic(cameraId,
+ CameraCharacteristics.LENS_FACING);
+ if (mBackCameraId == null && lensFacing == CameraMetadata.LENS_FACING_BACK) {
mBackCameraId = cameraId;
- } else if (mFrontCameraId == null && characteristics.get(
- CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
+ } else if (mFrontCameraId == null
+ && lensFacing == CameraMetadata.LENS_FACING_FRONT) {
mFrontCameraId = cameraId;
}
}
@@ -227,6 +234,26 @@
}
}
+ private CameraInfo createCameraInfo(String cameraId) {
+ return cameraId == null ? null : new CameraInfo(
+ getCameraCharacteristic(cameraId, CameraCharacteristics.LENS_FACING),
+ getCameraCharacteristic(cameraId, CameraCharacteristics.SENSOR_ORIENTATION),
+ getCameraCharacteristic(cameraId, CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
+ );
+ }
+
+ private <T> T getCameraCharacteristic(String cameraId, CameraCharacteristics.Key<T> key) {
+ try {
+ CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(
+ cameraId);
+ return characteristics.get(key);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Failed to get" + key.getName() + "characteristics for camera " + cameraId
+ + ".");
+ }
+ return null;
+ }
+
public void setWebcamStreamConfig(boolean mjpeg, int width, int height, int fps) {
if (VERBOSE) {
Log.v(TAG, "Set stream config service : mjpeg ? " + mjpeg + " width" + width +
@@ -545,25 +572,10 @@
}
/**
- * Returns the available zoom ratio range of the working camera.
- *
- * @return the zoom ratio range is retrieved from {@link CameraCharacteristics} with
- * {@link CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE} which is supported since Android 11
- * . The returned value might be null when failed to obtain it.
+ * Returns the {@link CameraInfo} of the working camera.
*/
- public Range<Float> getZoomRatioRange() {
- if (mCameraId == null) {
- Log.e(TAG, "No camera is found on the device.");
- return null;
- }
- try {
- CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(
- mCameraId);
- return characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
- } catch (CameraAccessException e) {
- Log.e(TAG, "Failed to get zoom ratio range for camera " + mCameraId + ".", e);
- }
- return null;
+ public CameraInfo getCameraInfo() {
+ return mCameraInfo;
}
/**
@@ -617,6 +629,7 @@
} else {
mCameraId = mBackCameraId;
}
+ mCameraInfo = createCameraInfo(mCameraId);
switch (mCurrentState) {
case WEBCAM_STREAMING:
setupWebcamOnlyStreamAndOpenCameraLocked();
@@ -633,6 +646,13 @@
});
}
+ /**
+ * Returns current rotation degrees value.
+ */
+ public int getCurrentRotation() {
+ return mRotationProvider.getRotation();
+ }
+
private static class ImageAndBuffer {
public Image image;
public HardwareBuffer buffer;
diff --git a/src/com/android/DeviceAsWebcam/CameraInfo.java b/src/com/android/DeviceAsWebcam/CameraInfo.java
new file mode 100644
index 0000000..91939ee
--- /dev/null
+++ b/src/com/android/DeviceAsWebcam/CameraInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.DeviceAsWebcam;
+
+import android.util.Range;
+
+/**
+ * A class for providing camera related information.
+ */
+public class CameraInfo {
+ private final int mLensFacing;
+ private final int mSensorOrientation;
+ private final Range<Float> mZoomRatioRange;
+ public CameraInfo(int lensFacing, int sensorOrientation, Range<Float> zoomRatioRange) {
+ mLensFacing = lensFacing;
+ mSensorOrientation = sensorOrientation;
+ mZoomRatioRange = zoomRatioRange;
+ }
+
+ /**
+ * Returns lens facing.
+ */
+ public int getLensFacing() {
+ return mLensFacing;
+ }
+
+ /**
+ * Returns sensor orientation characteristics value.
+ */
+ public int getSensorOrientation() {
+ return mSensorOrientation;
+ }
+
+ /**
+ * Returns zoom ratio range characteristics value.
+ */
+ public Range<Float> getZoomRatioRange() {
+ return mZoomRatioRange;
+ }
+}
diff --git a/src/com/android/DeviceAsWebcam/DeviceAsWebcamFgService.java b/src/com/android/DeviceAsWebcam/DeviceAsWebcamFgService.java
index 58dbaef..ba3b40d 100644
--- a/src/com/android/DeviceAsWebcam/DeviceAsWebcamFgService.java
+++ b/src/com/android/DeviceAsWebcam/DeviceAsWebcamFgService.java
@@ -30,7 +30,6 @@
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
-import android.util.Range;
import android.util.Size;
import androidx.core.app.NotificationCompat;
@@ -200,18 +199,15 @@
}
/**
- * Returns the available zoom ratio range of the working camera.
- *
- * @return the zoom ratio range retrieved from the camera characteristic or null when failed
- * to obtain the value.
+ * Returns the {@link CameraInfo} of the working camera.
*/
- public Range<Float> getZoomRatioRange() {
+ public CameraInfo getCameraInfo() {
synchronized (mServiceLock) {
if (!mServiceRunning) {
- Log.e(TAG, "getZoomRatioRange called after Service was destroyed.");
+ Log.e(TAG, "getCameraInfo called after Service was destroyed.");
return null;
}
- return mCameraController.getZoomRatioRange();
+ return mCameraController.getCameraInfo();
}
}
@@ -347,7 +343,7 @@
* @param timestamp timestamp associated with the buffer which uniquely identifies the buffer
* @return 0 if buffer was successfully queued for encoding. non-0 otherwise.
*/
- public native int nativeEncodeImage(HardwareBuffer buffer, long timestamp);
+ public native int nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation);
/**
* Called by {@link #onDestroy} to give the JNI code a chance to clean up before the service
diff --git a/src/com/android/DeviceAsWebcam/DeviceAsWebcamPreview.java b/src/com/android/DeviceAsWebcam/DeviceAsWebcamPreview.java
index 415ce91..00fe63a 100644
--- a/src/com/android/DeviceAsWebcam/DeviceAsWebcamPreview.java
+++ b/src/com/android/DeviceAsWebcam/DeviceAsWebcamPreview.java
@@ -157,11 +157,11 @@
@SuppressLint("ClickableViewAccessibility")
private void setupZoomUiControl() {
- if (mLocalFgService == null) {
+ if (mLocalFgService == null || mLocalFgService.getCameraInfo() == null) {
return;
}
- Range<Float> zoomRatioRange = mLocalFgService.getZoomRatioRange();
+ Range<Float> zoomRatioRange = mLocalFgService.getCameraInfo().getZoomRatioRange();
if (zoomRatioRange == null) {
return;
@@ -268,6 +268,6 @@
mLocalFgService.toggleCamera();
mMotionEventToZoomRatioConverter.resetWithNewRange(
- mLocalFgService.getZoomRatioRange());
+ mLocalFgService.getCameraInfo().getZoomRatioRange());
}
}
diff --git a/src/com/android/DeviceAsWebcam/RotationProvider.java b/src/com/android/DeviceAsWebcam/RotationProvider.java
new file mode 100644
index 0000000..1fe57f4
--- /dev/null
+++ b/src/com/android/DeviceAsWebcam/RotationProvider.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.DeviceAsWebcam;
+
+import android.annotation.IntRange;
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provider for receiving rotation updates from the {@link SensorManager} when the rotation of
+ * the device has changed.
+ *
+ * <p> This class monitors motion sensor and notifies the listener about physical orientation
+ * changes in the rotation degrees value which can be used to rotate the stream images to the
+ * upright orientation.
+ *
+ * <pre><code>
+ * // Create a provider.
+ * RotationProvider mRotationProvider = new RotationProvider(getApplicationContext());
+ *
+ * // Add listener to receive updates.
+ * mRotationProvider.addListener(rotation -> {
+ * // Apply the rotation values to the related targets
+ * });
+ *
+ * // Remove when no longer needed.
+ * mRotationProvider.clearListener();
+ * </code></pre>
+ */
+public final class RotationProvider {
+
+ private final Object mLock = new Object();
+ private final OrientationEventListener mOrientationListener;
+ private final Map<Listener, ListenerWrapper> mListeners = new HashMap<>();
+ private int mRotation;
+ private int mSensorOrientation;
+
+ /**
+ * Creates a new RotationProvider.
+ *
+ * @param applicationContext the application context used to register
+ * {@link OrientationEventListener} or get display rotation.
+ * @param sensorOrientation the camera sensor orientation value
+ */
+ public RotationProvider(Context applicationContext, int sensorOrientation) {
+ int displayRotation = applicationContext.getSystemService(DisplayManager.class).getDisplay(
+ Display.DEFAULT_DISPLAY).getRotation();
+ mRotation = displayRotation == Surface.ROTATION_270 ? 180 : 0;
+ mSensorOrientation = sensorOrientation;
+ mOrientationListener = new OrientationEventListener(applicationContext) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ // Short-circuit if orientation is unknown. Unknown rotation
+ // can't be handled so it shouldn't be sent.
+ return;
+ }
+
+ int newRotation = sensorOrientationToRotationDegrees(orientation);
+ int originalRotation;
+ List<ListenerWrapper> listeners = new ArrayList<>();
+ // Take a snapshot for thread safety.
+ synchronized (mLock) {
+ originalRotation = mRotation;
+ if (mRotation != newRotation) {
+ mRotation = newRotation;
+ listeners.addAll(mListeners.values());
+ }
+ }
+
+ if (originalRotation != newRotation) {
+ if (!listeners.isEmpty()) {
+ for (ListenerWrapper listenerWrapper : listeners) {
+ listenerWrapper.onRotationChanged(newRotation);
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public int getRotation() {
+ synchronized (mLock) {
+ return mRotation;
+ }
+ }
+
+ /**
+ * Sets a {@link Listener} that listens for rotation changes.
+ *
+ * @param executor The executor in which the {@link Listener#onRotationChanged(int)} will be
+ * run.
+ * @return false if the device cannot detection rotation changes. In that case, the listener
+ * will not be set.
+ */
+ public boolean addListener(Executor executor, Listener listener) {
+ synchronized (mLock) {
+ if (!mOrientationListener.canDetectOrientation()) {
+ return false;
+ }
+ mListeners.put(listener, new ListenerWrapper(listener, executor));
+ mOrientationListener.enable();
+ }
+ return true;
+ }
+
+ /**
+ * Removes the given {@link Listener} from this object.
+ *
+ * <p> The removed listener will no longer receive rotation updates.
+ */
+ public void removeListener(Listener listener) {
+ synchronized (mLock) {
+ ListenerWrapper listenerWrapper = mListeners.get(listener);
+ if (listenerWrapper != null) {
+ listenerWrapper.disable();
+ mListeners.remove(listener);
+ }
+ if (mListeners.isEmpty()) {
+ mOrientationListener.disable();
+ }
+ }
+ }
+
+ /**
+ * Converts sensor orientation degrees to the image rotation degrees.
+ *
+ * <p>Currently, the returned value can only be 0 or 180 because DeviceAsWebcam only support
+ * in the landscape mode. The webcam stream images will be rotated to upright orientation when
+ * the device is in the landscape orientation.
+ */
+ private int sensorOrientationToRotationDegrees(@IntRange(from = 0, to = 359) int orientation) {
+ if ((mSensorOrientation % 180 == 90 && orientation >= 45 && orientation < 135) || (
+ mSensorOrientation % 180 == 0 && orientation >= 135 && orientation < 225)) {
+ return 180;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Wrapper of {@link Listener} with the executor and a tombstone flag.
+ */
+ private static class ListenerWrapper {
+ private final Listener mListener;
+ private final Executor mExecutor;
+ private final AtomicBoolean mEnabled;
+
+ ListenerWrapper(Listener listener, Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ mEnabled = new AtomicBoolean(true);
+ }
+
+ void onRotationChanged(int rotation) {
+ mExecutor.execute(() -> {
+ if (mEnabled.get()) {
+ mListener.onRotationChanged(rotation);
+ }
+ });
+ }
+
+ /**
+ * Once disabled, the app will not receive callback even if it has already been posted on
+ * the callback thread.
+ */
+ void disable() {
+ mEnabled.set(false);
+ }
+ }
+
+ /**
+ * Callback interface to receive rotation updates.
+ *
+ * <p>Currently, CameraController only sets an empty listener to enable the RotationProvider's
+ * orientation listener. In the coming CLs, a real implementation will be set to notify the
+ * preview activity to rotate the UI controls to correct orientation.
+ */
+ public interface Listener {
+
+ /**
+ * Called when the physical rotation of the device changes to cause the corresponding
+ * rotation value is changed.
+ *
+ * <p>Currently, the returned value can only be 0 or 180 because DeviceAsWebcam only
+ * support in the landscape mode. The webcam stream images will be rotated to upright
+ * orientation when the device is in the landscape orientation.
+ */
+ void onRotationChanged(int rotation);
+ }
+}
+