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);
+    }
+}
+