1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.DeviceAsWebcam;
18 
19 import android.annotation.IntRange;
20 import android.content.Context;
21 import android.hardware.SensorManager;
22 import android.hardware.camera2.CameraCharacteristics;
23 import android.hardware.display.DisplayManager;
24 import android.view.Display;
25 import android.view.OrientationEventListener;
26 
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.concurrent.Executor;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 
34 /**
35  * Provider for receiving rotation updates from the {@link SensorManager} when the rotation of
36  * the device has changed.
37  *
38  * <p> This class monitors motion sensor and notifies the listener about physical orientation
39  * changes in the rotation degrees value which can be used to rotate the stream images to the
40  * upright orientation.
41  *
42  * <pre><code>
43  * // Create a provider.
44  * RotationProvider mRotationProvider = new RotationProvider(getApplicationContext());
45  *
46  * // Add listener to receive updates.
47  * mRotationProvider.addListener(rotation -> {
48  *     // Apply the rotation values to the related targets
49  * });
50  *
51  * // Remove when no longer needed.
52  * mRotationProvider.clearListener();
53  * </code></pre>
54  */
55 public final class RotationProvider {
56     private final Object mLock = new Object();
57     private final OrientationEventListener mOrientationListener;
58     private final Map<Listener, ListenerWrapper> mListeners = new HashMap<>();
59     private int mRotation;
60     private int mSensorOrientation;
61 
62     private int mLastDisplayOrientation;
63 
64     /**
65      * Creates a new RotationProvider.
66      *
67      * @param applicationContext the application context used to register
68      *                           {@link OrientationEventListener} or get display rotation.
69      * @param sensorOrientation  the camera sensor orientation value
70      */
RotationProvider(Context applicationContext, int sensorOrientation, int lensFacing)71     public RotationProvider(Context applicationContext, int sensorOrientation, int lensFacing) {
72         mLastDisplayOrientation = applicationContext.getSystemService(DisplayManager.class)
73                 .getDisplay(Display.DEFAULT_DISPLAY).getRotation();
74 
75         // sensor orientation is reported as the clockwise rotation needed for back camera, and
76         // counter clockwise rotation needed for front camera. For consistent logic, always track
77         // clockwise rotation needed in mSensorOrientation.
78         mSensorOrientation = lensFacing == CameraCharacteristics.LENS_FACING_FRONT ?
79                 (360 - sensorOrientation) : sensorOrientation;
80         mRotation = sensorOrientationToRotationDegrees(mLastDisplayOrientation);
81         mOrientationListener = new OrientationEventListener(applicationContext) {
82             @Override
83             public void onOrientationChanged(int orientation) {
84                 if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
85                     // Short-circuit if orientation is unknown. Unknown rotation
86                     // can't be handled so it shouldn't be sent.
87                     return;
88                 }
89 
90                 int newRotation;
91                 int originalRotation;
92                 List<ListenerWrapper> listeners = new ArrayList<>();
93                 // Take a snapshot for thread safety.
94                 synchronized (mLock) {
95                     mLastDisplayOrientation = orientation;
96                     newRotation = sensorOrientationToRotationDegrees(orientation);
97                     originalRotation = mRotation;
98                     if (mRotation != newRotation) {
99                         mRotation = newRotation;
100                         listeners.addAll(mListeners.values());
101                     }
102                 }
103 
104                 if (originalRotation != newRotation) {
105                     for (ListenerWrapper listenerWrapper : listeners) {
106                         listenerWrapper.onRotationChanged(newRotation);
107                     }
108                 }
109             }
110         };
111     }
112 
getRotation()113     public int getRotation() {
114         synchronized (mLock) {
115             return mRotation;
116         }
117     }
118 
119     /**
120      * Sets a {@link Listener} that listens for rotation changes.
121      *
122      * @param executor The executor in which the {@link Listener#onRotationChanged(int)} will be
123      *                 run.
124      * @return false if the device cannot detection rotation changes. In that case, the listener
125      * will not be set.
126      */
addListener(Executor executor, Listener listener)127     public boolean addListener(Executor executor, Listener listener) {
128         synchronized (mLock) {
129             if (!mOrientationListener.canDetectOrientation()) {
130                 return false;
131             }
132             mListeners.put(listener, new ListenerWrapper(listener, executor));
133             mOrientationListener.enable();
134         }
135         return true;
136     }
137 
138     /**
139      * Removes the given {@link Listener} from this object.
140      *
141      * <p> The removed listener will no longer receive rotation updates.
142      */
removeListener(Listener listener)143     public void removeListener(Listener listener) {
144         synchronized (mLock) {
145             ListenerWrapper listenerWrapper = mListeners.get(listener);
146             if (listenerWrapper != null) {
147                 listenerWrapper.disable();
148                 mListeners.remove(listener);
149             }
150             if (mListeners.isEmpty()) {
151                 mOrientationListener.disable();
152             }
153         }
154     }
155 
updateSensorOrientation(int sensorOrientation, int lensFacing)156     public void updateSensorOrientation(int sensorOrientation, int lensFacing) {
157         synchronized (mLock) {
158             // sensor orientation is reported as the clockwise rotation needed for back camera, and
159             // counter clockwise rotation needed for front camera. For consistent logic, always
160             // track clockwise rotation needed in mSensorOrientation.
161             mSensorOrientation = lensFacing == CameraCharacteristics.LENS_FACING_FRONT ?
162                     (360 - sensorOrientation) : sensorOrientation;
163 
164             // Fire callbacks with the new rotation
165             mOrientationListener.onOrientationChanged(mLastDisplayOrientation);
166         }
167     }
168 
169     /**
170      * Converts sensor orientation degrees to the image rotation degrees. Also debounces edge cases
171      * to prevent stream from flipping very quickly while the user is handling the device.
172      *
173      * <p>Currently, the returned value can only be 0 or 180 because DeviceAsWebcam only support
174      * in the landscape mode. The webcam stream images will be rotated to upright orientation when
175      * the device is in the landscape orientation.
176      */
sensorOrientationToRotationDegrees(@ntRangefrom = 0, to = 359) int orientation)177     private int sensorOrientationToRotationDegrees(@IntRange(from = 0, to = 359) int orientation) {
178         synchronized (mLock) {
179             // Orientation is reported as the clockwise angle from device's natural orientation.
180             // Camera sensor orientation is reported as the clockwise angle that the buffer must be
181             // rotated to match device's natural orientation, so the sensor orientation is reported
182             // counter clockwise.
183             int bufferAngle = 360 - mSensorOrientation;
184 
185             // If the angle between the image buffer and device is greater than 90 degrees on either
186             // side, we want to flip the stream.
187             int dAngle = (360 + (bufferAngle - orientation)) % 360;
188 
189             // To prevent stream from wildly flipping around while the user is handling the device,
190             // we debounce values that are too close to the trigger points. "Too close" is being
191             // arbitrarily defined as within 10 degrees.
192             int ident = dAngle / 10;
193             if (ident == 8 || ident == 9
194                     || ident == 26 || ident == 27) {
195                 // orientation too close to 90 or 270; don't change rotation
196                 return mRotation;
197             }
198 
199             // Orientation past the debounce zone. Return ideal rotation
200             if (dAngle >= 90 && dAngle < 270) {
201                 return 180;
202             } else {
203                 return 0;
204             }
205         }
206     }
207 
208     /**
209      * Wrapper of {@link Listener} with the executor and a tombstone flag.
210      */
211     private static class ListenerWrapper {
212         private final Listener mListener;
213         private final Executor mExecutor;
214         private final AtomicBoolean mEnabled;
215 
ListenerWrapper(Listener listener, Executor executor)216         ListenerWrapper(Listener listener, Executor executor) {
217             mListener = listener;
218             mExecutor = executor;
219             mEnabled = new AtomicBoolean(true);
220         }
221 
onRotationChanged(int rotation)222         void onRotationChanged(int rotation) {
223             mExecutor.execute(() -> {
224                 if (mEnabled.get()) {
225                     mListener.onRotationChanged(rotation);
226                 }
227             });
228         }
229 
230         /**
231          * Once disabled, the app will not receive callback even if it has already been posted on
232          * the callback thread.
233          */
disable()234         void disable() {
235             mEnabled.set(false);
236         }
237     }
238 
239     /**
240      * Callback interface to receive rotation updates.
241      */
242     public interface Listener {
243 
244         /**
245          * Called when the physical rotation of the device changes to cause the corresponding
246          * rotation value is changed.
247          *
248          * <p>Currently, the returned value can only be 0 or 180 because DeviceAsWebcam only
249          * support in the landscape mode. The webcam stream images will be rotated to upright
250          * orientation when the device is in the landscape orientation.
251          */
onRotationChanged(int rotation)252         void onRotationChanged(int rotation);
253     }
254 }
255 
256