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