1 /*
2  * Copyright (C) 2015 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 android.hardware.multiprocess.camera.cts;
18 
19 import android.app.Activity;
20 import android.app.ActivityManager;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.hardware.Camera;
24 import android.hardware.camera2.CameraAccessException;
25 import android.hardware.camera2.CameraDevice;
26 import android.hardware.camera2.CameraManager;
27 import android.hardware.cts.CameraCtsActivity;
28 import android.os.Handler;
29 import android.test.ActivityInstrumentationTestCase2;
30 import android.util.Log;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.List;
35 import java.util.Objects;
36 import java.util.concurrent.TimeoutException;
37 
38 import static org.mockito.Mockito.*;
39 
40 /**
41  * Tests for multi-process camera usage behavior.
42  */
43 public class CameraEvictionTest extends ActivityInstrumentationTestCase2<CameraCtsActivity> {
44 
45     public static final String TAG = "CameraEvictionTest";
46 
47     private static final int OPEN_TIMEOUT = 2000; // Timeout for camera to open (ms).
48     private static final int SETUP_TIMEOUT = 5000; // Remote camera setup timeout (ms).
49     private static final int EVICTION_TIMEOUT = 1000; // Remote camera eviction timeout (ms).
50     private static final int WAIT_TIME = 2000; // Time to wait for process to launch (ms).
51     private static final int UI_TIMEOUT = 10000; // Time to wait for UI event before timeout (ms).
52     ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection;
53 
54     private ActivityManager mActivityManager;
55     private Context mContext;
56     private Camera mCamera;
57     private CameraDevice mCameraDevice;
58     private final Object mLock = new Object();
59     private boolean mCompleted = false;
60     private int mProcessPid = -1;
61 
62     /** Load jni on initialization */
63     static {
64         System.loadLibrary("ctscamera2_jni");
65     }
66 
initializeAvailabilityCallbacksNative()67     private static native long initializeAvailabilityCallbacksNative();
getAccessCallbacksCountAndResetNative(long context)68     private static native int getAccessCallbacksCountAndResetNative(long context);
releaseAvailabilityCallbacksNative(long context)69     private static native long releaseAvailabilityCallbacksNative(long context);
70 
CameraEvictionTest()71     public CameraEvictionTest() {
72         super(CameraCtsActivity.class);
73     }
74 
75     public static class StateCallbackImpl extends CameraDevice.StateCallback {
76         CameraDevice mCameraDevice;
77 
StateCallbackImpl()78         public StateCallbackImpl() {
79             super();
80         }
81 
82         @Override
onOpened(CameraDevice cameraDevice)83         public void onOpened(CameraDevice cameraDevice) {
84             synchronized(this) {
85                 mCameraDevice = cameraDevice;
86             }
87             Log.i(TAG, "CameraDevice onOpened called for main CTS test process.");
88         }
89 
90         @Override
onClosed(CameraDevice camera)91         public void onClosed(CameraDevice camera) {
92             super.onClosed(camera);
93             synchronized(this) {
94                 mCameraDevice = null;
95             }
96             Log.i(TAG, "CameraDevice onClosed called for main CTS test process.");
97         }
98 
99         @Override
onDisconnected(CameraDevice cameraDevice)100         public void onDisconnected(CameraDevice cameraDevice) {
101             synchronized(this) {
102                 mCameraDevice = null;
103             }
104             Log.i(TAG, "CameraDevice onDisconnected called for main CTS test process.");
105 
106         }
107 
108         @Override
onError(CameraDevice cameraDevice, int i)109         public void onError(CameraDevice cameraDevice, int i) {
110             Log.i(TAG, "CameraDevice onError called for main CTS test process with error " +
111                     "code: " + i);
112         }
113 
getCameraDevice()114         public synchronized CameraDevice getCameraDevice() {
115             return mCameraDevice;
116         }
117     }
118 
119     @Override
setUp()120     protected void setUp() throws Exception {
121         super.setUp();
122 
123         mCompleted = false;
124         getActivity();
125         mContext = getInstrumentation().getTargetContext();
126         System.setProperty("dexmaker.dexcache", mContext.getCacheDir().toString());
127         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
128         mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(mContext);
129         mErrorServiceConnection.start();
130     }
131 
132     @Override
tearDown()133     protected void tearDown() throws Exception {
134         if (mProcessPid != -1) {
135             android.os.Process.killProcess(mProcessPid);
136             mProcessPid = -1;
137         }
138         if (mErrorServiceConnection != null) {
139             mErrorServiceConnection.stop();
140             mErrorServiceConnection = null;
141         }
142         if (mCamera != null) {
143             mCamera.release();
144             mCamera = null;
145         }
146         if (mCameraDevice != null) {
147             mCameraDevice.close();
148             mCameraDevice = null;
149         }
150         mContext = null;
151         mActivityManager = null;
152         super.tearDown();
153     }
154 
155     /**
156      * Test basic eviction scenarios for the Camera1 API.
157      */
testCamera1ActivityEviction()158     public void testCamera1ActivityEviction() throws Throwable {
159         testAPI1ActivityEviction(Camera1Activity.class, "camera1ActivityProcess");
160     }
161 
162     /**
163      * Test basic eviction scenarios for the Camera2 API.
164      */
testBasicCamera2ActivityEviction()165     public void testBasicCamera2ActivityEviction() throws Throwable {
166         CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
167         assertNotNull(manager);
168         String[] cameraIds = manager.getCameraIdList();
169 
170         if (cameraIds.length == 0) {
171             Log.i(TAG, "Skipping testBasicCamera2ActivityEviction, device has no cameras.");
172             return;
173         }
174 
175         assertTrue(mContext.getMainLooper() != null);
176 
177         // Setup camera manager
178         String chosenCamera = cameraIds[0];
179         Handler cameraHandler = new Handler(mContext.getMainLooper());
180         final CameraManager.AvailabilityCallback mockAvailCb =
181                 mock(CameraManager.AvailabilityCallback.class);
182 
183         manager.registerAvailabilityCallback(mockAvailCb, cameraHandler);
184 
185         Thread.sleep(WAIT_TIME);
186 
187         verify(mockAvailCb, times(1)).onCameraAvailable(chosenCamera);
188         verify(mockAvailCb, never()).onCameraUnavailable(chosenCamera);
189 
190         // Setup camera device
191         final CameraDevice.StateCallback spyStateCb = spy(new StateCallbackImpl());
192         manager.openCamera(chosenCamera, spyStateCb, cameraHandler);
193 
194         verify(spyStateCb, timeout(OPEN_TIMEOUT).times(1)).onOpened(any(CameraDevice.class));
195         verify(spyStateCb, never()).onClosed(any(CameraDevice.class));
196         verify(spyStateCb, never()).onDisconnected(any(CameraDevice.class));
197         verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt());
198 
199         // Open camera from remote process
200         startRemoteProcess(Camera2Activity.class, "camera2ActivityProcess");
201 
202         // Verify that the remote camera was opened correctly
203         List<ErrorLoggingService.LogEvent> allEvents  = mErrorServiceConnection.getLog(SETUP_TIMEOUT,
204                 TestConstants.EVENT_CAMERA_CONNECT);
205         assertNotNull("Camera device not setup in remote process!", allEvents);
206 
207         // Filter out relevant events for other camera devices
208         ArrayList<ErrorLoggingService.LogEvent> events = new ArrayList<>();
209         for (ErrorLoggingService.LogEvent e : allEvents) {
210             int eventTag = e.getEvent();
211             if (eventTag == TestConstants.EVENT_CAMERA_UNAVAILABLE ||
212                     eventTag == TestConstants.EVENT_CAMERA_CONNECT ||
213                     eventTag == TestConstants.EVENT_CAMERA_AVAILABLE) {
214                 if (!Objects.equals(e.getLogText(), chosenCamera)) {
215                     continue;
216                 }
217             }
218             events.add(e);
219         }
220         int[] eventList = new int[events.size()];
221         int eventIdx = 0;
222         for (ErrorLoggingService.LogEvent e : events) {
223             eventList[eventIdx++] = e.getEvent();
224         }
225         String[] actualEvents = TestConstants.convertToStringArray(eventList);
226         String[] expectedEvents = new String[] {TestConstants.EVENT_CAMERA_UNAVAILABLE_STR,
227                 TestConstants.EVENT_CAMERA_CONNECT_STR};
228         String[] ignoredEvents = new String[] { TestConstants.EVENT_CAMERA_AVAILABLE_STR,
229                 TestConstants.EVENT_CAMERA_UNAVAILABLE_STR };
230         assertOrderedEvents(actualEvents, expectedEvents, ignoredEvents);
231 
232         // Verify that the local camera was evicted properly
233         verify(spyStateCb, times(1)).onDisconnected(any(CameraDevice.class));
234         verify(spyStateCb, never()).onClosed(any(CameraDevice.class));
235         verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt());
236         verify(spyStateCb, times(1)).onOpened(any(CameraDevice.class));
237 
238         // Verify that we can no longer open the camera, as it is held by a higher priority process
239         try {
240             manager.openCamera(chosenCamera, spyStateCb, cameraHandler);
241             fail("Didn't receive exception when trying to open camera held by higher priority " +
242                     "process.");
243         } catch(CameraAccessException e) {
244             assertTrue("Received incorrect camera exception when opening camera: " + e,
245                     e.getReason() == CameraAccessException.CAMERA_IN_USE);
246         }
247 
248         // Verify that attempting to open the camera didn't cause anything weird to happen in the
249         // other process.
250         List<ErrorLoggingService.LogEvent> eventList2 = null;
251         boolean timeoutExceptionHit = false;
252         try {
253             eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT);
254         } catch (TimeoutException e) {
255             timeoutExceptionHit = true;
256         }
257 
258         assertNone("Remote camera service received invalid events: ", eventList2);
259         assertTrue("Remote camera service exited early", timeoutExceptionHit);
260         android.os.Process.killProcess(mProcessPid);
261         mProcessPid = -1;
262         forceCtsActivityToTop();
263     }
264 
265     /**
266      * Test camera availability access callback.
267      */
testCamera2AccessCallback()268     public void testCamera2AccessCallback() throws Throwable {
269         int PERMISSION_CALLBACK_TIMEOUT_MS = 2000;
270         CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
271         assertNotNull(manager);
272         String[] cameraIds = manager.getCameraIdList();
273 
274         if (cameraIds.length == 0) {
275             Log.i(TAG, "Skipping testCamera2AccessCallback, device has no cameras.");
276             return;
277         }
278 
279         assertTrue(mContext.getMainLooper() != null);
280 
281         // Setup camera manager
282         Handler cameraHandler = new Handler(mContext.getMainLooper());
283 
284         final CameraManager.AvailabilityCallback mockAvailCb =
285                 mock(CameraManager.AvailabilityCallback.class);
286         manager.registerAvailabilityCallback(mockAvailCb, cameraHandler);
287 
288         // Remove current task from top of stack. This will impact the camera access
289         // pririorties.
290         getActivity().moveTaskToBack(/*nonRoot*/true);
291 
292         verify(mockAvailCb, timeout(
293                 PERMISSION_CALLBACK_TIMEOUT_MS).atLeastOnce()).onCameraAccessPrioritiesChanged();
294 
295         forceCtsActivityToTop();
296 
297         verify(mockAvailCb, timeout(
298                 PERMISSION_CALLBACK_TIMEOUT_MS).atLeastOnce()).onCameraAccessPrioritiesChanged();
299     }
300 
301     /**
302      * Test native camera availability access callback.
303      */
testCamera2NativeAccessCallback()304     public void testCamera2NativeAccessCallback() throws Throwable {
305         int PERMISSION_CALLBACK_TIMEOUT_MS = 2000;
306         CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
307         assertNotNull(manager);
308         String[] cameraIds = manager.getCameraIdList();
309 
310         if (cameraIds.length == 0) {
311             Log.i(TAG, "Skipping testBasicCamera2AccessCallback, device has no cameras.");
312             return;
313         }
314 
315         // Setup camera manager
316         long context = 0;
317         try {
318             context = initializeAvailabilityCallbacksNative();
319             assertTrue("Failed to initialize native availability callbacks", (context != 0));
320 
321             // Remove current task from top of stack. This will impact the camera access
322             // pririorties.
323             getActivity().moveTaskToBack(/*nonRoot*/true);
324 
325             Thread.sleep(PERMISSION_CALLBACK_TIMEOUT_MS);
326             assertTrue("No camera permission access changed callback received",
327                     (getAccessCallbacksCountAndResetNative(context) > 0));
328 
329             forceCtsActivityToTop();
330 
331             assertTrue("No camera permission access changed callback received",
332                     (getAccessCallbacksCountAndResetNative(context) > 0));
333         } finally {
334             if (context != 0) {
335                 releaseAvailabilityCallbacksNative(context);
336             }
337         }
338     }
339 
340     /**
341      * Test basic eviction scenarios for camera used in MediaRecoder
342      */
testMediaRecorderCameraActivityEviction()343     public void testMediaRecorderCameraActivityEviction() throws Throwable {
344         testAPI1ActivityEviction(MediaRecorderCameraActivity.class,
345                 "mediaRecorderCameraActivityProcess");
346     }
347 
348     /**
349      * Test basic eviction scenarios for Camera1 API.
350      *
351      * This test will open camera, create a higher priority process to run the specified activity,
352      * open camera again, and verify the right clients are evicted.
353      *
354      * @param activityKlass An activity to run in a higher priority process.
355      * @param processName The process name.
356      */
testAPI1ActivityEviction(java.lang.Class<?> activityKlass, String processName)357     private void testAPI1ActivityEviction (java.lang.Class<?> activityKlass, String processName)
358             throws Throwable {
359         // Open a camera1 client in the main CTS process's activity
360         final Camera.ErrorCallback mockErrorCb1 = mock(Camera.ErrorCallback.class);
361         final boolean[] skip = {false};
362         runTestOnUiThread(new Runnable() {
363             @Override
364             public void run() {
365                 // Open camera
366                 mCamera = Camera.open();
367                 if (mCamera == null) {
368                     skip[0] = true;
369                 } else {
370                     mCamera.setErrorCallback(mockErrorCb1);
371                 }
372                 notifyFromUI();
373             }
374         });
375         waitForUI();
376 
377         if (skip[0]) {
378             Log.i(TAG, "Skipping testCamera1ActivityEviction, device has no cameras.");
379             return;
380         }
381 
382         verifyZeroInteractions(mockErrorCb1);
383 
384         startRemoteProcess(activityKlass, processName);
385 
386         // Make sure camera was setup correctly in remote activity
387         List<ErrorLoggingService.LogEvent> events = null;
388         try {
389             events = mErrorServiceConnection.getLog(SETUP_TIMEOUT,
390                     TestConstants.EVENT_CAMERA_CONNECT);
391         } finally {
392             if (events != null) assertOnly(TestConstants.EVENT_CAMERA_CONNECT, events);
393         }
394 
395         Thread.sleep(WAIT_TIME);
396 
397         // Ensure UI thread has a chance to process callbacks.
398         runTestOnUiThread(new Runnable() {
399             @Override
400             public void run() {
401                 Log.i("CTS", "Did something on UI thread.");
402                 notifyFromUI();
403             }
404         });
405         waitForUI();
406 
407         // Make sure we received correct callback in error listener, and nothing else
408         verify(mockErrorCb1, only()).onError(eq(Camera.CAMERA_ERROR_EVICTED), isA(Camera.class));
409         mCamera = null;
410 
411         // Try to open the camera again (even though other TOP process holds the camera).
412         final boolean[] pass = {false};
413         runTestOnUiThread(new Runnable() {
414             @Override
415             public void run() {
416                 // Open camera
417                 try {
418                     mCamera = Camera.open();
419                 } catch (RuntimeException e) {
420                     pass[0] = true;
421                 }
422                 notifyFromUI();
423             }
424         });
425         waitForUI();
426 
427         assertTrue("Did not receive exception when opening camera while camera is held by a" +
428                 " higher priority client process.", pass[0]);
429 
430         // Verify that attempting to open the camera didn't cause anything weird to happen in the
431         // other process.
432         List<ErrorLoggingService.LogEvent> eventList2 = null;
433         boolean timeoutExceptionHit = false;
434         try {
435             eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT);
436         } catch (TimeoutException e) {
437             timeoutExceptionHit = true;
438         }
439 
440         assertNone("Remote camera service received invalid events: ", eventList2);
441         assertTrue("Remote camera service exited early", timeoutExceptionHit);
442         android.os.Process.killProcess(mProcessPid);
443         mProcessPid = -1;
444         forceCtsActivityToTop();
445     }
446 
447     /**
448      * Ensure the CTS activity becomes foreground again instead of launcher.
449      */
forceCtsActivityToTop()450     private void forceCtsActivityToTop() throws InterruptedException {
451         Thread.sleep(WAIT_TIME);
452         Activity a = getActivity();
453         Intent activityIntent = new Intent(a, CameraCtsActivity.class);
454         activityIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
455         a.startActivity(activityIntent);
456         Thread.sleep(WAIT_TIME);
457     }
458 
459     /**
460      * Block until UI thread calls {@link #notifyFromUI()}.
461      * @throws InterruptedException
462      */
waitForUI()463     private void waitForUI() throws InterruptedException {
464         synchronized(mLock) {
465             if (mCompleted) return;
466             while (!mCompleted) {
467                 mLock.wait();
468             }
469             mCompleted = false;
470         }
471     }
472 
473     /**
474      * Wake up any threads waiting in calls to {@link #waitForUI()}.
475      */
notifyFromUI()476     private void notifyFromUI() {
477         synchronized (mLock) {
478             mCompleted = true;
479             mLock.notifyAll();
480         }
481     }
482 
483     /**
484      * Return the PID for the process with the given name in the given list of process info.
485      *
486      * @param processName the name of the process who's PID to return.
487      * @param list a list of {@link ActivityManager.RunningAppProcessInfo} to check.
488      * @return the PID of the given process, or -1 if it was not included in the list.
489      */
getPid(String processName, List<ActivityManager.RunningAppProcessInfo> list)490     private static int getPid(String processName,
491                               List<ActivityManager.RunningAppProcessInfo> list) {
492         for (ActivityManager.RunningAppProcessInfo rai : list) {
493             if (processName.equals(rai.processName))
494                 return rai.pid;
495         }
496         return -1;
497     }
498 
499     /**
500      * Start an activity of the given class running in a remote process with the given name.
501      *
502      * @param klass the class of the {@link android.app.Activity} to start.
503      * @param processName the remote activity name.
504      * @throws InterruptedException
505      */
startRemoteProcess(java.lang.Class<?> klass, String processName)506     public void startRemoteProcess(java.lang.Class<?> klass, String processName)
507             throws InterruptedException {
508         // Ensure no running activity process with same name
509         Activity a = getActivity();
510         String cameraActivityName = a.getPackageName() + ":" + processName;
511         List<ActivityManager.RunningAppProcessInfo> list =
512                 mActivityManager.getRunningAppProcesses();
513         assertEquals(-1, getPid(cameraActivityName, list));
514 
515         // Start activity in a new top foreground process
516         Intent activityIntent = new Intent(a, klass);
517         a.startActivity(activityIntent);
518         Thread.sleep(WAIT_TIME);
519 
520         // Fail if activity isn't running
521         list = mActivityManager.getRunningAppProcesses();
522         mProcessPid = getPid(cameraActivityName, list);
523         assertTrue(-1 != mProcessPid);
524     }
525 
526     /**
527      * Assert that there is only one event of the given type in the event list.
528      *
529      * @param event event type to check for.
530      * @param events {@link List} of events.
531      */
assertOnly(int event, List<ErrorLoggingService.LogEvent> events)532     public static void assertOnly(int event, List<ErrorLoggingService.LogEvent> events) {
533         assertTrue("Remote camera activity never received event: " + event, events != null);
534         for (ErrorLoggingService.LogEvent e : events) {
535             assertFalse("Remote camera activity received invalid event (" + e +
536                     ") while waiting for event: " + event,
537                     e.getEvent() < 0 || e.getEvent() != event);
538         }
539         assertTrue("Remote camera activity never received event: " + event, events.size() >= 1);
540         assertTrue("Remote camera activity received too many " + event + " events, received: " +
541                 events.size(), events.size() == 1);
542     }
543 
544     /**
545      * Assert there were no logEvents in the given list.
546      *
547      * @param msg message to show on assertion failure.
548      * @param events {@link List} of events.
549      */
assertNone(String msg, List<ErrorLoggingService.LogEvent> events)550     public static void assertNone(String msg, List<ErrorLoggingService.LogEvent> events) {
551         if (events == null) return;
552         StringBuilder builder = new StringBuilder(msg + "\n");
553         for (ErrorLoggingService.LogEvent e : events) {
554             builder.append(e).append("\n");
555         }
556         assertTrue(builder.toString(), events.isEmpty());
557     }
558 
559     /**
560      * Assert array is null or empty.
561      *
562      * @param array array to check.
563      */
assertNotEmpty(T[] array)564     public static <T> void assertNotEmpty(T[] array) {
565         assertNotNull(array);
566         assertFalse("Array is empty: " + Arrays.toString(array), array.length == 0);
567     }
568 
569     /**
570      * Given an 'actual' array of objects, check that the objects given in the 'expected'
571      * array are also present in the 'actual' array in the same order.  Objects in the 'actual'
572      * array that are not in the 'expected' array are skipped and ignored if they are given
573      * in the 'ignored' array, otherwise this assertion will fail.
574      *
575      * @param actual the ordered array of objects to check.
576      * @param expected the ordered array of expected objects.
577      * @param ignored the array of objects that will be ignored if present in actual,
578      *                but not in expected (or are out of order).
579      * @param <T>
580      */
assertOrderedEvents(T[] actual, T[] expected, T[] ignored)581     public static <T> void assertOrderedEvents(T[] actual, T[] expected, T[] ignored) {
582         assertNotNull(actual);
583         assertNotNull(expected);
584         assertNotNull(ignored);
585 
586         int expIndex = 0;
587         int index = 0;
588         for (T i : actual) {
589             // If explicitly expected, move to next
590             if (expIndex < expected.length && Objects.equals(i, expected[expIndex])) {
591                 expIndex++;
592                 continue;
593             }
594 
595             // Fail if not ignored
596             boolean canIgnore = false;
597             for (T j : ignored) {
598                 if (Objects.equals(i, j)) {
599                     canIgnore = true;
600                     break;
601                 }
602 
603             }
604 
605             // Fail if not ignored.
606             assertTrue("Event at index " + index + " in actual array " +
607                     Arrays.toString(actual) + " was unexpected: expected array was " +
608                     Arrays.toString(expected) + ", ignored array was: " +
609                     Arrays.toString(ignored), canIgnore);
610             index++;
611         }
612         assertTrue("Only had " + expIndex + " of " + expected.length +
613                 " expected objects in array " + Arrays.toString(actual) + ", expected was " +
614                 Arrays.toString(expected), expIndex == expected.length);
615     }
616 }
617