| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.hardware.multiprocess.camera.cts; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.hardware.Camera; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.CameraManager; |
| import android.hardware.cts.CameraCtsActivity; |
| import android.hardware.multiprocess.ErrorLoggingService; |
| import android.os.Handler; |
| import android.test.ActivityInstrumentationTestCase2; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.TimeoutException; |
| |
| import static org.mockito.Mockito.*; |
| |
| /** |
| * Tests for multi-process camera usage behavior. |
| */ |
| public class CameraEvictionTest extends ActivityInstrumentationTestCase2<CameraCtsActivity> { |
| |
| public static final String TAG = "CameraEvictionTest"; |
| |
| private static final int OPEN_TIMEOUT = 2000; // Timeout for camera to open (ms). |
| private static final int SETUP_TIMEOUT = 5000; // Remote camera setup timeout (ms). |
| private static final int EVICTION_TIMEOUT = 1000; // Remote camera eviction timeout (ms). |
| private static final int WAIT_TIME = 2000; // Time to wait for process to launch (ms). |
| private static final int UI_TIMEOUT = 10000; // Time to wait for UI event before timeout (ms). |
| ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection; |
| |
| private ActivityManager mActivityManager; |
| private Context mContext; |
| private Camera mCamera; |
| private CameraDevice mCameraDevice; |
| private final Object mLock = new Object(); |
| private boolean mCompleted = false; |
| private int mProcessPid = -1; |
| |
| public CameraEvictionTest() { |
| super(CameraCtsActivity.class); |
| } |
| |
| public static class StateCallbackImpl extends CameraDevice.StateCallback { |
| CameraDevice mCameraDevice; |
| |
| public StateCallbackImpl() { |
| super(); |
| } |
| |
| @Override |
| public void onOpened(CameraDevice cameraDevice) { |
| synchronized(this) { |
| mCameraDevice = cameraDevice; |
| } |
| Log.i(TAG, "CameraDevice onOpened called for main CTS test process."); |
| } |
| |
| @Override |
| public void onClosed(CameraDevice camera) { |
| super.onClosed(camera); |
| synchronized(this) { |
| mCameraDevice = null; |
| } |
| Log.i(TAG, "CameraDevice onClosed called for main CTS test process."); |
| } |
| |
| @Override |
| public void onDisconnected(CameraDevice cameraDevice) { |
| synchronized(this) { |
| mCameraDevice = null; |
| } |
| Log.i(TAG, "CameraDevice onDisconnected called for main CTS test process."); |
| |
| } |
| |
| @Override |
| public void onError(CameraDevice cameraDevice, int i) { |
| Log.i(TAG, "CameraDevice onError called for main CTS test process with error " + |
| "code: " + i); |
| } |
| |
| public synchronized CameraDevice getCameraDevice() { |
| return mCameraDevice; |
| } |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| |
| mCompleted = false; |
| getActivity(); |
| mContext = getInstrumentation().getTargetContext(); |
| System.setProperty("dexmaker.dexcache", mContext.getCacheDir().toString()); |
| mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); |
| mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(mContext); |
| mErrorServiceConnection.start(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| if (mProcessPid != -1) { |
| android.os.Process.killProcess(mProcessPid); |
| mProcessPid = -1; |
| } |
| if (mErrorServiceConnection != null) { |
| mErrorServiceConnection.stop(); |
| mErrorServiceConnection = null; |
| } |
| if (mCamera != null) { |
| mCamera.release(); |
| mCamera = null; |
| } |
| if (mCameraDevice != null) { |
| mCameraDevice.close(); |
| mCameraDevice = null; |
| } |
| mContext = null; |
| mActivityManager = null; |
| super.tearDown(); |
| } |
| |
| /** |
| * Test basic eviction scenarios for the Camera1 API. |
| */ |
| public void testCamera1ActivityEviction() throws Throwable { |
| |
| // Open a camera1 client in the main CTS process's activity |
| final Camera.ErrorCallback mockErrorCb1 = mock(Camera.ErrorCallback.class); |
| final boolean[] skip = {false}; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| // Open camera |
| mCamera = Camera.open(); |
| if (mCamera == null) { |
| skip[0] = true; |
| } else { |
| mCamera.setErrorCallback(mockErrorCb1); |
| } |
| notifyFromUI(); |
| } |
| }); |
| waitForUI(); |
| |
| if (skip[0]) { |
| Log.i(TAG, "Skipping testCamera1ActivityEviction, device has no cameras."); |
| return; |
| } |
| |
| verifyZeroInteractions(mockErrorCb1); |
| |
| startRemoteProcess(Camera1Activity.class, "camera1ActivityProcess"); |
| |
| // Make sure camera was setup correctly in remote activity |
| List<ErrorLoggingService.LogEvent> events = null; |
| try { |
| events = mErrorServiceConnection.getLog(SETUP_TIMEOUT, |
| TestConstants.EVENT_CAMERA_CONNECT); |
| } finally { |
| if (events != null) assertOnly(TestConstants.EVENT_CAMERA_CONNECT, events); |
| } |
| |
| Thread.sleep(WAIT_TIME); |
| |
| // Ensure UI thread has a chance to process callbacks. |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| Log.i("CTS", "Did something on UI thread."); |
| notifyFromUI(); |
| } |
| }); |
| waitForUI(); |
| |
| // Make sure we received correct callback in error listener, and nothing else |
| verify(mockErrorCb1, only()).onError(eq(Camera.CAMERA_ERROR_EVICTED), isA(Camera.class)); |
| mCamera = null; |
| |
| // Try to open the camera again (even though other TOP process holds the camera). |
| final boolean[] pass = {false}; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| // Open camera |
| try { |
| mCamera = Camera.open(); |
| } catch (RuntimeException e) { |
| pass[0] = true; |
| } |
| notifyFromUI(); |
| } |
| }); |
| waitForUI(); |
| |
| assertTrue("Did not receive exception when opening camera while camera is held by a" + |
| " higher priority client process.", pass[0]); |
| |
| // Verify that attempting to open the camera didn't cause anything weird to happen in the |
| // other process. |
| List<ErrorLoggingService.LogEvent> eventList2 = null; |
| boolean timeoutExceptionHit = false; |
| try { |
| eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT); |
| } catch (TimeoutException e) { |
| timeoutExceptionHit = true; |
| } |
| |
| assertNone("Remote camera service received invalid events: ", eventList2); |
| assertTrue("Remote camera service exited early", timeoutExceptionHit); |
| android.os.Process.killProcess(mProcessPid); |
| mProcessPid = -1; |
| forceCtsActivityToTop(); |
| } |
| |
| /** |
| * Test basic eviction scenarios for the Camera2 API. |
| */ |
| public void testBasicCamera2ActivityEviction() throws Throwable { |
| CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); |
| assertNotNull(manager); |
| String[] cameraIds = manager.getCameraIdList(); |
| |
| if (cameraIds.length == 0) { |
| Log.i(TAG, "Skipping testBasicCamera2ActivityEviction, device has no cameras."); |
| return; |
| } |
| |
| assertTrue(mContext.getMainLooper() != null); |
| |
| // Setup camera manager |
| String chosenCamera = cameraIds[0]; |
| Handler cameraHandler = new Handler(mContext.getMainLooper()); |
| final CameraManager.AvailabilityCallback mockAvailCb = |
| mock(CameraManager.AvailabilityCallback.class); |
| |
| manager.registerAvailabilityCallback(mockAvailCb, cameraHandler); |
| |
| Thread.sleep(WAIT_TIME); |
| |
| verify(mockAvailCb, times(1)).onCameraAvailable(chosenCamera); |
| verify(mockAvailCb, never()).onCameraUnavailable(chosenCamera); |
| |
| // Setup camera device |
| final CameraDevice.StateCallback spyStateCb = spy(new StateCallbackImpl()); |
| manager.openCamera(chosenCamera, spyStateCb, cameraHandler); |
| |
| verify(spyStateCb, timeout(OPEN_TIMEOUT).times(1)).onOpened(any(CameraDevice.class)); |
| verify(spyStateCb, never()).onClosed(any(CameraDevice.class)); |
| verify(spyStateCb, never()).onDisconnected(any(CameraDevice.class)); |
| verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt()); |
| |
| // Open camera from remote process |
| startRemoteProcess(Camera2Activity.class, "camera2ActivityProcess"); |
| |
| // Verify that the remote camera was opened correctly |
| List<ErrorLoggingService.LogEvent> allEvents = mErrorServiceConnection.getLog(SETUP_TIMEOUT, |
| TestConstants.EVENT_CAMERA_CONNECT); |
| assertNotNull("Camera device not setup in remote process!", allEvents); |
| |
| // Filter out relevant events for other camera devices |
| ArrayList<ErrorLoggingService.LogEvent> events = new ArrayList<>(); |
| for (ErrorLoggingService.LogEvent e : allEvents) { |
| int eventTag = e.getEvent(); |
| if (eventTag == TestConstants.EVENT_CAMERA_UNAVAILABLE || |
| eventTag == TestConstants.EVENT_CAMERA_CONNECT || |
| eventTag == TestConstants.EVENT_CAMERA_AVAILABLE) { |
| if (!Objects.equals(e.getLogText(), chosenCamera)) { |
| continue; |
| } |
| } |
| events.add(e); |
| } |
| int[] eventList = new int[events.size()]; |
| int eventIdx = 0; |
| for (ErrorLoggingService.LogEvent e : events) { |
| eventList[eventIdx++] = e.getEvent(); |
| } |
| String[] actualEvents = TestConstants.convertToStringArray(eventList); |
| String[] expectedEvents = new String[] {TestConstants.EVENT_CAMERA_UNAVAILABLE_STR, |
| TestConstants.EVENT_CAMERA_CONNECT_STR}; |
| String[] ignoredEvents = new String[] { TestConstants.EVENT_CAMERA_AVAILABLE_STR, |
| TestConstants.EVENT_CAMERA_UNAVAILABLE_STR }; |
| assertOrderedEvents(actualEvents, expectedEvents, ignoredEvents); |
| |
| // Verify that the local camera was evicted properly |
| verify(spyStateCb, times(1)).onDisconnected(any(CameraDevice.class)); |
| verify(spyStateCb, never()).onClosed(any(CameraDevice.class)); |
| verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt()); |
| verify(spyStateCb, times(1)).onOpened(any(CameraDevice.class)); |
| |
| // Verify that we can no longer open the camera, as it is held by a higher priority process |
| boolean openException = false; |
| try { |
| manager.openCamera(chosenCamera, spyStateCb, cameraHandler); |
| } catch(CameraAccessException e) { |
| assertTrue("Received incorrect camera exception when opening camera: " + e, |
| e.getReason() == CameraAccessException.CAMERA_IN_USE); |
| openException = true; |
| } |
| |
| assertTrue("Didn't receive exception when trying to open camera held by higher priority " + |
| "process.", openException); |
| |
| // Verify that attempting to open the camera didn't cause anything weird to happen in the |
| // other process. |
| List<ErrorLoggingService.LogEvent> eventList2 = null; |
| boolean timeoutExceptionHit = false; |
| try { |
| eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT); |
| } catch (TimeoutException e) { |
| timeoutExceptionHit = true; |
| } |
| |
| assertNone("Remote camera service received invalid events: ", eventList2); |
| assertTrue("Remote camera service exited early", timeoutExceptionHit); |
| android.os.Process.killProcess(mProcessPid); |
| mProcessPid = -1; |
| forceCtsActivityToTop(); |
| } |
| |
| /** |
| * Ensure the CTS activity becomes foreground again instead of launcher. |
| */ |
| private void forceCtsActivityToTop() throws InterruptedException { |
| Thread.sleep(WAIT_TIME); |
| Activity a = getActivity(); |
| Intent activityIntent = new Intent(a, CameraCtsActivity.class); |
| activityIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| a.startActivity(activityIntent); |
| Thread.sleep(WAIT_TIME); |
| } |
| |
| /** |
| * Block until UI thread calls {@link #notifyFromUI()}. |
| * @throws InterruptedException |
| */ |
| private void waitForUI() throws InterruptedException { |
| synchronized(mLock) { |
| if (mCompleted) return; |
| while (!mCompleted) { |
| mLock.wait(); |
| } |
| mCompleted = false; |
| } |
| } |
| |
| /** |
| * Wake up any threads waiting in calls to {@link #waitForUI()}. |
| */ |
| private void notifyFromUI() { |
| synchronized (mLock) { |
| mCompleted = true; |
| mLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Return the PID for the process with the given name in the given list of process info. |
| * |
| * @param processName the name of the process who's PID to return. |
| * @param list a list of {@link ActivityManager.RunningAppProcessInfo} to check. |
| * @return the PID of the given process, or -1 if it was not included in the list. |
| */ |
| private static int getPid(String processName, |
| List<ActivityManager.RunningAppProcessInfo> list) { |
| for (ActivityManager.RunningAppProcessInfo rai : list) { |
| if (processName.equals(rai.processName)) |
| return rai.pid; |
| } |
| return -1; |
| } |
| |
| /** |
| * Start an activity of the given class running in a remote process with the given name. |
| * |
| * @param klass the class of the {@link android.app.Activity} to start. |
| * @param processName the remote activity name. |
| * @throws InterruptedException |
| */ |
| public void startRemoteProcess(java.lang.Class<?> klass, String processName) |
| throws InterruptedException { |
| // Ensure no running activity process with same name |
| Activity a = getActivity(); |
| String cameraActivityName = a.getPackageName() + ":" + processName; |
| List<ActivityManager.RunningAppProcessInfo> list = |
| mActivityManager.getRunningAppProcesses(); |
| assertEquals(-1, getPid(cameraActivityName, list)); |
| |
| // Start activity in a new top foreground process |
| Intent activityIntent = new Intent(a, klass); |
| a.startActivity(activityIntent); |
| Thread.sleep(WAIT_TIME); |
| |
| // Fail if activity isn't running |
| list = mActivityManager.getRunningAppProcesses(); |
| mProcessPid = getPid(cameraActivityName, list); |
| assertTrue(-1 != mProcessPid); |
| } |
| |
| /** |
| * Assert that there is only one event of the given type in the event list. |
| * |
| * @param event event type to check for. |
| * @param events {@link List} of events. |
| */ |
| public static void assertOnly(int event, List<ErrorLoggingService.LogEvent> events) { |
| assertTrue("Remote camera activity never received event: " + event, events != null); |
| for (ErrorLoggingService.LogEvent e : events) { |
| assertFalse("Remote camera activity received invalid event (" + e + |
| ") while waiting for event: " + event, |
| e.getEvent() < 0 || e.getEvent() != event); |
| } |
| assertTrue("Remote camera activity never received event: " + event, events.size() >= 1); |
| assertTrue("Remote camera activity received too many " + event + " events, received: " + |
| events.size(), events.size() == 1); |
| } |
| |
| /** |
| * Assert there were no logEvents in the given list. |
| * |
| * @param msg message to show on assertion failure. |
| * @param events {@link List} of events. |
| */ |
| public static void assertNone(String msg, List<ErrorLoggingService.LogEvent> events) { |
| if (events == null) return; |
| StringBuilder builder = new StringBuilder(msg + "\n"); |
| for (ErrorLoggingService.LogEvent e : events) { |
| builder.append(e).append("\n"); |
| } |
| assertTrue(builder.toString(), events.isEmpty()); |
| } |
| |
| /** |
| * Assert array is null or empty. |
| * |
| * @param array array to check. |
| */ |
| public static <T> void assertNotEmpty(T[] array) { |
| assertNotNull(array); |
| assertFalse("Array is empty: " + Arrays.toString(array), array.length == 0); |
| } |
| |
| /** |
| * Given an 'actual' array of objects, check that the objects given in the 'expected' |
| * array are also present in the 'actual' array in the same order. Objects in the 'actual' |
| * array that are not in the 'expected' array are skipped and ignored if they are given |
| * in the 'ignored' array, otherwise this assertion will fail. |
| * |
| * @param actual the ordered array of objects to check. |
| * @param expected the ordered array of expected objects. |
| * @param ignored the array of objects that will be ignored if present in actual, |
| * but not in expected (or are out of order). |
| * @param <T> |
| */ |
| public static <T> void assertOrderedEvents(T[] actual, T[] expected, T[] ignored) { |
| assertNotNull(actual); |
| assertNotNull(expected); |
| assertNotNull(ignored); |
| |
| int expIndex = 0; |
| int index = 0; |
| for (T i : actual) { |
| // If explicitly expected, move to next |
| if (expIndex < expected.length && Objects.equals(i, expected[expIndex])) { |
| expIndex++; |
| continue; |
| } |
| |
| // Fail if not ignored |
| boolean canIgnore = false; |
| for (T j : ignored) { |
| if (Objects.equals(i, j)) { |
| canIgnore = true; |
| break; |
| } |
| |
| } |
| |
| // Fail if not ignored. |
| assertTrue("Event at index " + index + " in actual array " + |
| Arrays.toString(actual) + " was unexpected: expected array was " + |
| Arrays.toString(expected) + ", ignored array was: " + |
| Arrays.toString(ignored), canIgnore); |
| index++; |
| } |
| assertTrue("Only had " + expIndex + " of " + expected.length + |
| " expected objects in array " + Arrays.toString(actual) + ", expected was " + |
| Arrays.toString(expected), expIndex == expected.length); |
| } |
| } |