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