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.app.AppOpsManager.OPSTR_CAMERA; 20 21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 22 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; 23 24 import static org.junit.Assert.assertEquals; 25 import static org.mockito.ArgumentMatchers.any; 26 import static org.mockito.ArgumentMatchers.anyInt; 27 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.Mockito.atLeastOnce; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.never; 31 import static org.mockito.Mockito.times; 32 import static org.mockito.Mockito.verifyZeroInteractions; 33 34 import android.app.AppOpsManager; 35 import android.content.Context; 36 import android.content.res.Resources; 37 import android.hardware.Sensor; 38 import android.hardware.SensorEvent; 39 import android.hardware.SensorEventListener; 40 import android.hardware.SensorManager; 41 import android.hardware.lights.Light; 42 import android.hardware.lights.LightState; 43 import android.hardware.lights.LightsManager; 44 import android.hardware.lights.LightsRequest; 45 import android.os.Handler; 46 import android.os.Looper; 47 import android.permission.PermissionManager; 48 import android.testing.TestableLooper; 49 50 import com.android.dx.mockito.inline.extended.ExtendedMockito; 51 import com.android.internal.R; 52 53 import org.junit.After; 54 import org.junit.Before; 55 import org.junit.Test; 56 import org.mockito.ArgumentCaptor; 57 import org.mockito.Mock; 58 import org.mockito.MockitoSession; 59 import org.mockito.quality.Strictness; 60 61 import java.util.ArrayList; 62 import java.util.Collections; 63 import java.util.List; 64 import java.util.Random; 65 import java.util.Set; 66 import java.util.concurrent.TimeUnit; 67 import java.util.stream.Collectors; 68 69 public class CameraPrivacyLightControllerTest { 70 private int[] mDefaultColors = {0, 1, 2}; 71 private int[] mDefaultAlsThresholdsLux = {10, 50}; 72 private int mDefaultAlsAveragingIntervalMillis = 5000; 73 74 private TestableLooper mTestableLooper; 75 76 private MockitoSession mMockitoSession; 77 78 @Mock 79 private LightsManager mLightsManager; 80 81 @Mock 82 private AppOpsManager mAppOpsManager; 83 84 @Mock 85 private SensorManager mSensorManager; 86 87 @Mock 88 private LightsManager.LightsSession mLightsSession; 89 90 @Mock 91 private Sensor mLightSensor; 92 93 private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor = 94 ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class); 95 96 private ArgumentCaptor<LightsRequest> mLightsRequestCaptor = 97 ArgumentCaptor.forClass(LightsRequest.class); 98 99 private ArgumentCaptor<SensorEventListener> mLightSensorListenerCaptor = 100 ArgumentCaptor.forClass(SensorEventListener.class); 101 102 private Set<String> mExemptedPackages; 103 private List<Light> mLights; 104 105 private int mNextLightId = 1; 106 prepareDefaultCameraPrivacyLightController()107 public CameraPrivacyLightController prepareDefaultCameraPrivacyLightController() { 108 return prepareDefaultCameraPrivacyLightController(List.of(getNextLight(true))); 109 } 110 prepareDefaultCameraPrivacyLightController( List<Light> lights)111 public CameraPrivacyLightController prepareDefaultCameraPrivacyLightController( 112 List<Light> lights) { 113 return prepareCameraPrivacyLightController(lights, Set.of(), true, mDefaultColors, 114 mDefaultAlsThresholdsLux, mDefaultAlsAveragingIntervalMillis); 115 } 116 prepareCameraPrivacyLightController(List<Light> lights, Set<String> exemptedPackages, boolean hasLightSensor, int[] lightColors, int[] alsThresholds, int averagingInterval)117 public CameraPrivacyLightController prepareCameraPrivacyLightController(List<Light> lights, 118 Set<String> exemptedPackages, boolean hasLightSensor, int[] lightColors, 119 int[] alsThresholds, int averagingInterval) { 120 Looper looper = Looper.myLooper(); 121 if (looper == null) { 122 Looper.prepare(); 123 looper = Looper.myLooper(); 124 } 125 if (mTestableLooper == null) { 126 try { 127 mTestableLooper = new TestableLooper(looper); 128 } catch (Exception e) { 129 throw new RuntimeException(e); 130 } 131 } 132 133 Context context = mock(Context.class); 134 Resources resources = mock(Resources.class); 135 doReturn(resources).when(context).getResources(); 136 doReturn(lightColors).when(resources).getIntArray(R.array.config_cameraPrivacyLightColors); 137 doReturn(alsThresholds).when(resources) 138 .getIntArray(R.array.config_cameraPrivacyLightAlsLuxThresholds); 139 doReturn(averagingInterval).when(resources) 140 .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis); 141 142 doReturn(mLightsManager).when(context).getSystemService(LightsManager.class); 143 doReturn(mAppOpsManager).when(context).getSystemService(AppOpsManager.class); 144 doReturn(mSensorManager).when(context).getSystemService(SensorManager.class); 145 146 mLights = lights; 147 mExemptedPackages = exemptedPackages; 148 doReturn(mLights).when(mLightsManager).getLights(); 149 doReturn(mLightsSession).when(mLightsManager).openSession(anyInt()); 150 if (!hasLightSensor) { 151 mLightSensor = null; 152 } 153 doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); 154 doReturn(exemptedPackages) 155 .when(() -> PermissionManager.getIndicatorExemptedPackages(any())); 156 157 return new CameraPrivacyLightController(context, looper); 158 } 159 160 @Before setUp()161 public void setUp() { 162 mMockitoSession = ExtendedMockito.mockitoSession() 163 .initMocks(this) 164 .strictness(Strictness.WARN) 165 .spyStatic(PermissionManager.class) 166 .startMocking(); 167 } 168 169 @After tearDown()170 public void tearDown() { 171 mMockitoSession.finishMocking(); 172 } 173 174 @Test testNoInteractionsWithServicesIfNoColorsSpecified()175 public void testNoInteractionsWithServicesIfNoColorsSpecified() { 176 prepareCameraPrivacyLightController(List.of(getNextLight(true)), Collections.EMPTY_SET, 177 true, new int[0], mDefaultAlsThresholdsLux, mDefaultAlsAveragingIntervalMillis); 178 179 verifyZeroInteractions(mLightsManager); 180 verifyZeroInteractions(mAppOpsManager); 181 verifyZeroInteractions(mSensorManager); 182 } 183 184 @Test testAppsOpsListenerNotRegisteredWithoutCameraLights()185 public void testAppsOpsListenerNotRegisteredWithoutCameraLights() { 186 prepareDefaultCameraPrivacyLightController(List.of(getNextLight(false))); 187 188 verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any()); 189 } 190 191 @Test testAppsOpsListenerRegisteredWithCameraLight()192 public void testAppsOpsListenerRegisteredWithCameraLight() { 193 prepareDefaultCameraPrivacyLightController(); 194 195 verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any()); 196 } 197 198 @Test testAllCameraLightsAreRequestedOnOpActive()199 public void testAllCameraLightsAreRequestedOnOpActive() { 200 Random r = new Random(0); 201 List<Light> lights = new ArrayList<>(); 202 for (int i = 0; i < 30; i++) { 203 lights.add(getNextLight(r.nextBoolean())); 204 } 205 206 prepareDefaultCameraPrivacyLightController(lights); 207 208 // Verify no session has been opened at this point. 209 verify(mLightsManager, times(0)).openSession(anyInt()); 210 211 // Set camera op as active. 212 openCamera(); 213 214 // Verify session has been opened exactly once 215 verify(mLightsManager, times(1)).openSession(anyInt()); 216 217 verify(mLightsSession).requestLights(mLightsRequestCaptor.capture()); 218 219 List<Integer> expectedCameraLightIds = mLights.stream() 220 .filter(l -> l.getType() == Light.LIGHT_TYPE_CAMERA) 221 .map(l -> l.getId()) 222 .collect(Collectors.toList()); 223 List<Integer> lightsRequestLightIds = mLightsRequestCaptor.getValue().getLights(); 224 225 // We don't own lights framework, don't assume it will retain order 226 lightsRequestLightIds.sort(Integer::compare); 227 expectedCameraLightIds.sort(Integer::compare); 228 229 assertEquals(expectedCameraLightIds, lightsRequestLightIds); 230 } 231 232 @Test testWillOnlyOpenOnceWhenTwoPackagesStartOp()233 public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() { 234 prepareDefaultCameraPrivacyLightController(); 235 notifyCamOpChanged(10101, "pkg1", true); 236 verify(mLightsManager, times(1)).openSession(anyInt()); 237 notifyCamOpChanged(10102, "pkg2", true); 238 verify(mLightsManager, times(1)).openSession(anyInt()); 239 } 240 241 @Test testWillCloseOnFinishOp()242 public void testWillCloseOnFinishOp() { 243 prepareDefaultCameraPrivacyLightController(); 244 notifyCamOpChanged(10101, "pkg1", true); 245 verify(mLightsSession, times(0)).close(); 246 notifyCamOpChanged(10101, "pkg1", false); 247 verify(mLightsSession, times(1)).close(); 248 } 249 250 @Test testWillCloseOnFinishOpForAllPackages()251 public void testWillCloseOnFinishOpForAllPackages() { 252 prepareDefaultCameraPrivacyLightController(); 253 254 int numUids = 100; 255 List<Integer> uids = new ArrayList<>(numUids); 256 for (int i = 0; i < numUids; i++) { 257 uids.add(10001 + i); 258 } 259 260 for (int i = 0; i < numUids; i++) { 261 notifyCamOpChanged(uids.get(i), "pkg" + (int) uids.get(i), true); 262 } 263 264 // Change the order which their ops are finished 265 Collections.shuffle(uids, new Random(0)); 266 267 for (int i = 0; i < numUids - 1; i++) { 268 notifyCamOpChanged(uids.get(i), "pkg" + (int) uids.get(i), false); 269 } 270 271 verify(mLightsSession, times(0)).close(); 272 int lastUid = uids.get(numUids - 1); 273 notifyCamOpChanged(lastUid, "pkg" + lastUid, false); 274 verify(mLightsSession, times(1)).close(); 275 } 276 277 @Test testWontOpenForExemptedPackage()278 public void testWontOpenForExemptedPackage() { 279 String exemptPackage = "pkg1"; 280 prepareCameraPrivacyLightController(List.of(getNextLight(true)), 281 Set.of(exemptPackage), true, mDefaultColors, mDefaultAlsThresholdsLux, 282 mDefaultAlsAveragingIntervalMillis); 283 284 notifyCamOpChanged(10101, exemptPackage, true); 285 verify(mLightsManager, times(0)).openSession(anyInt()); 286 } 287 288 @Test testNoLightSensor()289 public void testNoLightSensor() { 290 prepareCameraPrivacyLightController(List.of(getNextLight(true)), 291 Set.of(), true, mDefaultColors, mDefaultAlsThresholdsLux, 292 mDefaultAlsAveragingIntervalMillis); 293 294 openCamera(); 295 296 verify(mLightsSession).requestLights(mLightsRequestCaptor.capture()); 297 LightsRequest lightsRequest = mLightsRequestCaptor.getValue(); 298 for (LightState lightState : lightsRequest.getLightStates()) { 299 assertEquals(mDefaultColors[mDefaultColors.length - 1], lightState.getColor()); 300 } 301 } 302 303 @Test testALSListenerNotRegisteredUntilCameraIsOpened()304 public void testALSListenerNotRegisteredUntilCameraIsOpened() { 305 prepareDefaultCameraPrivacyLightController(); 306 307 verify(mSensorManager, never()).registerListener(any(SensorEventListener.class), 308 any(Sensor.class), anyInt(), any(Handler.class)); 309 310 openCamera(); 311 312 verify(mSensorManager, times(1)).registerListener(mLightSensorListenerCaptor.capture(), 313 any(Sensor.class), anyInt(), any(Handler.class)); 314 315 notifyCamOpChanged(10001, "pkg", false); 316 verify(mSensorManager, times(1)).unregisterListener(mLightSensorListenerCaptor.getValue()); 317 } 318 319 @Test testAlsThresholds()320 public void testAlsThresholds() { 321 CameraPrivacyLightController cplc = prepareDefaultCameraPrivacyLightController(); 322 long elapsedTime = 0; 323 cplc.setElapsedRealTime(0); 324 openCamera(); 325 for (int i = 0; i < mDefaultColors.length; i++) { 326 int expectedColor = mDefaultColors[i]; 327 int alsLuxValue = i 328 == mDefaultAlsThresholdsLux.length 329 ? mDefaultAlsThresholdsLux[i - 1] : mDefaultAlsThresholdsLux[i] - 1; 330 331 notifySensorEvent(cplc, elapsedTime, alsLuxValue); 332 elapsedTime += mDefaultAlsAveragingIntervalMillis + 1; 333 notifySensorEvent(cplc, elapsedTime, alsLuxValue); 334 335 verify(mLightsSession, atLeastOnce()).requestLights(mLightsRequestCaptor.capture()); 336 for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { 337 assertEquals(expectedColor, lightState.getColor()); 338 } 339 } 340 } 341 notifyCamOpChanged(int uid, String pkg, boolean active)342 private void notifyCamOpChanged(int uid, String pkg, boolean active) { 343 verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); 344 mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, uid, pkg, active); 345 } 346 notifySensorEvent(CameraPrivacyLightController cplc, long timestamp, int value)347 private void notifySensorEvent(CameraPrivacyLightController cplc, long timestamp, int value) { 348 cplc.setElapsedRealTime(timestamp); 349 verify(mSensorManager, atLeastOnce()).registerListener(mLightSensorListenerCaptor.capture(), 350 eq(mLightSensor), anyInt(), any()); 351 mLightSensorListenerCaptor.getValue().onSensorChanged(new SensorEvent(mLightSensor, 0, 352 TimeUnit.MILLISECONDS.toNanos(timestamp), new float[] {value})); 353 } 354 getNextLight(boolean cameraType)355 private Light getNextLight(boolean cameraType) { 356 Light light = ExtendedMockito.mock(Light.class); 357 if (cameraType) { 358 doReturn(Light.LIGHT_TYPE_CAMERA).when(light).getType(); 359 } else { 360 doReturn(Light.LIGHT_TYPE_MICROPHONE).when(light).getType(); 361 } 362 doReturn(mNextLightId++).when(light).getId(); 363 return light; 364 } 365 openCamera()366 private void openCamera() { 367 verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); 368 mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", true); 369 } 370 } 371