camera2: Add multiprocess arbitration cts test.

Bug: 19186859
Change-Id: I2946b670e555f61a847e204bd7225af1067c59bb
diff --git a/tests/tests/hardware/Android.mk b/tests/tests/hardware/Android.mk
index 153445d..1c144ff 100644
--- a/tests/tests/hardware/Android.mk
+++ b/tests/tests/hardware/Android.mk
@@ -56,4 +56,4 @@
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
-include $(BUILD_CTS_PACKAGE)
+include $(BUILD_CTS_PACKAGE)
\ No newline at end of file
diff --git a/tests/tests/hardware/AndroidManifest.xml b/tests/tests/hardware/AndroidManifest.xml
index ab81162..7b15b61 100644
--- a/tests/tests/hardware/AndroidManifest.xml
+++ b/tests/tests/hardware/AndroidManifest.xml
@@ -25,6 +25,7 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.BODY_SENSORS" />
     <uses-permission android:name="android.permission.TRANSMIT_IR" />
+    <uses-permission android:name="android.permission.REORDER_TASKS" />
 
     <application>
         <uses-library android:name="android.test.runner" />
@@ -50,6 +51,26 @@
         <activity android:name="android.hardware.cts.GLSurfaceViewCtsActivity"
             android:label="GLSurfaceViewCtsActivity"/>
 
+        <service android:name="android.hardware.multiprocess.ErrorLoggingService"
+            android:label="ErrorLoggingService"
+            android:process=":errorLoggingServiceProcess"
+            android:exported="false">
+        </service>
+
+        <activity android:name="android.hardware.multiprocess.camera.cts.Camera1Activity"
+            android:label="RemoteCamera1Activity"
+            android:screenOrientation="landscape"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:process=":camera1ActivityProcess">
+        </activity>
+
+        <activity android:name="android.hardware.multiprocess.camera.cts.Camera2Activity"
+            android:label="RemoteCamera2Activity"
+            android:screenOrientation="landscape"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:process=":camera2ActivityProcess">
+        </activity>
+
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/hardware/src/android/hardware/multiprocess/ErrorLoggingService.java b/tests/tests/hardware/src/android/hardware/multiprocess/ErrorLoggingService.java
new file mode 100644
index 0000000..1b713ba
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/multiprocess/ErrorLoggingService.java
@@ -0,0 +1,611 @@
+/*
+ * 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;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Service for collecting error messages from other processes.
+ *
+ * <p />
+ * Used by CTS for multi-process error logging.
+ */
+public class ErrorLoggingService extends Service {
+    public static final String TAG = "ErrorLoggingService";
+
+    /**
+     * Receive all currently logged error strings in replyTo Messenger.
+     */
+    public static final int MSG_GET_LOG = 0;
+
+    /**
+     * Append a new error string to the log maintained in this service.
+     */
+    public static final int MSG_LOG_EVENT = 1;
+
+    /**
+     * Logged errors being reported in a replyTo Messenger by this service.
+     */
+    public static final int MSG_LOG_REPORT = 2;
+
+    /**
+     * A list of strings containing all error messages reported to this service.
+     */
+    private final ArrayList<LogEvent> mLog = new ArrayList<>();
+
+    /**
+     * A list of Messengers waiting for logs for any event.
+     */
+    private final ArrayList<Pair<Integer, Messenger>> mEventWaiters = new ArrayList<>();
+
+    private static final int DO_EVENT_FILTER = 1;
+    private static final String LOG_EVENT = "log_event";
+    private static final String LOG_EVENT_ARRAY = "log_event_array";
+
+
+    /**
+     * The messenger binder used by clients of this service to report/retrieve errors.
+     */
+    private final Messenger mMessenger = new Messenger(new MainHandler(mLog, mEventWaiters));
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mLog.clear();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mMessenger.getBinder();
+    }
+
+    /**
+     * Handler implementing the message interface for this service.
+     */
+    private static class MainHandler extends Handler {
+
+        ArrayList<LogEvent> mErrorLog;
+        ArrayList<Pair<Integer, Messenger>> mEventWaiters;
+
+        MainHandler(ArrayList<LogEvent> log, ArrayList<Pair<Integer, Messenger>> waiters) {
+            mErrorLog = log;
+            mEventWaiters = waiters;
+        }
+
+        private void sendMessages() {
+            if (mErrorLog.size() > 0) {
+                ListIterator<Pair<Integer, Messenger>> iter = mEventWaiters.listIterator();
+                boolean messagesHandled = false;
+                while (iter.hasNext()) {
+                    Pair<Integer, Messenger> elem = iter.next();
+                    for (LogEvent i : mErrorLog) {
+                        if (elem.first == null || elem.first == i.getEvent()) {
+                            Message m = Message.obtain(null, MSG_LOG_REPORT);
+                            Bundle b = m.getData();
+                            b.putParcelableArray(LOG_EVENT_ARRAY,
+                                    mErrorLog.toArray(new LogEvent[mErrorLog.size()]));
+                            m.setData(b);
+                            try {
+                                elem.second.send(m);
+                                messagesHandled = true;
+                            } catch (RemoteException e) {
+                                Log.e(TAG, "Could not report log message to remote, " +
+                                        "received exception from remote: " + e +
+                                        "\n  Original errors: " +
+                                        Arrays.toString(mErrorLog.toArray()));
+                            }
+                            iter.remove();
+                        }
+                    }
+                }
+                if (messagesHandled) {
+                    mErrorLog.clear();
+                }
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+                case MSG_GET_LOG:
+                    if (msg.replyTo == null) {
+                        break;
+                    }
+
+                    if (msg.arg1 == DO_EVENT_FILTER) {
+                        mEventWaiters.add(new Pair<Integer, Messenger>(msg.arg2, msg.replyTo));
+                    } else {
+                        mEventWaiters.add(new Pair<Integer, Messenger>(null, msg.replyTo));
+                    }
+
+                    sendMessages();
+
+                    break;
+                case MSG_LOG_EVENT:
+                    Bundle b = msg.getData();
+                    b.setClassLoader(LogEvent.class.getClassLoader());
+                    LogEvent error = b.getParcelable(LOG_EVENT);
+                    mErrorLog.add(error);
+
+                    sendMessages();
+
+                    break;
+                default:
+                    Log.e(TAG, "Unknown message type: " + msg.what);
+                    super.handleMessage(msg);
+            }
+        }
+    }
+
+    /**
+     * Parcelable object to use with logged events.
+     */
+    public static class LogEvent implements Parcelable {
+
+        private final int mEvent;
+        private final String mLogText;
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(mEvent);
+            out.writeString(mLogText);
+        }
+
+        public int getEvent() {
+            return mEvent;
+        }
+
+        public String getLogText() {
+            return mLogText;
+        }
+
+        public static final Parcelable.Creator<LogEvent> CREATOR
+                = new Parcelable.Creator<LogEvent>() {
+
+            public LogEvent createFromParcel(Parcel in) {
+                return new LogEvent(in);
+            }
+
+            public LogEvent[] newArray(int size) {
+                return new LogEvent[size];
+            }
+        };
+
+        private LogEvent(Parcel in) {
+            mEvent = in.readInt();
+            mLogText = in.readString();
+        }
+
+        public LogEvent(int id, String msg) {
+            mEvent = id;
+            mLogText = msg;
+        }
+
+        @Override
+        public String toString() {
+            return "LogEvent{" +
+                    "Event=" + mEvent +
+                    ", LogText='" + mLogText + '\'' +
+                    '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            LogEvent logEvent = (LogEvent) o;
+
+            if (mEvent != logEvent.mEvent) return false;
+            if (mLogText != null ? !mLogText.equals(logEvent.mLogText) : logEvent.mLogText != null)
+                return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = mEvent;
+            result = 31 * result + (mLogText != null ? mLogText.hashCode() : 0);
+            return result;
+        }
+    }
+
+    /**
+     * Implementation of Future to use when retrieving error messages from service.
+     *
+     * <p />
+     * To use this, either pass a {@link Runnable} or {@link Callable} in the constructor,
+     * or use the default constructor and set the result externally with {@link #setResult(Object)}.
+     */
+    private static class SettableFuture<T> extends FutureTask<T> {
+
+        public SettableFuture() {
+            super(new Callable<T>() {
+                @Override
+                public T call() throws Exception {
+                    throw new IllegalStateException(
+                            "Empty task, use #setResult instead of calling run.");
+                }
+            });
+        }
+
+        public SettableFuture(Callable<T> callable) {
+            super(callable);
+        }
+
+        public SettableFuture(Runnable runnable, T result) {
+            super(runnable, result);
+        }
+
+        public void setResult(T result) {
+            set(result);
+        }
+    }
+
+    /**
+     * Helper class for setting up and using a connection to {@link ErrorLoggingService}.
+     */
+    public static class ErrorServiceConnection implements AutoCloseable {
+
+        private Messenger mService = null;
+        private boolean mBind = false;
+        private final Object mLock = new Object();
+        private final Context mContext;
+        private final HandlerThread mReplyThread;
+        private ReplyHandler mReplyHandler;
+        private Messenger mReplyMessenger;
+
+        /**
+         * Construct a connection to the {@link ErrorLoggingService} in the given {@link Context}.
+         *
+         * @param context the {@link Context} to bind the service in.
+         */
+        public ErrorServiceConnection(final Context context) {
+            mContext = context;
+            mReplyThread = new HandlerThread("ErrorServiceConnection");
+            mReplyThread.start();
+            mReplyHandler = new ReplyHandler(mReplyThread.getLooper());
+            mReplyMessenger = new Messenger(mReplyHandler);
+        }
+
+        @Override
+        public void close() {
+            stop();
+            mReplyThread.quit();
+            synchronized (mLock) {
+                mService = null;
+                mBind = false;
+                mReplyHandler.cancelAll();
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            close();
+            super.finalize();
+        }
+
+        private static final class ReplyHandler extends Handler {
+
+            private final LinkedBlockingQueue<SettableFuture<List<LogEvent>>> mFuturesQueue =
+                    new LinkedBlockingQueue<>();
+
+            private ReplyHandler(Looper looper) {
+                super(looper);
+            }
+
+            /**
+             * Cancel all pending futures for this handler.
+             */
+            public void cancelAll() {
+                List<SettableFuture<List<LogEvent>>> logFutures = new ArrayList<>();
+                mFuturesQueue.drainTo(logFutures);
+                for (SettableFuture<List<LogEvent>> i : logFutures) {
+                    i.cancel(true);
+                }
+            }
+
+            /**
+             * Cancel a given future, and remove from the pending futures for this handler.
+             *
+             * @param report future to remove.
+             */
+            public void cancel(SettableFuture<List<LogEvent>> report) {
+                mFuturesQueue.remove(report);
+                report.cancel(true);
+            }
+
+            /**
+             * Add future for the next received report from this service.
+             *
+             * @param report a future to get the next received event report from.
+             */
+            public void addFuture(SettableFuture<List<LogEvent>> report) {
+                if (!mFuturesQueue.offer(report)) {
+                    Log.e(TAG, "Could not request another error report, too many requests queued.");
+                }
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_LOG_REPORT:
+                        SettableFuture<List<LogEvent>> task = mFuturesQueue.poll();
+                        if (task == null) break;
+                        Bundle b = msg.getData();
+                        b.setClassLoader(LogEvent.class.getClassLoader());
+                        Parcelable[] array = b.getParcelableArray(LOG_EVENT_ARRAY);
+                        LogEvent[] events = Arrays.copyOf(array, array.length, LogEvent[].class);
+                        List<LogEvent> res = Arrays.asList(events);
+                        task.setResult(res);
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown message type: " + msg.what);
+                        super.handleMessage(msg);
+                }
+            }
+        }
+
+        private ServiceConnection mConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+                Log.i(TAG, "Service connected.");
+                synchronized (mLock) {
+                    mService = new Messenger(iBinder);
+                    mBind = true;
+                    mLock.notifyAll();
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName componentName) {
+                Log.i(TAG, "Service disconnected.");
+                synchronized (mLock) {
+                    mService = null;
+                    mBind = false;
+                    mReplyHandler.cancelAll();
+                }
+            }
+        };
+
+        private Messenger blockingGetBoundService() {
+            synchronized (mLock) {
+                if (!mBind) {
+                    mContext.bindService(new Intent(mContext, ErrorLoggingService.class), mConnection,
+                            Context.BIND_AUTO_CREATE);
+                    mBind = true;
+                }
+                try {
+                    while (mService == null && mBind) {
+                        mLock.wait();
+                    }
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "Waiting for error service interrupted: " + e);
+                }
+                if (!mBind) {
+                    Log.w(TAG, "Could not get service, service disconnected.");
+                }
+                return mService;
+            }
+        }
+
+        private Messenger getBoundService() {
+            synchronized (mLock) {
+                if (!mBind) {
+                    mContext.bindService(new Intent(mContext, ErrorLoggingService.class), mConnection,
+                            Context.BIND_AUTO_CREATE);
+                    mBind = true;
+                }
+                return mService;
+            }
+        }
+
+        /**
+         * If the {@link ErrorLoggingService} is not yet bound, begin service connection attempt.
+         *
+         * <p />
+         * Note: This will not block.
+         */
+        public void start() {
+            synchronized (mLock) {
+                if (!mBind) {
+                    mContext.bindService(new Intent(mContext, ErrorLoggingService.class), mConnection,
+                            Context.BIND_AUTO_CREATE);
+                    mBind = true;
+                }
+            }
+        }
+
+        /**
+         * Unbind from the {@link ErrorLoggingService} if it has been bound.
+         *
+         * <p />
+         * Note: This will not block.
+         */
+        public void stop() {
+            synchronized (mLock) {
+                if (mBind) {
+                    mContext.unbindService(mConnection);
+                    mBind = false;
+                }
+            }
+        }
+
+        /**
+         * Send an logged event to the bound {@link ErrorLoggingService}.
+         *
+         * <p />
+         * If the service is not yet bound, this will bind the service and wait until it has been
+         * connected.
+         *
+         * <p />
+         * This is not safe to call from the UI thread, as this will deadlock with the looper used
+         * when connecting the service.
+         *
+         * @param id an int indicating the ID of this event.
+         * @param msg a {@link String} message to send.
+         */
+        public void log(final int id, final String msg) {
+            Messenger service = blockingGetBoundService();
+            Message m = Message.obtain(null, MSG_LOG_EVENT);
+            m.getData().putParcelable(LOG_EVENT, new LogEvent(id, msg));
+            try {
+                service.send(m);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received exception while logging error: " + e);
+            }
+        }
+
+        /**
+         * Send an logged event to the bound {@link ErrorLoggingService} when it becomes available.
+         *
+         * <p />
+         * If the service is not yet bound, this will bind the service.
+         *
+         * @param id an int indicating the ID of this event.
+         * @param msg a {@link String} message to send.
+         */
+        public void logAsync(final int id, final String msg) {
+            AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
+                @Override
+                public void run() {
+                    log(id, msg);
+                }
+            });
+        }
+
+        /**
+         * Retrieve all events logged in the {@link ErrorLoggingService}.
+         *
+         * <p />
+         * If the service is not yet bound, this will bind the service and wait until it has been
+         * connected.  Likewise, after the service has been bound, this method will block until
+         * the given timeout passes or an event is logged in the service.  Passing a negative
+         * timeout is equivalent to using an infinite timeout value.
+         *
+         * <p />
+         * This is not safe to call from the UI thread, as this will deadlock with the looper used
+         * when connecting the service.
+         *
+         * <p />
+         * Note: This method clears the events stored in the bound {@link ErrorLoggingService}.
+         *
+         * @param timeoutMs the number of milliseconds to wait for a logging event.
+         * @return a list of {@link String} error messages reported to the bound
+         *          {@link ErrorLoggingService} since the last call to getLog.
+         *
+         * @throws TimeoutException if the given timeout elapsed with no events logged.
+         */
+        public List<LogEvent> getLog(long timeoutMs) throws TimeoutException {
+            return retrieveLog(false, 0, timeoutMs);
+        }
+
+        /**
+         * Retrieve all events logged in the {@link ErrorLoggingService}.
+         *
+         * <p />
+         * If the service is not yet bound, this will bind the service and wait until it has been
+         * connected.  Likewise, after the service has been bound, this method will block until
+         * the given timeout passes or an event with the given event ID is logged in the service.
+         * Passing a negative timeout is equivalent to using an infinite timeout value.
+         *
+         * <p />
+         * This is not safe to call from the UI thread, as this will deadlock with the looper used
+         * when connecting the service.
+         *
+         * <p />
+         * Note: This method clears the events stored in the bound {@link ErrorLoggingService}.
+         *
+         * @param timeoutMs the number of milliseconds to wait for a logging event.
+         * @param event the ID of the event to wait for.
+         * @return a list of {@link String} error messages reported to the bound
+         *          {@link ErrorLoggingService} since the last call to getLog.
+         *
+         * @throws TimeoutException if the given timeout elapsed with no events of the given type
+         *          logged.
+         */
+        public List<LogEvent> getLog(long timeoutMs, int event) throws TimeoutException {
+            return retrieveLog(true, event, timeoutMs);
+        }
+
+        private List<LogEvent> retrieveLog(boolean hasEvent, int event, long timeout)
+                throws TimeoutException {
+            Messenger service = blockingGetBoundService();
+
+            SettableFuture<List<LogEvent>> task = new SettableFuture<>();
+
+            Message m = (hasEvent) ?
+                    Message.obtain(null, MSG_GET_LOG, DO_EVENT_FILTER, event, null) :
+                    Message.obtain(null, MSG_GET_LOG);
+            m.replyTo = mReplyMessenger;
+
+            synchronized(this) {
+                mReplyHandler.addFuture(task);
+                try {
+                    service.send(m);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Received exception while retrieving errors: " + e);
+                    return null;
+                }
+            }
+
+            List<LogEvent> res = null;
+            try {
+                res = (timeout < 0) ? task.get() : task.get(timeout, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException|ExecutionException e) {
+                Log.e(TAG, "Received exception while retrieving errors: " + e);
+            }
+            return res;
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera1Activity.java b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera1Activity.java
new file mode 100644
index 0000000..5c27111
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera1Activity.java
@@ -0,0 +1,93 @@
+/*
+ * 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.hardware.Camera;
+import android.hardware.multiprocess.ErrorLoggingService;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Activity implementing basic access of the Camera1 API.
+ *
+ * <p />
+ * This will log all errors to {@link android.hardware.multiprocess.ErrorLoggingService}.
+ */
+public class Camera1Activity extends Activity {
+    private static final String TAG = "Camera1Activity";
+
+    Camera mCamera;
+    ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate called.");
+        super.onCreate(savedInstanceState);
+        mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(this);
+        mErrorServiceConnection.start();
+    }
+
+    @Override
+    protected void onResume() {
+        Log.i(TAG, "onResume called.");
+        super.onResume();
+        try {
+            mCamera = Camera.open();
+            if (mCamera == null) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " no cameras available.");
+            }
+            mCamera.setErrorCallback(new Camera.ErrorCallback() {
+                @Override
+                public void onError(int i, Camera camera) {
+                    if (i == Camera.CAMERA_ERROR_EVICTED) {
+                        mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_EVICTED,
+                                TAG + " camera evicted");
+                        Log.e(TAG, "onError called with event " + i + ", camera evicted");
+                    } else {
+                        mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR,
+                                TAG + " camera experienced error: " + i);
+                        Log.e(TAG, "onError called with event " + i + ", camera error");
+                    }
+                }
+            });
+            mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_CONNECT,
+                    TAG + " camera connected");
+        } catch (RuntimeException e) {
+            mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                    " camera exception during connection: " + e);
+            Log.e(TAG, "Runtime error: " + e);
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        Log.i(TAG, "onPause called.");
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.i(TAG, "onDestroy called.");
+        super.onDestroy();
+        if (mErrorServiceConnection != null) {
+            mErrorServiceConnection.stop();
+            mErrorServiceConnection = null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera2Activity.java b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera2Activity.java
new file mode 100644
index 0000000..2a78649
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/Camera2Activity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.multiprocess.ErrorLoggingService;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+/**
+ * Activity implementing basic access of the Camera2 API.
+ *
+ * <p />
+ * This will log all errors to {@link android.hardware.multiprocess.ErrorLoggingService}.
+ */
+public class Camera2Activity extends Activity {
+    private static final String TAG = "Camera2Activity";
+
+    ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate called.");
+        super.onCreate(savedInstanceState);
+        mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(this);
+        mErrorServiceConnection.start();
+    }
+
+    @Override
+    protected void onPause() {
+        Log.i(TAG, "onPause called.");
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        Log.i(TAG, "onResume called.");
+        super.onResume();
+
+        try {
+            CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
+
+            if (manager == null) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " could not connect camera service");
+                return;
+            }
+            String[] cameraIds = manager.getCameraIdList();
+
+            if (cameraIds == null || cameraIds.length == 0) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " device reported having no cameras");
+                return;
+            }
+
+            manager.registerAvailabilityCallback(new CameraManager.AvailabilityCallback() {
+                @Override
+                public void onCameraAvailable(String cameraId) {
+                    super.onCameraAvailable(cameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_AVAILABLE,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is available");
+                }
+
+                @Override
+                public void onCameraUnavailable(String cameraId) {
+                    super.onCameraUnavailable(cameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_UNAVAILABLE,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is unavailable");
+                }
+            }, null);
+
+            final String chosen = cameraIds[0];
+
+            manager.openCamera(chosen, new CameraDevice.StateCallback() {
+                @Override
+                public void onOpened(CameraDevice cameraDevice) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_CONNECT,
+                            chosen);
+                    Log.i(TAG, "Camera " + chosen + " is opened");
+                }
+
+                @Override
+                public void onDisconnected(CameraDevice cameraDevice) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_EVICTED,
+                            chosen);
+                    Log.i(TAG, "Camera " + chosen + " is disconnected");
+                }
+
+                @Override
+                public void onError(CameraDevice cameraDevice, int i) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                            " Camera " + chosen + " experienced error " + i);
+                    Log.e(TAG, "Camera " + chosen + " onError called with error " + i);
+                }
+            }, null);
+        } catch (CameraAccessException e) {
+            mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                    " camera exception during connection: " + e);
+            Log.e(TAG, "Access exception: " + e);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.i(TAG, "onDestroy called.");
+        super.onDestroy();
+        if (mErrorServiceConnection != null) {
+            mErrorServiceConnection.stop();
+            mErrorServiceConnection = null;
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/CameraEvictionTest.java b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/CameraEvictionTest.java
new file mode 100644
index 0000000..3cf1dc7
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/CameraEvictionTest.java
@@ -0,0 +1,494 @@
+/*
+ * 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.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;
+        mContext = getActivity();
+        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 {
+        super.tearDown();
+        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;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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();
+        assertNotEmpty(cameraIds);
+        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;
+    }
+
+    /**
+     * 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
+        String cameraActivityName = mContext.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(mContext, klass);
+        activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.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);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/TestConstants.java b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/TestConstants.java
new file mode 100644
index 0000000..2805e02
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/multiprocess/camera/cts/TestConstants.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+/**
+ * Constants used throughout the multi-process unit tests.
+ */
+public class TestConstants {
+
+    public static final int EVENT_CAMERA_ERROR = -1;
+    public static final int EVENT_CAMERA_CONNECT = 1;
+    public static final int EVENT_CAMERA_EVICTED = 2;
+    public static final int EVENT_CAMERA_AVAILABLE = 3;
+    public static final int EVENT_CAMERA_UNAVAILABLE = 4;
+
+    public static final String EVENT_CAMERA_ERROR_STR = "error";
+    public static final String EVENT_CAMERA_CONNECT_STR = "connect";
+    public static final String EVENT_CAMERA_EVICTED_STR = "evicted";
+    public static final String EVENT_CAMERA_AVAILABLE_STR = "available";
+    public static final String EVENT_CAMERA_UNAVAILABLE_STR = "unavailable";
+
+    public static final String EVENT_CAMERA_UNKNOWN_STR = "unknown";
+
+    /**
+     * Convert the given error code to a string.
+     *
+     * @param err error code from {@link TestConstants}.
+     * @return string for this error code.
+     */
+    public static String errToStr(int err) {
+        switch(err) {
+            case EVENT_CAMERA_ERROR:
+                return EVENT_CAMERA_ERROR_STR;
+            case EVENT_CAMERA_CONNECT:
+                return EVENT_CAMERA_CONNECT_STR;
+            case EVENT_CAMERA_EVICTED:
+                return EVENT_CAMERA_EVICTED_STR;
+            case EVENT_CAMERA_AVAILABLE:
+                return EVENT_CAMERA_AVAILABLE_STR;
+            case EVENT_CAMERA_UNAVAILABLE:
+                return EVENT_CAMERA_UNAVAILABLE_STR;
+            default:
+                return EVENT_CAMERA_UNKNOWN_STR + " " + err;
+        }
+    }
+
+    /**
+     * Convert the given array of error codes to an array of strings.
+     *
+     * @param err array of error codes from {@link TestConstants}.
+     * @return string array for the given error codes.
+     */
+    public static String[] convertToStringArray(int[] err) {
+        if (err == null) return null;
+        String[] ret = new String[err.length];
+        for (int i = 0; i < err.length; i++) {
+            ret[i] = errToStr(err[i]);
+        }
+        return ret;
+    }
+
+}