blob: f744d00b2066a64306e7ec4258744d995c056042 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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;
import java.util.concurrent.TimeUnit;
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 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 List<Light> mCameraLights = new ArrayList<>();
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);
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++) {
Light light = lights.get(i);
if (light.getType() == Light.LIGHT_TYPE_CAMERA) {
mCameraLights.add(light);
}
}
if (mCameraLights.isEmpty()) {
mLightSensor = null;
return;
}
mAppOpsManager.startWatchingActive(
new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA},
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
public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
final Set<String> activePackages;
if (AppOpsManager.OPSTR_CAMERA.equals(op)) {
activePackages = mActivePackages;
} else if (AppOpsManager.OPSTR_PHONE_CALL_CAMERA.equals(op)) {
activePackages = mActivePhonePackages;
} else {
return;
}
if (active) {
activePackages.add(packageName);
} else {
activePackages.remove(packageName);
}
updateLightSession();
}
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) {
return;
}
mLightsSession.close();
mLightsSession = null;
} else {
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(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(TimeUnit.NANOSECONDS.toMillis(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);
}
}