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