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

Commit 66149737 authored by Evan Severson's avatar Evan Severson
Browse files

Use ambient light sensor to adjust camera privacy light brightness

If there is a light sensor available at boot then we will watch for
changing values and compute the integral of the values over the past
specified interval. Light sensor values are transformed with a logorithm
since it was observed that values increase very quickly with addition of
artificial light although as a human it feels that brightness is
increasing linearly (some light research tells this is called
Fechner's law)

Bug: 216107392
Test: CameraPrivacyLightControlllerTest

Change-Id: I94742d099d6a5d074173a21e629a3044f0a10c9a
parent 5815b882
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -448,5 +448,6 @@
    <color name="accessibility_color_inversion_background">#546E7A</color>

    <!-- Color of camera light when camera is in use -->
    <color name="camera_privacy_light">#FFFFFF</color>
    <color name="camera_privacy_light_day">#FFFFFF</color>
    <color name="camera_privacy_light_night">#FFFFFF</color>
</resources>
+5 −0
Original line number Diff line number Diff line
@@ -5790,4 +5790,9 @@

    <!-- List of the labels of requestable device state config values -->
    <string-array name="config_deviceStatesAvailableForAppRequests"/>

    <!-- Interval in milliseconds to average light sensor values for camera light brightness -->
    <integer name="config_cameraPrivacyLightAlsAveragingIntervalMillis">3000</integer>
    <!-- Light sensor's lux value to use as the threshold between using day or night brightness -->
    <integer name="config_cameraPrivacyLightAlsNightThreshold">4</integer>
</resources>
+4 −1
Original line number Diff line number Diff line
@@ -4754,7 +4754,10 @@
  <!-- For VirtualDeviceManager -->
  <java-symbol type="string" name="vdm_camera_access_denied" />

  <java-symbol type="color" name="camera_privacy_light"/>
  <java-symbol type="color" name="camera_privacy_light_day"/>
  <java-symbol type="color" name="camera_privacy_light_night"/>
  <java-symbol type="integer" name="config_cameraPrivacyLightAlsAveragingIntervalMillis"/>
  <java-symbol type="integer" name="config_cameraPrivacyLightAlsNightThreshold"/>

  <java-symbol type="bool" name="config_bg_current_drain_monitor_enabled" />
  <java-symbol type="array" name="config_bg_current_drain_threshold_to_restricted_bucket" />
+177 −9
Original line number Diff line number Diff line
@@ -16,44 +16,105 @@

package com.android.server.sensorprivacy;

import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;

import android.annotation.ColorInt;
import android.app.AppOpsManager;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightsRequest;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.SystemClock;
import android.permission.PermissionManager;
import android.util.ArraySet;
import android.util.Pair;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.FgThread;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
        SensorEventListener {

class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener {
    @VisibleForTesting
    static final double LIGHT_VALUE_MULTIPLIER = 1 / Math.log(1.1);

    private final Handler mHandler;
    private final Executor mExecutor;
    private final Context mContext;
    private final AppOpsManager mAppOpsManager;
    private final LightsManager mLightsManager;
    private final SensorManager mSensorManager;

    private final Set<String> mActivePackages = new ArraySet<>();
    private final Set<String> mActivePhonePackages = new ArraySet<>();

    private final int mCameraPrivacyLightColor;

    private final List<Light> mCameraLights = new ArrayList<>();
    private final AppOpsManager mAppOpsManager;

    private LightsManager.LightsSession mLightsSession = null;

    @ColorInt
    private final int mDayColor;
    @ColorInt
    private final int mNightColor;

    private final Sensor mLightSensor;

    private boolean mIsAmbientLightListenerRegistered = false;
    private final long mMovingAverageIntervalMillis;
    /** When average of the time integral over the past {@link #mMovingAverageIntervalMillis}
     *  milliseconds of the log_1.1(lux(t)) is greater than this value, use the daytime brightness
     *  else use nighttime brightness. */
    private final long mNightThreshold;
    private final ArrayDeque<Pair<Long, Integer>> mAmbientLightValues = new ArrayDeque<>();
    /** Tracks the Riemann sum of {@link #mAmbientLightValues} to avoid O(n) operations when sum is
     *  needed */
    private long mAlvSum = 0;
    private int mLastLightColor = 0;
    /** The elapsed real time that the ALS was started watching */
    private long mElapsedTimeStartedReading;

    private final Object mDelayedUpdateToken = new Object();

    // Can't mock static native methods, workaround for testing
    private long mElapsedRealTime = -1;

    CameraPrivacyLightController(Context context) {
        this(context, FgThread.get().getLooper());
    }

    @VisibleForTesting
    CameraPrivacyLightController(Context context, Looper looper) {
        mContext = context;

        mHandler = new Handler(looper);
        mExecutor = new HandlerExecutor(mHandler);

        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
        mLightsManager = mContext.getSystemService(LightsManager.class);
        mSensorManager = mContext.getSystemService(SensorManager.class);

        mCameraPrivacyLightColor = mContext.getColor(R.color.camera_privacy_light);
        mDayColor = mContext.getColor(R.color.camera_privacy_light_day);
        mNightColor = mContext.getColor(R.color.camera_privacy_light_night);
        mMovingAverageIntervalMillis = mContext.getResources()
                .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis);
        mNightThreshold = (long) (Math.log(mContext.getResources()
                .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold))
                * LIGHT_VALUE_MULTIPLIER);

        List<Light> lights = mLightsManager.getLights();
        for (int i = 0; i < lights.size(); i++) {
@@ -64,12 +125,60 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
        }

        if (mCameraLights.isEmpty()) {
            mLightSensor = null;
            return;
        }

        mAppOpsManager.startWatchingActive(
                new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA},
                FgThread.getExecutor(), this);
                mExecutor, this);

        // It may be useful in the future to configure devices to know which lights are near which
        // sensors so that we can control individual lights based on their environment.
        mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
    }

    private void addElement(long time, int value) {
        if (mAmbientLightValues.isEmpty()) {
            // Eliminate the size == 1 edge case and assume the light value has been constant for
            // the previous interval
            mAmbientLightValues.add(new Pair<>(time - getCurrentIntervalMillis() - 1, value));
        }
        Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();
        mAmbientLightValues.add(new Pair<>(time, value));

        mAlvSum += (time - lastElement.first) * lastElement.second;
        removeObsoleteData(time);
    }

    private void removeObsoleteData(long time) {
        while (mAmbientLightValues.size() > 1) {
            Pair<Long, Integer> element0 = mAmbientLightValues.pollFirst(); // NOTICE: POLL
            Pair<Long, Integer> element1 = mAmbientLightValues.peekFirst(); // NOTICE: PEEK
            if (element1.first > time - getCurrentIntervalMillis()) {
                mAmbientLightValues.addFirst(element0);
                break;
            }
            mAlvSum -= (element1.first - element0.first) * element0.second;
        }
    }

    /**
     * Gives the Riemann sum of {@link #mAmbientLightValues} where the part of the interval that
     * stretches outside the time window is removed and the time since the last change is added in.
     */
    private long getLiveAmbientLightTotal() {
        if (mAmbientLightValues.isEmpty()) {
            return mAlvSum;
        }
        long time = getElapsedRealTime();
        removeObsoleteData(time);

        Pair<Long, Integer> firstElement = mAmbientLightValues.peekFirst();
        Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();

        return mAlvSum - Math.max(0, time - getCurrentIntervalMillis() - firstElement.first)
                * firstElement.second + (time - lastElement.first) * lastElement.second;
    }

    @Override
@@ -93,10 +202,16 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
    }

    private void updateLightSession() {
        if (Looper.myLooper() != mHandler.getLooper()) {
            mHandler.post(this::updateLightSession);
            return;
        }

        Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext);

        boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages)
                && exemptedPackages.containsAll(mActivePhonePackages);
        updateSensorListener(shouldSessionEnd);

        if (shouldSessionEnd) {
            if (mLightsSession == null) {
@@ -106,20 +221,73 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
            mLightsSession.close();
            mLightsSession = null;
        } else {
            if (mLightsSession != null) {
            int lightColor;
            if (mLightSensor != null && getLiveAmbientLightTotal()
                    < getCurrentIntervalMillis() * mNightThreshold) {
                lightColor = mNightColor;
            } else {
                lightColor = mDayColor;
            }

            if (mLastLightColor == lightColor && mLightsSession != null) {
                return;
            }
            mLastLightColor = lightColor;

            LightsRequest.Builder requestBuilder = new LightsRequest.Builder();
            for (int i = 0; i < mCameraLights.size(); i++) {
                requestBuilder.addLight(mCameraLights.get(i),
                        new LightState.Builder()
                                .setColor(mCameraPrivacyLightColor)
                                .setColor(lightColor)
                                .build());
            }

            if (mLightsSession == null) {
                mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
            }

            mLightsSession.requestLights(requestBuilder.build());
        }
    }

    private void updateSensorListener(boolean shouldSessionEnd) {
        if (shouldSessionEnd && mIsAmbientLightListenerRegistered) {
            mSensorManager.unregisterListener(this);
            mIsAmbientLightListenerRegistered = false;
        }
        if (!shouldSessionEnd && !mIsAmbientLightListenerRegistered && mLightSensor != null) {
            mSensorManager.registerListener(this, mLightSensor, SENSOR_DELAY_NORMAL, mHandler);
            mIsAmbientLightListenerRegistered = true;
            mElapsedTimeStartedReading = getElapsedRealTime();
        }
    }

    private long getElapsedRealTime() {
        return mElapsedRealTime == -1 ? SystemClock.elapsedRealtime() : mElapsedRealTime;
    }

    @VisibleForTesting
    void setElapsedRealTime(long time) {
        mElapsedRealTime = time;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // Using log space to represent human sensation (Fechner's Law) instead of lux
        // because lux values causes bright flashes to skew the average very high.
        addElement(event.timestamp, Math.max(0,
                (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
        updateLightSession();
        mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
        mHandler.postDelayed(CameraPrivacyLightController.this::updateLightSession,
                mDelayedUpdateToken, mMovingAverageIntervalMillis);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    private long getCurrentIntervalMillis() {
        return Math.min(mMovingAverageIntervalMillis,
                getElapsedRealTime() - mElapsedTimeStartedReading);
    }
}
+199 −9
Original line number Diff line number Diff line
@@ -24,20 +24,33 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;

import android.app.AppOpsManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightsRequest;
import android.os.Handler;
import android.os.Looper;
import android.permission.PermissionManager;
import android.util.ArraySet;

import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.R;

import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
@@ -53,26 +66,43 @@ import java.util.stream.Collectors;

public class CameraPrivacyLightControllerTest {

    private int mDayColor = 1;
    private int mNightColor = 0;
    private int mCameraPrivacyLightAlsAveragingIntervalMillis = 5000;
    private int mCameraPrivacyLightAlsNightThreshold = (int) getLightSensorValue(15);

    private MockitoSession mMockitoSession;

    @Mock
    private Context mContext;

    @Mock
    private Resources mResources;

    @Mock
    private LightsManager mLightsManager;

    @Mock
    private AppOpsManager mAppOpsManager;

    @Mock
    private SensorManager mSensorManager;

    @Mock
    private LightsManager.LightsSession mLightsSession;

    @Mock
    private Sensor mLightSensor;

    private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor =
            ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class);

    private ArgumentCaptor<LightsRequest> mLightsRequestCaptor =
            ArgumentCaptor.forClass(LightsRequest.class);

    private ArgumentCaptor<SensorEventListener> mLightSensorListenerCaptor =
            ArgumentCaptor.forClass(SensorEventListener.class);

    private Set<String> mExemptedPackages = new ArraySet<>();
    private List<Light> mLights = new ArrayList<>();

@@ -86,11 +116,22 @@ public class CameraPrivacyLightControllerTest {
                .spyStatic(PermissionManager.class)
                .startMocking();

        doReturn(mDayColor).when(mContext).getColor(R.color.camera_privacy_light_day);
        doReturn(mNightColor).when(mContext).getColor(R.color.camera_privacy_light_night);

        doReturn(mResources).when(mContext).getResources();
        doReturn(mCameraPrivacyLightAlsAveragingIntervalMillis).when(mResources)
                .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis);
        doReturn(mCameraPrivacyLightAlsNightThreshold).when(mResources)
                .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold);

        doReturn(mLightsManager).when(mContext).getSystemService(LightsManager.class);
        doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
        doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class);

        doReturn(mLights).when(mLightsManager).getLights();
        doReturn(mLightsSession).when(mLightsManager).openSession(anyInt());
        doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);

        doReturn(mExemptedPackages)
                .when(() -> PermissionManager.getIndicatorExemptedPackages(any()));
@@ -107,7 +148,7 @@ public class CameraPrivacyLightControllerTest {
    @Test
    public void testAppsOpsListenerNotRegisteredWithoutCameraLights() {
        mLights.add(getNextLight(false));
        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any());
    }
@@ -116,7 +157,7 @@ public class CameraPrivacyLightControllerTest {
    public void testAppsOpsListenerRegisteredWithCameraLight() {
        mLights.add(getNextLight(true));

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any());
    }
@@ -128,14 +169,13 @@ public class CameraPrivacyLightControllerTest {
            mLights.add(getNextLight(r.nextBoolean()));
        }

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        // Verify no session has been opened at this point.
        verify(mLightsManager, times(0)).openSession(anyInt());

        // Set camera op as active.
        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10101, "pkg1", true);
        openCamera();

        // Verify session has been opened exactly once
        verify(mLightsManager, times(1)).openSession(anyInt());
@@ -161,7 +201,7 @@ public class CameraPrivacyLightControllerTest {
    public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() {
        mLights.add(getNextLight(true));

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());

@@ -176,7 +216,7 @@ public class CameraPrivacyLightControllerTest {
    public void testWillCloseOnFinishOp() {
        mLights.add(getNextLight(true));

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());

@@ -192,7 +232,7 @@ public class CameraPrivacyLightControllerTest {
    public void testWillCloseOnFinishOpForAllPackages() {
        mLights.add(getNextLight(true));

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        int numUids = 100;
        List<Integer> uids = new ArrayList<>(numUids);
@@ -226,7 +266,7 @@ public class CameraPrivacyLightControllerTest {
        mLights.add(getNextLight(true));
        mExemptedPackages.add("pkg1");

        new CameraPrivacyLightController(mContext);
        createCameraPrivacyLightController();

        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());

@@ -235,6 +275,147 @@ public class CameraPrivacyLightControllerTest {
        verify(mLightsManager, times(0)).openSession(anyInt());
    }

    @Test
    public void testNoLightSensor() {
        mLights.add(getNextLight(true));
        doReturn(null).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);

        createCameraPrivacyLightController();

        openCamera();

        verify(mLightsSession).requestLights(mLightsRequestCaptor.capture());
        LightsRequest lightsRequest = mLightsRequestCaptor.getValue();
        for (LightState lightState : lightsRequest.getLightStates()) {
            assertEquals(mDayColor, lightState.getColor());
        }
    }

    @Test
    public void testALSListenerNotRegisteredUntilCameraIsOpened() {
        mLights.add(getNextLight(true));
        Sensor sensor = mock(Sensor.class);
        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);

        CameraPrivacyLightController cplc = createCameraPrivacyLightController();

        verify(mSensorManager, never()).registerListener(any(SensorEventListener.class),
                any(Sensor.class), anyInt(), any(Handler.class));

        openCamera();

        verify(mSensorManager, times(1)).registerListener(mLightSensorListenerCaptor.capture(),
                any(Sensor.class), anyInt(), any(Handler.class));

        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", false);
        verify(mSensorManager, times(1)).unregisterListener(mLightSensorListenerCaptor.getValue());
    }

    @Ignore
    @Test
    public void testDayColor() {
        testBrightnessToColor(20, mDayColor);
    }

    @Ignore
    @Test
    public void testNightColor() {
        testBrightnessToColor(10, mNightColor);
    }

    private void testBrightnessToColor(int brightnessValue, int color) {
        mLights.add(getNextLight(true));
        Sensor sensor = mock(Sensor.class);
        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);

        CameraPrivacyLightController cplc = createCameraPrivacyLightController();
        cplc.setElapsedRealTime(0);

        openCamera();

        verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(),
                any(Sensor.class), anyInt(), any(Handler.class));
        SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue();
        float[] sensorEventValues = new float[1];
        SensorEvent sensorEvent = new SensorEvent(sensor, 0, 0, sensorEventValues);

        sensorEventValues[0] = getLightSensorValue(brightnessValue);
        sensorListener.onSensorChanged(sensorEvent);

        verify(mLightsSession, atLeastOnce()).requestLights(mLightsRequestCaptor.capture());
        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
            assertEquals(color, lightState.getColor());
        }
    }

    @Ignore
    @Test
    public void testDayToNightTransistion() {
        mLights.add(getNextLight(true));
        Sensor sensor = mock(Sensor.class);
        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);

        CameraPrivacyLightController cplc = createCameraPrivacyLightController();
        cplc.setElapsedRealTime(0);

        openCamera();
        // There will be an initial call at brightness 0
        verify(mLightsSession, times(1)).requestLights(any(LightsRequest.class));

        verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(),
                any(Sensor.class), anyInt(), any(Handler.class));
        SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue();

        onSensorEvent(cplc, sensorListener, sensor, 0, 20);

        // 5 sec avg = 20
        onSensorEvent(cplc, sensorListener, sensor, 5000, 30);

        verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture());
        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
            assertEquals(mDayColor, lightState.getColor());
        }

        // 5 sec avg = 22

        onSensorEvent(cplc, sensorListener, sensor, 6000, 10);

        // 5 sec avg = 18

        onSensorEvent(cplc, sensorListener, sensor, 8000, 5);

        // Should have always been day
        verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture());
        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
            assertEquals(mDayColor, lightState.getColor());
        }

        // 5 sec avg = 12

        onSensorEvent(cplc, sensorListener, sensor, 10000, 5);

        // Should now be night
        verify(mLightsSession, times(3)).requestLights(mLightsRequestCaptor.capture());
        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
            assertEquals(mNightColor, lightState.getColor());
        }
    }

    private void onSensorEvent(CameraPrivacyLightController cplc,
            SensorEventListener sensorListener, Sensor sensor, long timestamp, int value) {
        cplc.setElapsedRealTime(timestamp);
        sensorListener.onSensorChanged(new SensorEvent(sensor, 0, timestamp,
                new float[] {getLightSensorValue(value)}));
    }

    // Use the test thread so that the test is deterministic
    private CameraPrivacyLightController createCameraPrivacyLightController() {
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
        return new CameraPrivacyLightController(mContext, Looper.myLooper());
    }

    private Light getNextLight(boolean cameraType) {
        Light light = ExtendedMockito.mock(Light.class);
        if (cameraType) {
@@ -245,4 +426,13 @@ public class CameraPrivacyLightControllerTest {
        doReturn(mNextLightId++).when(light).getId();
        return light;
    }

    private float getLightSensorValue(int i) {
        return (float) Math.exp(i / CameraPrivacyLightController.LIGHT_VALUE_MULTIPLIER);
    }

    private void openCamera() {
        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", true);
    }
}