Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0f436120 authored by Mina Granic's avatar Mina Granic
Browse files

Sandbox OrientationEventListener orientation when sandboxing Display rotation.

CompatibilityInfo.overrideDisplayRotation is set while in camera compat
 mode. Many apps use Display.getRotation() - which is changed in camera
 compat mode - or via OrientationEventListener.
OrientationEventListener is rarely used to calculate camera preview
 orientation, but it is sometimes used to calculate captured image
 rotation. Both ways should result in equivalent rotations,
for the camera image to be upright (not stretched or sideways).

Flag: com.android.window.flags.enable_camera_compat_for_desktop_windowing
Fixes: 389977891
Test: atest FrameworksCoreTests:android.view.OrientationEventListenerFrameworkTest
Change-Id: I6a693a9180dda9ada77166d81a31a5b7afd141f7
parent 8277e2b6
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -40,8 +40,6 @@ import android.view.Surface;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;

import com.android.aconfig.annotations.VisibleForTesting;

/**
 * CompatibilityInfo class keeps the information about the screen compatibility mode that the
 * application is running under.
@@ -763,7 +761,6 @@ public class CompatibilityInfo implements Parcelable {
    }

    /** @see #sOverrideDisplayRotation */
    @VisibleForTesting
    public static int getOverrideDisplayRotation() {
        return sOverrideDisplayRotation;
    }
+53 −6
Original line number Diff line number Diff line
@@ -16,13 +16,19 @@

package android.view;

import static android.app.WindowConfiguration.ROTATION_UNDEFINED;

import android.annotation.NonNull;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;

import com.android.window.flags.Flags;

/**
 * Helper class for receiving notifications from the SensorManager when
 * the orientation of the device has changed.
@@ -70,8 +76,10 @@ public abstract class OrientationEventListener {
        mRate = rate;
        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (mSensor != null) {
            // Create listener only if sensors do exist
            mSensorEventListener = new SensorEventListenerImpl();
            // Create listener only if sensors do exist.
            mSensorEventListener = Flags.enableCameraCompatForDesktopWindowing()
                    ? new CompatSensorEventListenerImpl(new SensorEventListenerImpl())
                    : new SensorEventListenerImpl();
        }
    }
    
@@ -149,6 +157,45 @@ public abstract class OrientationEventListener {
        }
    }

    /** Decorator to the {@link SensorEventListenerImpl}, which provides compat values if needed. */
    class CompatSensorEventListenerImpl implements SensorEventListener {
        // SensorEventListener without compatibility capabilities.
        final SensorEventListenerImpl mSensorEventListener;

        CompatSensorEventListenerImpl(@NonNull SensorEventListenerImpl sensorEventListener) {
            mSensorEventListener = sensorEventListener;
        }

        public void onSensorChanged(SensorEvent event) {
            // If the display rotation override is set, the same override should be applied to
            // this orientation too. This rotation override will only be set when an app has a
            // camera open and it is in camera compat mode for desktop windowing (freeform mode).
            // Values of this override is Surface.ROTATION_0/90/180/270, or
            // WindowConfiguration.ROTATION_UNDEFINED when not set.
            if (CompatibilityInfo.getOverrideDisplayRotation() != ROTATION_UNDEFINED) {
                // SensorEventListener reports the rotation in the opposite direction from the
                // display rotation.
                int orientation = (360 - CompatibilityInfo.getOverrideDisplayRotation() * 90) % 360;
                if (orientation != mOrientation) {
                    mOrientation = orientation;
                    onOrientationChanged(orientation);
                }
                // `mOldListener` is deprecated and returns 3D values, which are highly unlikely to
                // be used for orienting camera image. Thus this listener is not called here, as
                // opposed to extrapolating values from display rotation, from 1D->3D.
            } else {
                // Use the default implementation: calculate the orientation from event coordinates.
                // This method will call OrientationEventListener.onOrientationChanged(orientation)
                // if the orientation has changed.
                mSensorEventListener.onSensorChanged(event);
            }
        }

        public void onAccuracyChanged(Sensor sensor, int accuracy) {

        }
    }

    /*
     * Returns true if sensor is enabled and false otherwise
     */
+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.view;

import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import android.annotation.NonNull;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.input.InputSensorInfo;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.util.ArrayList;

/**
 * Test {@link OrientationEventListener}.
 *
 * <p>Build/Install/Run:
 *  atest FrameworksCoreTests:OrientationEventListenerFrameworkTest
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class OrientationEventListenerFrameworkTest {
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private Context mContext;
    private SensorManager mSensorManager;

    @Before
    public void setup() {
        mContext = mock(Context.class);
        mSensorManager = mock(SensorManager.class);
        doReturn(mSensorManager).when(mContext).getSystemService(Context.SENSOR_SERVICE);
    }

    @After
    public void tearDown() {
        // Reset the override rotation for tests that use the default value.
        CompatibilityInfo.setOverrideDisplayRotation(WindowConfiguration.ROTATION_UNDEFINED);
    }

    @Test
    public void testConstructor() {
        new TestOrientationEventListener(mContext);

        new TestOrientationEventListener(mContext, SensorManager.SENSOR_DELAY_UI);
    }

    @Test
    public void testEnableAndDisable() {
        final TestOrientationEventListener listener = new TestOrientationEventListener(mContext);
        listener.enable();
        listener.disable();
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testSensorOrientationUpdate() {
        final Sensor mockSensor = setupMockAccelerometerSensor();
        final TestOrientationEventListener listener = new TestOrientationEventListener(mContext);

        listener.enable();

        sendSensorEventWithOrientation270(mockSensor);

        assertEquals(1, listener.mReportedOrientations.size());
        assertEquals(270, (int) listener.mReportedOrientations.get(0));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testSensorOrientationUpdate_overriddenDisplayRotationReportedWhenSet() {
        final Sensor mockSensor = setupMockAccelerometerSensor();
        final TestOrientationEventListener listener = new TestOrientationEventListener(mContext);

        listener.enable();

        // This should change the reported sensor rotation.
        CompatibilityInfo.setOverrideDisplayRotation(Surface.ROTATION_180);

        sendSensorEventWithOrientation270(mockSensor);

        assertEquals(1, listener.mReportedOrientations.size());
        assertEquals(180, (int) listener.mReportedOrientations.get(0));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testSensorOrientationUpdate_overriddenDisplayRotationIsNegativeFromSensor() {
        final Sensor mockSensor = setupMockAccelerometerSensor();
        final TestOrientationEventListener listener = new TestOrientationEventListener(mContext);

        listener.enable();

        // Display rotation is counted in the opposite direction from the sensor orientation, thus
        // this call should change the reported sensor rotation to 90, as 360 - 270 = 90.
        CompatibilityInfo.setOverrideDisplayRotation(Surface.ROTATION_270);

        sendSensorEventWithOrientation270(mockSensor);

        assertEquals(1, listener.mReportedOrientations.size());
        assertEquals(90, (int) listener.mReportedOrientations.get(0));
    }

    @Test
    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testSensorOrientationUpdate_notOverriddenWhenCameraFeatureDisabled() {
        final Sensor mockSensor = setupMockAccelerometerSensor();
        final TestOrientationEventListener listener = new TestOrientationEventListener(mContext);

        listener.enable();

        CompatibilityInfo.setOverrideDisplayRotation(Surface.ROTATION_180);

        sendSensorEventWithOrientation270(mockSensor);

        assertEquals(1, listener.mReportedOrientations.size());
        // Sensor unchanged by override because the feature is disabled.
        assertEquals(270, (int) listener.mReportedOrientations.get(0));
    }

    @NonNull
    private Sensor setupMockAccelerometerSensor() {
        final Sensor mockSensor = new Sensor(mock(InputSensorInfo.class));
        doReturn(mockSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        return mockSensor;
    }

    @NonNull
    private SensorEventListener getRegisteredEventListener() {
        // Get the SensorEventListenerImpl that has subscribed in `listener.enable()`.
        final ArgumentCaptor<SensorEventListener> listenerArgCaptor = ArgumentCaptor
                .forClass(SensorEventListener.class);
        verify(mSensorManager).registerListener(listenerArgCaptor.capture(), any(), anyInt());
        return listenerArgCaptor.getValue();
    }

    private void sendSensorEventWithOrientation270(@NonNull Sensor sensor) {
        final SensorEventListener sensorEventListener = getRegisteredEventListener();
        // Arbitrary values that return orientation 270.
        final SensorEvent sensorEvent = new SensorEvent(sensor, 1, 1L,
                new float[]{1.0f, 0.0f, 0.0f});
        sensorEventListener.onSensorChanged(sensorEvent);
    }

    private static class TestOrientationEventListener extends OrientationEventListener {
        final ArrayList<Integer> mReportedOrientations = new ArrayList<>();

        TestOrientationEventListener(Context context) {
            super(context);
        }

        TestOrientationEventListener(Context context, int rate) {
            super(context, rate);
        }

        @Override
        public void onOrientationChanged(int orientation) {
            mReportedOrientations.add(orientation);
        }
    }
}