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