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