1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.sensorprivacy;
18 
19 import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
20 
21 import android.app.AppOpsManager;
22 import android.content.Context;
23 import android.hardware.Sensor;
24 import android.hardware.SensorEvent;
25 import android.hardware.SensorEventListener;
26 import android.hardware.SensorManager;
27 import android.hardware.lights.Light;
28 import android.hardware.lights.LightState;
29 import android.hardware.lights.LightsManager;
30 import android.hardware.lights.LightsRequest;
31 import android.os.Handler;
32 import android.os.HandlerExecutor;
33 import android.os.Looper;
34 import android.os.SystemClock;
35 import android.permission.PermissionManager;
36 import android.util.ArraySet;
37 import android.util.Pair;
38 
39 import com.android.internal.R;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.util.ArrayUtils;
42 import com.android.server.FgThread;
43 
44 import java.util.ArrayDeque;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Set;
48 import java.util.concurrent.Executor;
49 import java.util.concurrent.TimeUnit;
50 
51 class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
52         SensorEventListener {
53 
54     private static final double LIGHT_VALUE_MULTIPLIER = 1 / Math.log(1.1);
55 
56     private final Handler mHandler;
57     private final Executor mExecutor;
58     private final Context mContext;
59     private final AppOpsManager mAppOpsManager;
60     private final LightsManager mLightsManager;
61     private final SensorManager mSensorManager;
62 
63     private final Set<String> mActivePackages = new ArraySet<>();
64     private final Set<String> mActivePhonePackages = new ArraySet<>();
65 
66     private final List<Light> mCameraLights = new ArrayList<>();
67 
68     private LightsManager.LightsSession mLightsSession = null;
69 
70     private final Sensor mLightSensor;
71 
72     private boolean mIsAmbientLightListenerRegistered = false;
73     private final long mMovingAverageIntervalMillis;
74     /** When average of the time integral over the past {@link #mMovingAverageIntervalMillis}
75      *  milliseconds of the log_1.1(lux(t)) is greater than this value, use the daytime brightness
76      *  else use nighttime brightness. */
77     private final long[] mThresholds;
78 
79     private final int[] mColors;
80     private final ArrayDeque<Pair<Long, Integer>> mAmbientLightValues = new ArrayDeque<>();
81     /** Tracks the Riemann sum of {@link #mAmbientLightValues} to avoid O(n) operations when sum is
82      *  needed */
83     private long mAlvSum = 0;
84     private int mLastLightColor = 0;
85     /** The elapsed real time that the ALS was started watching */
86     private long mElapsedTimeStartedReading;
87 
88     private final Object mDelayedUpdateToken = new Object();
89 
90     // Can't mock static native methods, workaround for testing
91     private long mElapsedRealTime = -1;
92 
CameraPrivacyLightController(Context context)93     CameraPrivacyLightController(Context context) {
94         this(context, FgThread.get().getLooper());
95     }
96 
97     @VisibleForTesting
CameraPrivacyLightController(Context context, Looper looper)98     CameraPrivacyLightController(Context context, Looper looper) {
99         mColors = context.getResources().getIntArray(R.array.config_cameraPrivacyLightColors);
100         if (ArrayUtils.isEmpty(mColors)) {
101             mHandler = null;
102             mExecutor = null;
103             mContext = null;
104             mAppOpsManager = null;
105             mLightsManager = null;
106             mSensorManager = null;
107             mLightSensor = null;
108             mMovingAverageIntervalMillis = 0;
109             mThresholds = null;
110             // Return here before this class starts interacting with other services.
111             return;
112         }
113         mContext = context;
114 
115         mHandler = new Handler(looper);
116         mExecutor = new HandlerExecutor(mHandler);
117 
118         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
119         mLightsManager = mContext.getSystemService(LightsManager.class);
120         mSensorManager = mContext.getSystemService(SensorManager.class);
121         mMovingAverageIntervalMillis = mContext.getResources()
122                 .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis);
123         int[] thresholdsLux = mContext.getResources().getIntArray(
124                 R.array.config_cameraPrivacyLightAlsLuxThresholds);
125         if (thresholdsLux.length != mColors.length - 1) {
126             throw new IllegalStateException("There must be exactly one more color than thresholds."
127                     + " Found " + mColors.length + " colors and " + thresholdsLux.length
128                     + " thresholds.");
129         }
130         mThresholds = new long[thresholdsLux.length];
131         for (int i = 0; i < thresholdsLux.length; i++) {
132             int luxValue = thresholdsLux[i];
133             mThresholds[i] = (long) (Math.log(luxValue) * LIGHT_VALUE_MULTIPLIER);
134         }
135 
136         List<Light> lights = mLightsManager.getLights();
137         for (int i = 0; i < lights.size(); i++) {
138             Light light = lights.get(i);
139             if (light.getType() == Light.LIGHT_TYPE_CAMERA) {
140                 mCameraLights.add(light);
141             }
142         }
143 
144         if (mCameraLights.isEmpty()) {
145             mLightSensor = null;
146             return;
147         }
148 
149         mAppOpsManager.startWatchingActive(
150                 new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA},
151                 mExecutor, this);
152 
153         // It may be useful in the future to configure devices to know which lights are near which
154         // sensors so that we can control individual lights based on their environment.
155         mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
156     }
157 
addElement(long time, int value)158     private void addElement(long time, int value) {
159         if (mAmbientLightValues.isEmpty()) {
160             // Eliminate the size == 1 edge case and assume the light value has been constant for
161             // the previous interval
162             mAmbientLightValues.add(new Pair<>(time - getCurrentIntervalMillis() - 1, value));
163         }
164         Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();
165         mAmbientLightValues.add(new Pair<>(time, value));
166 
167         mAlvSum += (time - lastElement.first) * lastElement.second;
168         removeObsoleteData(time);
169     }
170 
removeObsoleteData(long time)171     private void removeObsoleteData(long time) {
172         while (mAmbientLightValues.size() > 1) {
173             Pair<Long, Integer> element0 = mAmbientLightValues.pollFirst(); // NOTICE: POLL
174             Pair<Long, Integer> element1 = mAmbientLightValues.peekFirst(); // NOTICE: PEEK
175             if (element1.first > time - getCurrentIntervalMillis()) {
176                 mAmbientLightValues.addFirst(element0);
177                 break;
178             }
179             mAlvSum -= (element1.first - element0.first) * element0.second;
180         }
181     }
182 
183     /**
184      * Gives the Riemann sum of {@link #mAmbientLightValues} where the part of the interval that
185      * stretches outside the time window is removed and the time since the last change is added in.
186      */
getLiveAmbientLightTotal()187     private long getLiveAmbientLightTotal() {
188         if (mAmbientLightValues.isEmpty()) {
189             return mAlvSum;
190         }
191         long time = getElapsedRealTime();
192         removeObsoleteData(time);
193 
194         Pair<Long, Integer> firstElement = mAmbientLightValues.peekFirst();
195         Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();
196 
197         return mAlvSum - Math.max(0, time - getCurrentIntervalMillis() - firstElement.first)
198                 * firstElement.second + (time - lastElement.first) * lastElement.second;
199     }
200 
201     @Override
onOpActiveChanged(String op, int uid, String packageName, boolean active)202     public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
203         final Set<String> activePackages;
204         if (AppOpsManager.OPSTR_CAMERA.equals(op)) {
205             activePackages = mActivePackages;
206         } else if (AppOpsManager.OPSTR_PHONE_CALL_CAMERA.equals(op)) {
207             activePackages = mActivePhonePackages;
208         } else {
209             return;
210         }
211 
212         if (active) {
213             activePackages.add(packageName);
214         } else {
215             activePackages.remove(packageName);
216         }
217 
218         updateLightSession();
219     }
220 
updateLightSession()221     private void updateLightSession() {
222         if (Looper.myLooper() != mHandler.getLooper()) {
223             mHandler.post(this::updateLightSession);
224             return;
225         }
226 
227         Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext);
228 
229         boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages)
230                 && exemptedPackages.containsAll(mActivePhonePackages);
231         updateSensorListener(shouldSessionEnd);
232 
233         if (shouldSessionEnd) {
234             if (mLightsSession == null) {
235                 return;
236             }
237 
238             mLightsSession.close();
239             mLightsSession = null;
240         } else {
241             int lightColor =
242                     mLightSensor == null ? mColors[mColors.length - 1] : computeCurrentLightColor();
243 
244             if (mLastLightColor == lightColor && mLightsSession != null) {
245                 return;
246             }
247             mLastLightColor = lightColor;
248 
249             LightsRequest.Builder requestBuilder = new LightsRequest.Builder();
250             for (int i = 0; i < mCameraLights.size(); i++) {
251                 requestBuilder.addLight(mCameraLights.get(i),
252                         new LightState.Builder()
253                                 .setColor(lightColor)
254                                 .build());
255             }
256 
257             if (mLightsSession == null) {
258                 mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
259             }
260 
261             mLightsSession.requestLights(requestBuilder.build());
262         }
263     }
264 
computeCurrentLightColor()265     private int computeCurrentLightColor() {
266         long liveAmbientLightTotal = getLiveAmbientLightTotal();
267         long currentInterval = getCurrentIntervalMillis();
268 
269         for (int i = 0; i < mThresholds.length; i++) {
270             if (liveAmbientLightTotal < currentInterval * mThresholds[i]) {
271                 return mColors[i];
272             }
273         }
274         return mColors[mColors.length - 1];
275     }
276 
updateSensorListener(boolean shouldSessionEnd)277     private void updateSensorListener(boolean shouldSessionEnd) {
278         if (shouldSessionEnd && mIsAmbientLightListenerRegistered) {
279             mSensorManager.unregisterListener(this);
280             mIsAmbientLightListenerRegistered = false;
281         }
282         if (!shouldSessionEnd && !mIsAmbientLightListenerRegistered && mLightSensor != null) {
283             mSensorManager.registerListener(this, mLightSensor, SENSOR_DELAY_NORMAL, mHandler);
284             mIsAmbientLightListenerRegistered = true;
285             mElapsedTimeStartedReading = getElapsedRealTime();
286         }
287     }
288 
getElapsedRealTime()289     private long getElapsedRealTime() {
290         return mElapsedRealTime == -1 ? SystemClock.elapsedRealtime() : mElapsedRealTime;
291     }
292 
293     @VisibleForTesting
setElapsedRealTime(long time)294     void setElapsedRealTime(long time) {
295         mElapsedRealTime = time;
296     }
297 
298     @Override
onSensorChanged(SensorEvent event)299     public void onSensorChanged(SensorEvent event) {
300         // Using log space to represent human sensation (Fechner's Law) instead of lux
301         // because lux values causes bright flashes to skew the average very high.
302         addElement(TimeUnit.NANOSECONDS.toMillis(event.timestamp), Math.max(0,
303                 (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
304         updateLightSession();
305         mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
306         mHandler.postDelayed(CameraPrivacyLightController.this::updateLightSession,
307                 mDelayedUpdateToken, mMovingAverageIntervalMillis);
308     }
309 
310     @Override
onAccuracyChanged(Sensor sensor, int accuracy)311     public void onAccuracyChanged(Sensor sensor, int accuracy) {}
312 
getCurrentIntervalMillis()313     private long getCurrentIntervalMillis() {
314         return Math.min(mMovingAverageIntervalMillis,
315                 getElapsedRealTime() - mElapsedTimeStartedReading);
316     }
317 }
318