Loading core/res/res/values/colors.xml +2 −1 Original line number Diff line number Diff line Loading @@ -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> core/res/res/values/config.xml +5 −0 Original line number Diff line number Diff line Loading @@ -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> core/res/res/values/symbols.xml +4 −1 Original line number Diff line number Diff line Loading @@ -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" /> Loading services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java +177 −9 Original line number Diff line number Diff line Loading @@ -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++) { Loading @@ -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 Loading @@ -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) { Loading @@ -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); } } services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java +199 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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<>(); Loading @@ -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())); Loading @@ -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()); } Loading @@ -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()); } Loading @@ -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()); Loading @@ -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()); Loading @@ -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()); Loading @@ -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); Loading Loading @@ -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()); Loading @@ -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) { Loading @@ -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); } } Loading
core/res/res/values/colors.xml +2 −1 Original line number Diff line number Diff line Loading @@ -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>
core/res/res/values/config.xml +5 −0 Original line number Diff line number Diff line Loading @@ -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>
core/res/res/values/symbols.xml +4 −1 Original line number Diff line number Diff line Loading @@ -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" /> Loading
services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java +177 −9 Original line number Diff line number Diff line Loading @@ -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++) { Loading @@ -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 Loading @@ -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) { Loading @@ -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); } }
services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java +199 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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<>(); Loading @@ -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())); Loading @@ -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()); } Loading @@ -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()); } Loading @@ -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()); Loading @@ -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()); Loading @@ -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()); Loading @@ -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); Loading Loading @@ -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()); Loading @@ -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) { Loading @@ -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); } }