From ddc70c75f35de7be6752b9138574445216c3aa0c Mon Sep 17 00:00:00 2001 From: Austin Borger Date: Thu, 17 Nov 2022 17:02:09 -0800 Subject: [PATCH] DO NOT MERGE CameraManager: Provide flag for overriding camera output to portrait. Apps commonly do not handle landscape orientation cameras correctly. In order to prevent stretching and rotation issues in these apps, this patch adds a flag to override the behavior of these landscape cameras to produce a portrait image instead by changing the SENSOR_ORIENTATION reported by CameraCharacteristics and applying a 90 degree rotate and crop. It also adds a CompatChanges entry which will track which apps require this treatment. Test: Ran on foldable device with several camera apps to verify behavior. Bug: 250678880 Change-Id: I92b6f227916b97828ab6a7f856eea5a1e427644d --- core/api/test-current.txt | 1 + core/java/android/hardware/Camera.java | 24 ++- .../hardware/camera2/CameraManager.java | 43 +++++- core/jni/android_hardware_Camera.cpp | 144 ++++++------------ .../integration/CameraBinderTest.java | 9 +- .../integration/CameraDeviceBinderTest.java | 5 +- 6 files changed, 116 insertions(+), 110 deletions(-) diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 93c0c4d7a0242..70a23cdf106ba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1139,6 +1139,7 @@ package android.hardware.camera2 { public final class CameraManager { method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; + field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L } public abstract static class CameraManager.AvailabilityCallback { diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 3bdd39f5d7d76..5291d2b738918 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -29,12 +29,14 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.ActivityThread; import android.app.AppOpsManager; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.ImageFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraManager; import android.media.AudioAttributes; import android.media.IAudioService; import android.os.Build; @@ -45,6 +47,7 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; @@ -281,6 +284,14 @@ public class Camera { */ public native static int getNumberOfCameras(); + private static final boolean sLandscapeToPortrait = + SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false); + + private static boolean shouldOverrideToPortrait() { + return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT) + && sLandscapeToPortrait; + } + /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. @@ -290,7 +301,9 @@ public class Camera { * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { - _getCameraInfo(cameraId, cameraInfo); + boolean overrideToPortrait = shouldOverrideToPortrait(); + + _getCameraInfo(cameraId, overrideToPortrait, cameraInfo); IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); IAudioService audioService = IAudioService.Stub.asInterface(b); try { @@ -303,7 +316,8 @@ public class Camera { Log.e(TAG, "Audio service is unavailable for queries"); } } - private native static void _getCameraInfo(int cameraId, CameraInfo cameraInfo); + private native static void _getCameraInfo(int cameraId, boolean overrideToPortrait, + CameraInfo cameraInfo); /** * Information about a camera @@ -484,8 +498,9 @@ public class Camera { mEventHandler = null; } + boolean overrideToPortrait = shouldOverrideToPortrait(); return native_setup(new WeakReference(this), cameraId, - ActivityThread.currentOpPackageName()); + ActivityThread.currentOpPackageName(), overrideToPortrait); } /** used by Camera#open, Camera#open(int) */ @@ -555,7 +570,8 @@ public class Camera { } @UnsupportedAppUsage - private native int native_setup(Object cameraThis, int cameraId, String packageName); + private native int native_setup(Object cameraThis, int cameraId, String packageName, + boolean overrideToPortrait); private native final void native_release(); diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index dff2f7ed1cf31..7fed2200d6064 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -23,6 +23,10 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; @@ -103,6 +107,24 @@ public final class CameraManager { "android.permission.CAMERA_OPEN_CLOSE_LISTENER"; private final boolean mHasOpenCloseListenerPermission; + /** + * Force camera output to be rotated to portrait orientation on landscape cameras. + * Many apps do not handle this situation and display stretched images otherwise. + * @hide + */ + @ChangeId + @Overridable + @Disabled + @TestApi + public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; + + /** + * System property for allowing the above + * @hide + */ + public static final String LANDSCAPE_TO_PORTRAIT_PROP = + "camera.enable_landscape_to_portrait"; + /** * @hide */ @@ -520,7 +542,8 @@ public final class CameraManager { for (String physicalCameraId : physicalCameraIds) { CameraMetadataNative physicalCameraInfo = cameraService.getCameraCharacteristics(physicalCameraId, - mContext.getApplicationInfo().targetSdkVersion); + mContext.getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); StreamConfiguration[] configs = physicalCameraInfo.get( CameraCharacteristics. SCALER_PHYSICAL_CAMERA_MULTI_RESOLUTION_STREAM_CONFIGURATIONS); @@ -579,8 +602,9 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); + boolean overrideToPortrait = shouldOverrideToPortrait(); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, - mContext.getApplicationInfo().targetSdkVersion); + mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { info.setCameraId(Integer.parseInt(cameraId)); } catch (NumberFormatException e) { @@ -697,9 +721,12 @@ public final class CameraManager { ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); } + + boolean overrideToPortrait = shouldOverrideToPortrait(); cameraUser = cameraService.connectDevice(callbacks, cameraId, - mContext.getOpPackageName(), mContext.getAttributionTag(), uid, - oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion); + mContext.getOpPackageName(), mContext.getAttributionTag(), uid, + oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, + overrideToPortrait); } catch (ServiceSpecificException e) { if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) { throw new AssertionError("Should've gone down the shim path"); @@ -1127,6 +1154,11 @@ public final class CameraManager { return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId); } + private static boolean shouldOverrideToPortrait() { + return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT) + && CameraManagerGlobal.sLandscapeToPortrait; + } + /** * A callback for camera devices becoming available or unavailable to open. * @@ -1573,6 +1605,9 @@ public final class CameraManager { public static final boolean sCameraServiceDisabled = SystemProperties.getBoolean("config.disable_cameraservice", false); + public static final boolean sLandscapeToPortrait = + SystemProperties.getBoolean(LANDSCAPE_TO_PORTRAIT_PROP, false); + public static CameraManagerGlobal get() { return gCameraManager; } diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 365a18d174c9e..a8abe50a97554 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -529,9 +529,8 @@ static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz return Camera::getNumberOfCameras(); } -static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, - jint cameraId, jobject info_obj) -{ +static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jint cameraId, + jboolean overrideToPortrait, jobject info_obj) { CameraInfo cameraInfo; if (cameraId >= Camera::getNumberOfCameras() || cameraId < 0) { ALOGE("%s: Unknown camera ID %d", __FUNCTION__, cameraId); @@ -539,7 +538,7 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, return; } - status_t rc = Camera::getCameraInfo(cameraId, &cameraInfo); + status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo); if (rc != NO_ERROR) { jniThrowRuntimeException(env, "Fail to get camera info"); return; @@ -555,9 +554,9 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, } // connect to camera service -static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, - jobject weak_this, jint cameraId, jstring clientPackageName) -{ +static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this, + jint cameraId, jstring clientPackageName, + jboolean overrideToPortrait) { // Convert jstring to String16 const char16_t *rawClientName = reinterpret_cast( env->GetStringChars(clientPackageName, NULL)); @@ -567,8 +566,9 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, reinterpret_cast(rawClientName)); int targetSdkVersion = android_get_application_target_sdk_version(); - sp camera = Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, - Camera::USE_CALLING_PID, targetSdkVersion); + sp camera = + Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID, + targetSdkVersion, overrideToPortrait); if (camera == NULL) { return -EACCES; } @@ -596,7 +596,7 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, // Update default display orientation in case the sensor is reverse-landscape CameraInfo cameraInfo; - status_t rc = Camera::getCameraInfo(cameraId, &cameraInfo); + status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo); if (rc != NO_ERROR) { ALOGE("%s: getCameraInfo error: %d", __FUNCTION__, rc); return rc; @@ -1051,93 +1051,43 @@ static int32_t android_hardware_Camera_getAudioRestriction( //------------------------------------------------- static const JNINativeMethod camMethods[] = { - { "getNumberOfCameras", - "()I", - (void *)android_hardware_Camera_getNumberOfCameras }, - { "_getCameraInfo", - "(ILandroid/hardware/Camera$CameraInfo;)V", - (void*)android_hardware_Camera_getCameraInfo }, - { "native_setup", - "(Ljava/lang/Object;ILjava/lang/String;)I", - (void*)android_hardware_Camera_native_setup }, - { "native_release", - "()V", - (void*)android_hardware_Camera_release }, - { "setPreviewSurface", - "(Landroid/view/Surface;)V", - (void *)android_hardware_Camera_setPreviewSurface }, - { "setPreviewTexture", - "(Landroid/graphics/SurfaceTexture;)V", - (void *)android_hardware_Camera_setPreviewTexture }, - { "setPreviewCallbackSurface", - "(Landroid/view/Surface;)V", - (void *)android_hardware_Camera_setPreviewCallbackSurface }, - { "startPreview", - "()V", - (void *)android_hardware_Camera_startPreview }, - { "_stopPreview", - "()V", - (void *)android_hardware_Camera_stopPreview }, - { "previewEnabled", - "()Z", - (void *)android_hardware_Camera_previewEnabled }, - { "setHasPreviewCallback", - "(ZZ)V", - (void *)android_hardware_Camera_setHasPreviewCallback }, - { "_addCallbackBuffer", - "([BI)V", - (void *)android_hardware_Camera_addCallbackBuffer }, - { "native_autoFocus", - "()V", - (void *)android_hardware_Camera_autoFocus }, - { "native_cancelAutoFocus", - "()V", - (void *)android_hardware_Camera_cancelAutoFocus }, - { "native_takePicture", - "(I)V", - (void *)android_hardware_Camera_takePicture }, - { "native_setParameters", - "(Ljava/lang/String;)V", - (void *)android_hardware_Camera_setParameters }, - { "native_getParameters", - "()Ljava/lang/String;", - (void *)android_hardware_Camera_getParameters }, - { "reconnect", - "()V", - (void*)android_hardware_Camera_reconnect }, - { "lock", - "()V", - (void*)android_hardware_Camera_lock }, - { "unlock", - "()V", - (void*)android_hardware_Camera_unlock }, - { "startSmoothZoom", - "(I)V", - (void *)android_hardware_Camera_startSmoothZoom }, - { "stopSmoothZoom", - "()V", - (void *)android_hardware_Camera_stopSmoothZoom }, - { "setDisplayOrientation", - "(I)V", - (void *)android_hardware_Camera_setDisplayOrientation }, - { "_enableShutterSound", - "(Z)Z", - (void *)android_hardware_Camera_enableShutterSound }, - { "_startFaceDetection", - "(I)V", - (void *)android_hardware_Camera_startFaceDetection }, - { "_stopFaceDetection", - "()V", - (void *)android_hardware_Camera_stopFaceDetection}, - { "enableFocusMoveCallback", - "(I)V", - (void *)android_hardware_Camera_enableFocusMoveCallback}, - { "setAudioRestriction", - "(I)V", - (void *)android_hardware_Camera_setAudioRestriction}, - { "getAudioRestriction", - "()I", - (void *)android_hardware_Camera_getAudioRestriction}, + {"getNumberOfCameras", "()I", (void *)android_hardware_Camera_getNumberOfCameras}, + {"_getCameraInfo", "(IZLandroid/hardware/Camera$CameraInfo;)V", + (void *)android_hardware_Camera_getCameraInfo}, + {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;Z)I", + (void *)android_hardware_Camera_native_setup}, + {"native_release", "()V", (void *)android_hardware_Camera_release}, + {"setPreviewSurface", "(Landroid/view/Surface;)V", + (void *)android_hardware_Camera_setPreviewSurface}, + {"setPreviewTexture", "(Landroid/graphics/SurfaceTexture;)V", + (void *)android_hardware_Camera_setPreviewTexture}, + {"setPreviewCallbackSurface", "(Landroid/view/Surface;)V", + (void *)android_hardware_Camera_setPreviewCallbackSurface}, + {"startPreview", "()V", (void *)android_hardware_Camera_startPreview}, + {"_stopPreview", "()V", (void *)android_hardware_Camera_stopPreview}, + {"previewEnabled", "()Z", (void *)android_hardware_Camera_previewEnabled}, + {"setHasPreviewCallback", "(ZZ)V", (void *)android_hardware_Camera_setHasPreviewCallback}, + {"_addCallbackBuffer", "([BI)V", (void *)android_hardware_Camera_addCallbackBuffer}, + {"native_autoFocus", "()V", (void *)android_hardware_Camera_autoFocus}, + {"native_cancelAutoFocus", "()V", (void *)android_hardware_Camera_cancelAutoFocus}, + {"native_takePicture", "(I)V", (void *)android_hardware_Camera_takePicture}, + {"native_setParameters", "(Ljava/lang/String;)V", + (void *)android_hardware_Camera_setParameters}, + {"native_getParameters", "()Ljava/lang/String;", + (void *)android_hardware_Camera_getParameters}, + {"reconnect", "()V", (void *)android_hardware_Camera_reconnect}, + {"lock", "()V", (void *)android_hardware_Camera_lock}, + {"unlock", "()V", (void *)android_hardware_Camera_unlock}, + {"startSmoothZoom", "(I)V", (void *)android_hardware_Camera_startSmoothZoom}, + {"stopSmoothZoom", "()V", (void *)android_hardware_Camera_stopSmoothZoom}, + {"setDisplayOrientation", "(I)V", (void *)android_hardware_Camera_setDisplayOrientation}, + {"_enableShutterSound", "(Z)Z", (void *)android_hardware_Camera_enableShutterSound}, + {"_startFaceDetection", "(I)V", (void *)android_hardware_Camera_startFaceDetection}, + {"_stopFaceDetection", "()V", (void *)android_hardware_Camera_stopFaceDetection}, + {"enableFocusMoveCallback", "(I)V", + (void *)android_hardware_Camera_enableFocusMoveCallback}, + {"setAudioRestriction", "(I)V", (void *)android_hardware_Camera_setAudioRestriction}, + {"getAudioRestriction", "()I", (void *)android_hardware_Camera_getAudioRestriction}, }; struct field { diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index a2eae2c9579a7..2b7bcbee79fd5 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -85,7 +85,8 @@ public class CameraBinderTest extends AndroidTestCase { public void testCameraInfo() throws Exception { for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) { - CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId); + CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId, + /*overrideToPortrait*/false); assertTrue("Facing was not set for camera " + cameraId, info.info.facing != -1); assertTrue("Orientation was not set for camera " + cameraId, info.info.orientation != -1); @@ -159,7 +160,8 @@ public class CameraBinderTest extends AndroidTestCase { .connect(dummyCallbacks, cameraId, clientPackageName, ICameraService.USE_CALLING_UID, ICameraService.USE_CALLING_PID, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); @@ -264,7 +266,8 @@ public class CameraBinderTest extends AndroidTestCase { dummyCallbacks, String.valueOf(cameraId), clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID, 0 /*oomScoreOffset*/, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java index 0890346db1985..9d09dcc5c4407 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java @@ -244,7 +244,8 @@ public class CameraDeviceBinderTest extends AndroidTestCase { mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId, clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID, - /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion); + /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser); mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); @@ -417,7 +418,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { @SmallTest public void testCameraCharacteristics() throws RemoteException { CameraMetadataNative info = mUtils.getCameraService().getCameraCharacteristics(mCameraId, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, /*overrideToPortrait*/false); assertFalse(info.isEmpty()); assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS));