Merge "Use ambient light sensor to adjust camera privacy light brightness" into tm-dev am: 1b025f37ff am: 807eef0d9b
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17546090 Change-Id: Ie7d751f2541af0f2d3fd8b51036cf74b00a9349d Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -448,5 +448,6 @@
|
|||||||
<color name="accessibility_color_inversion_background">#546E7A</color>
|
<color name="accessibility_color_inversion_background">#546E7A</color>
|
||||||
|
|
||||||
<!-- Color of camera light when camera is in use -->
|
<!-- 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>
|
</resources>
|
||||||
|
|||||||
@@ -5790,4 +5790,9 @@
|
|||||||
|
|
||||||
<!-- List of the labels of requestable device state config values -->
|
<!-- List of the labels of requestable device state config values -->
|
||||||
<string-array name="config_deviceStatesAvailableForAppRequests"/>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -4759,7 +4759,10 @@
|
|||||||
<!-- For VirtualDeviceManager -->
|
<!-- For VirtualDeviceManager -->
|
||||||
<java-symbol type="string" name="vdm_camera_access_denied" />
|
<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="bool" name="config_bg_current_drain_monitor_enabled" />
|
||||||
<java-symbol type="array" name="config_bg_current_drain_threshold_to_restricted_bucket" />
|
<java-symbol type="array" name="config_bg_current_drain_threshold_to_restricted_bucket" />
|
||||||
|
|||||||
@@ -16,44 +16,105 @@
|
|||||||
|
|
||||||
package com.android.server.sensorprivacy;
|
package com.android.server.sensorprivacy;
|
||||||
|
|
||||||
|
import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
|
||||||
|
|
||||||
|
import android.annotation.ColorInt;
|
||||||
import android.app.AppOpsManager;
|
import android.app.AppOpsManager;
|
||||||
import android.content.Context;
|
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.Light;
|
||||||
import android.hardware.lights.LightState;
|
import android.hardware.lights.LightState;
|
||||||
import android.hardware.lights.LightsManager;
|
import android.hardware.lights.LightsManager;
|
||||||
import android.hardware.lights.LightsRequest;
|
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.permission.PermissionManager;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import com.android.internal.R;
|
import com.android.internal.R;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.server.FgThread;
|
import com.android.server.FgThread;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener {
|
class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
|
||||||
|
SensorEventListener {
|
||||||
|
|
||||||
|
@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 Context mContext;
|
||||||
|
private final AppOpsManager mAppOpsManager;
|
||||||
private final LightsManager mLightsManager;
|
private final LightsManager mLightsManager;
|
||||||
|
private final SensorManager mSensorManager;
|
||||||
|
|
||||||
private final Set<String> mActivePackages = new ArraySet<>();
|
private final Set<String> mActivePackages = new ArraySet<>();
|
||||||
private final Set<String> mActivePhonePackages = new ArraySet<>();
|
private final Set<String> mActivePhonePackages = new ArraySet<>();
|
||||||
|
|
||||||
private final int mCameraPrivacyLightColor;
|
|
||||||
|
|
||||||
private final List<Light> mCameraLights = new ArrayList<>();
|
private final List<Light> mCameraLights = new ArrayList<>();
|
||||||
private final AppOpsManager mAppOpsManager;
|
|
||||||
|
|
||||||
private LightsManager.LightsSession mLightsSession = null;
|
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) {
|
CameraPrivacyLightController(Context context) {
|
||||||
|
this(context, FgThread.get().getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
CameraPrivacyLightController(Context context, Looper looper) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
|
|
||||||
|
mHandler = new Handler(looper);
|
||||||
|
mExecutor = new HandlerExecutor(mHandler);
|
||||||
|
|
||||||
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
|
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
|
||||||
mLightsManager = mContext.getSystemService(LightsManager.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();
|
List<Light> lights = mLightsManager.getLights();
|
||||||
for (int i = 0; i < lights.size(); i++) {
|
for (int i = 0; i < lights.size(); i++) {
|
||||||
@@ -64,12 +125,60 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mCameraLights.isEmpty()) {
|
if (mCameraLights.isEmpty()) {
|
||||||
|
mLightSensor = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mAppOpsManager.startWatchingActive(
|
mAppOpsManager.startWatchingActive(
|
||||||
new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA},
|
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
|
@Override
|
||||||
@@ -93,10 +202,16 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateLightSession() {
|
private void updateLightSession() {
|
||||||
|
if (Looper.myLooper() != mHandler.getLooper()) {
|
||||||
|
mHandler.post(this::updateLightSession);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext);
|
Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext);
|
||||||
|
|
||||||
boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages)
|
boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages)
|
||||||
&& exemptedPackages.containsAll(mActivePhonePackages);
|
&& exemptedPackages.containsAll(mActivePhonePackages);
|
||||||
|
updateSensorListener(shouldSessionEnd);
|
||||||
|
|
||||||
if (shouldSessionEnd) {
|
if (shouldSessionEnd) {
|
||||||
if (mLightsSession == null) {
|
if (mLightsSession == null) {
|
||||||
@@ -106,20 +221,73 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis
|
|||||||
mLightsSession.close();
|
mLightsSession.close();
|
||||||
mLightsSession = null;
|
mLightsSession = null;
|
||||||
} else {
|
} else {
|
||||||
if (mLightsSession != null) {
|
int lightColor;
|
||||||
|
if (mLightSensor != null && getLiveAmbientLightTotal()
|
||||||
|
< getCurrentIntervalMillis() * mNightThreshold) {
|
||||||
|
lightColor = mNightColor;
|
||||||
|
} else {
|
||||||
|
lightColor = mDayColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mLastLightColor == lightColor && mLightsSession != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mLastLightColor = lightColor;
|
||||||
|
|
||||||
LightsRequest.Builder requestBuilder = new LightsRequest.Builder();
|
LightsRequest.Builder requestBuilder = new LightsRequest.Builder();
|
||||||
for (int i = 0; i < mCameraLights.size(); i++) {
|
for (int i = 0; i < mCameraLights.size(); i++) {
|
||||||
requestBuilder.addLight(mCameraLights.get(i),
|
requestBuilder.addLight(mCameraLights.get(i),
|
||||||
new LightState.Builder()
|
new LightState.Builder()
|
||||||
.setColor(mCameraPrivacyLightColor)
|
.setColor(lightColor)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
|
if (mLightsSession == null) {
|
||||||
|
mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
mLightsSession.requestLights(requestBuilder.build());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,33 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
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 static org.mockito.Mockito.times;
|
||||||
|
|
||||||
import android.app.AppOpsManager;
|
import android.app.AppOpsManager;
|
||||||
import android.content.Context;
|
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.Light;
|
||||||
|
import android.hardware.lights.LightState;
|
||||||
import android.hardware.lights.LightsManager;
|
import android.hardware.lights.LightsManager;
|
||||||
import android.hardware.lights.LightsRequest;
|
import android.hardware.lights.LightsRequest;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.permission.PermissionManager;
|
import android.permission.PermissionManager;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
|
|
||||||
import com.android.dx.mockito.inline.extended.ExtendedMockito;
|
import com.android.dx.mockito.inline.extended.ExtendedMockito;
|
||||||
|
import com.android.internal.R;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
@@ -53,26 +66,43 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class CameraPrivacyLightControllerTest {
|
public class CameraPrivacyLightControllerTest {
|
||||||
|
|
||||||
|
private int mDayColor = 1;
|
||||||
|
private int mNightColor = 0;
|
||||||
|
private int mCameraPrivacyLightAlsAveragingIntervalMillis = 5000;
|
||||||
|
private int mCameraPrivacyLightAlsNightThreshold = (int) getLightSensorValue(15);
|
||||||
|
|
||||||
private MockitoSession mMockitoSession;
|
private MockitoSession mMockitoSession;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Resources mResources;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LightsManager mLightsManager;
|
private LightsManager mLightsManager;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AppOpsManager mAppOpsManager;
|
private AppOpsManager mAppOpsManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SensorManager mSensorManager;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LightsManager.LightsSession mLightsSession;
|
private LightsManager.LightsSession mLightsSession;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Sensor mLightSensor;
|
||||||
|
|
||||||
private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor =
|
private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor =
|
||||||
ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class);
|
ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class);
|
||||||
|
|
||||||
private ArgumentCaptor<LightsRequest> mLightsRequestCaptor =
|
private ArgumentCaptor<LightsRequest> mLightsRequestCaptor =
|
||||||
ArgumentCaptor.forClass(LightsRequest.class);
|
ArgumentCaptor.forClass(LightsRequest.class);
|
||||||
|
|
||||||
|
private ArgumentCaptor<SensorEventListener> mLightSensorListenerCaptor =
|
||||||
|
ArgumentCaptor.forClass(SensorEventListener.class);
|
||||||
|
|
||||||
private Set<String> mExemptedPackages = new ArraySet<>();
|
private Set<String> mExemptedPackages = new ArraySet<>();
|
||||||
private List<Light> mLights = new ArrayList<>();
|
private List<Light> mLights = new ArrayList<>();
|
||||||
|
|
||||||
@@ -86,11 +116,22 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
.spyStatic(PermissionManager.class)
|
.spyStatic(PermissionManager.class)
|
||||||
.startMocking();
|
.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(mLightsManager).when(mContext).getSystemService(LightsManager.class);
|
||||||
doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
|
doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
|
||||||
|
doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class);
|
||||||
|
|
||||||
doReturn(mLights).when(mLightsManager).getLights();
|
doReturn(mLights).when(mLightsManager).getLights();
|
||||||
doReturn(mLightsSession).when(mLightsManager).openSession(anyInt());
|
doReturn(mLightsSession).when(mLightsManager).openSession(anyInt());
|
||||||
|
doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
|
||||||
|
|
||||||
doReturn(mExemptedPackages)
|
doReturn(mExemptedPackages)
|
||||||
.when(() -> PermissionManager.getIndicatorExemptedPackages(any()));
|
.when(() -> PermissionManager.getIndicatorExemptedPackages(any()));
|
||||||
@@ -107,7 +148,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testAppsOpsListenerNotRegisteredWithoutCameraLights() {
|
public void testAppsOpsListenerNotRegisteredWithoutCameraLights() {
|
||||||
mLights.add(getNextLight(false));
|
mLights.add(getNextLight(false));
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any());
|
verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any());
|
||||||
}
|
}
|
||||||
@@ -116,7 +157,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
public void testAppsOpsListenerRegisteredWithCameraLight() {
|
public void testAppsOpsListenerRegisteredWithCameraLight() {
|
||||||
mLights.add(getNextLight(true));
|
mLights.add(getNextLight(true));
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any());
|
verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any());
|
||||||
}
|
}
|
||||||
@@ -128,14 +169,13 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
mLights.add(getNextLight(r.nextBoolean()));
|
mLights.add(getNextLight(r.nextBoolean()));
|
||||||
}
|
}
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
// Verify no session has been opened at this point.
|
// Verify no session has been opened at this point.
|
||||||
verify(mLightsManager, times(0)).openSession(anyInt());
|
verify(mLightsManager, times(0)).openSession(anyInt());
|
||||||
|
|
||||||
// Set camera op as active.
|
// Set camera op as active.
|
||||||
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
openCamera();
|
||||||
mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10101, "pkg1", true);
|
|
||||||
|
|
||||||
// Verify session has been opened exactly once
|
// Verify session has been opened exactly once
|
||||||
verify(mLightsManager, times(1)).openSession(anyInt());
|
verify(mLightsManager, times(1)).openSession(anyInt());
|
||||||
@@ -161,7 +201,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() {
|
public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() {
|
||||||
mLights.add(getNextLight(true));
|
mLights.add(getNextLight(true));
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
||||||
|
|
||||||
@@ -176,7 +216,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
public void testWillCloseOnFinishOp() {
|
public void testWillCloseOnFinishOp() {
|
||||||
mLights.add(getNextLight(true));
|
mLights.add(getNextLight(true));
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
||||||
|
|
||||||
@@ -192,7 +232,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
public void testWillCloseOnFinishOpForAllPackages() {
|
public void testWillCloseOnFinishOpForAllPackages() {
|
||||||
mLights.add(getNextLight(true));
|
mLights.add(getNextLight(true));
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
int numUids = 100;
|
int numUids = 100;
|
||||||
List<Integer> uids = new ArrayList<>(numUids);
|
List<Integer> uids = new ArrayList<>(numUids);
|
||||||
@@ -226,7 +266,7 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
mLights.add(getNextLight(true));
|
mLights.add(getNextLight(true));
|
||||||
mExemptedPackages.add("pkg1");
|
mExemptedPackages.add("pkg1");
|
||||||
|
|
||||||
new CameraPrivacyLightController(mContext);
|
createCameraPrivacyLightController();
|
||||||
|
|
||||||
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
|
||||||
|
|
||||||
@@ -235,6 +275,147 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
verify(mLightsManager, times(0)).openSession(anyInt());
|
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) {
|
private Light getNextLight(boolean cameraType) {
|
||||||
Light light = ExtendedMockito.mock(Light.class);
|
Light light = ExtendedMockito.mock(Light.class);
|
||||||
if (cameraType) {
|
if (cameraType) {
|
||||||
@@ -245,4 +426,13 @@ public class CameraPrivacyLightControllerTest {
|
|||||||
doReturn(mNextLightId++).when(light).getId();
|
doReturn(mNextLightId++).when(light).getId();
|
||||||
return light;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user