OSDN Git Service

Partial CameraManager implementation
authorIgor Murashkin <iam@google.com>
Tue, 11 Jun 2013 21:23:50 +0000 (14:23 -0700)
committerIgor Murashkin <iam@google.com>
Tue, 25 Jun 2013 00:41:09 +0000 (17:41 -0700)
Bug: 9213377
Change-Id: I29864a5d1f7971ed589d1ffaddeefbb703e34018

15 files changed:
api/current.txt
core/java/android/app/ContextImpl.java
core/java/android/hardware/photography/CameraAccessException.java
core/java/android/hardware/photography/CameraDevice.java
core/java/android/hardware/photography/CameraManager.java
core/java/android/hardware/photography/utils/CameraBinderDecorator.java [new file with mode: 0644]
core/java/android/hardware/photography/utils/CameraRuntimeException.java [new file with mode: 0644]
core/java/android/hardware/photography/utils/Decorator.java [new file with mode: 0644]
core/java/android/hardware/photography/utils/UncheckedThrow.java [new file with mode: 0644]
media/tests/MediaFrameworkTest/Android.mk
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsBinderDecoratorTest.java [new file with mode: 0644]
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsDecoratorTest.java [new file with mode: 0644]
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsRuntimeExceptionTest.java [new file with mode: 0644]
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsUncheckedThrowTest.java [new file with mode: 0644]

index a6f2cf5..ca40f4f 100644 (file)
@@ -10653,7 +10653,7 @@ package android.hardware.location {
 
 package android.hardware.photography {
 
-  public class CameraAccessException extends java.lang.Exception {
+  public class CameraAccessException extends android.util.AndroidException {
     ctor public CameraAccessException(int);
     ctor public CameraAccessException(int, java.lang.String);
     ctor public CameraAccessException(int, java.lang.String, java.lang.Throwable);
@@ -10666,7 +10666,6 @@ package android.hardware.photography {
   }
 
   public final class CameraDevice implements java.lang.AutoCloseable {
-    ctor public CameraDevice();
     method public void capture(android.hardware.photography.CaptureRequest, android.hardware.photography.CameraDevice.CaptureListener) throws android.hardware.photography.CameraAccessException;
     method public void captureBurst(java.util.List<android.hardware.photography.CaptureRequest>, android.hardware.photography.CameraDevice.CaptureListener) throws android.hardware.photography.CameraAccessException;
     method public void close();
index 155aac1..a5106e4 100644 (file)
@@ -549,9 +549,9 @@ class ContextImpl extends Context {
                 return new AppOpsManager(ctx, service);
             }});
 
-        registerService(CAMERA_SERVICE, new StaticServiceFetcher() {
-            public Object createStaticService() {
-                return new CameraManager();
+        registerService(CAMERA_SERVICE, new ServiceFetcher() {
+            public Object createService(ContextImpl ctx) {
+                return new CameraManager(ctx);
             }
         });
 
index 01114df..fac5086 100644 (file)
@@ -16,6 +16,8 @@
 
 package android.hardware.photography;
 
+import android.util.AndroidException;
+
 /**
  * <p><code>CameraAccessException</code> is thrown if a camera device could not
  * be queried or opened by the {@link CameraManager}, or if the connection to an
@@ -24,7 +26,7 @@ package android.hardware.photography;
  * @see CameraManager
  * @see CameraDevice
  */
-public class CameraAccessException extends Exception {
+public class CameraAccessException extends AndroidException {
     /**
      * The camera device is in use already
      */
@@ -51,7 +53,10 @@ public class CameraAccessException extends Exception {
      */
     public static final int CAMERA_DISCONNECTED = 4;
 
-    private int mReason;
+    // Make the eclipse warning about serializable exceptions go away
+    private static final long serialVersionUID = 5630338637471475675L; // randomly generated
+
+    private final int mReason;
 
     /**
      * The reason for the failure to access the camera.
@@ -66,6 +71,7 @@ public class CameraAccessException extends Exception {
     }
 
     public CameraAccessException(int problem) {
+        super(getDefaultMessage(problem));
         mReason = problem;
     }
 
@@ -80,7 +86,25 @@ public class CameraAccessException extends Exception {
     }
 
     public CameraAccessException(int problem, Throwable cause) {
-        super(cause);
+        super(getDefaultMessage(problem), cause);
         mReason = problem;
     }
+
+    private static String getDefaultMessage(int problem) {
+        switch (problem) {
+            case CAMERA_IN_USE:
+                return "The camera device is in use already";
+            case MAX_CAMERAS_IN_USE:
+                return "The system-wide limit for number of open cameras has been reached, " +
+                       "and more camera devices cannot be opened until previous instances " +
+                       "are closed.";
+            case CAMERA_DISABLED:
+                return "The camera is disabled due to a device policy, and cannot be opened.";
+            case CAMERA_DISCONNECTED:
+                return "The camera device is removable and has been disconnected from the Android" +
+                       " device, or the camera service has shut down the connection due to a " +
+                       "higher-priority access request for the camera device.";
+        }
+        return null;
+    }
 }
index 2062db2..e94e3a1 100644 (file)
 package android.hardware.photography;
 
 import android.graphics.ImageFormat;
+import android.os.IBinder;
 import android.renderscript.Allocation;
 import android.renderscript.RenderScript;
+import android.util.Log;
 import android.view.Surface;
 
 import java.lang.AutoCloseable;
@@ -101,6 +103,8 @@ public final class CameraDevice implements AutoCloseable {
      */
     public static final int TEMPLATE_MANUAL = 5;
 
+    private static final String TAG = "CameraDevice";
+
     /**
      * Get the static properties for this camera. These are identical to the
      * properties returned by {@link CameraManager#getCameraProperties}.
@@ -451,6 +455,7 @@ public final class CameraDevice implements AutoCloseable {
      * the camera device interface will throw a {@link IllegalStateException},
      * except for calls to close().
      */
+    @Override
     public void close() {
     }
 
@@ -552,4 +557,11 @@ public final class CameraDevice implements AutoCloseable {
         public void onCameraDeviceError(CameraDevice camera, int error);
     }
 
+    /**
+     * @hide
+     */
+    public CameraDevice(IBinder binder) {
+        Log.e(TAG, "CameraDevice constructor not implemented yet");
+    }
+
 }
index 328ba4b..115f151 100644 (file)
 
 package android.hardware.photography;
 
+import android.content.Context;
+import android.hardware.ICameraService;
+import android.hardware.ICameraServiceListener;
+import android.hardware.IProCameraUser;
+import android.hardware.photography.utils.CameraBinderDecorator;
+import android.hardware.photography.utils.CameraRuntimeException;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
 /**
  * <p>An interface for iterating, listing, and connecting to
  * {@link CameraDevice CameraDevices}.</p>
@@ -32,9 +48,40 @@ package android.hardware.photography;
 public final class CameraManager {
 
     /**
+     * This should match the ICameraService definition
+     */
+    private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
+    private static final int USE_CALLING_UID = -1;
+
+    private final ICameraService mCameraService;
+    private ArrayList<String> mDeviceIdList;
+    private HashSet<CameraListener> mListenerSet;
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    /**
      * @hide
      */
-    public CameraManager() {
+    public CameraManager(Context context) {
+        mContext = context;
+
+        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
+        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);
+
+        /**
+         * Wrap the camera service in a decorator which automatically translates return codes
+         * into exceptions, and RemoteExceptions into other exceptions.
+         */
+        mCameraService = CameraBinderDecorator.newInstance(cameraServiceRaw);
+
+        try {
+            mCameraService.addListener(new CameraServiceListener());
+        } catch(CameraRuntimeException e) {
+            throw new IllegalStateException("Failed to register a camera service listener",
+                    e.asChecked());
+        } catch (RemoteException e) {
+            // impossible
+        }
     }
 
     /**
@@ -45,25 +92,37 @@ public final class CameraManager {
      *
      * @return The list of currently connected camera devices.
      */
-    public String[] getDeviceIdList() {
-        return null;
+    public String[] getDeviceIdList() throws CameraAccessException {
+        synchronized (mLock) {
+            return (String[]) getOrCreateDeviceIdListLocked().toArray();
+        }
     }
 
     /**
      * Register a listener to be notified about camera device availability.
      *
-     * @param listener the new listener to send camera availablity notices to.
+     * Registering a listener more than once has no effect.
+     *
+     * @param listener the new listener to send camera availability notices to.
      */
     public void registerCameraListener(CameraListener listener) {
+        synchronized (mLock) {
+            mListenerSet.add(listener);
+        }
     }
 
     /**
      * Remove a previously-added listener; the listener will no longer receive
      * connection and disconnection callbacks.
      *
+     * Removing a listener that isn't registered has no effect.
+     *
      * @param listener the listener to remove from the notification list
      */
     public void unregisterCameraListener(CameraListener listener) {
+        synchronized (mLock) {
+            mListenerSet.remove(listener);
+        }
     }
 
     /**
@@ -84,7 +143,18 @@ public final class CameraManager {
      */
     public CameraProperties getCameraProperties(String cameraId)
             throws CameraAccessException {
-        throw new IllegalArgumentException();
+
+        synchronized (mLock) {
+            if (!getOrCreateDeviceIdListLocked().contains(cameraId)) {
+                throw new IllegalArgumentException(String.format("Camera id %s does not match any" +
+                        " currently connected camera device", cameraId));
+            }
+        }
+
+        // TODO: implement and call a service function to get the capabilities on C++ side
+
+        // TODO: get properties from service
+        return new CameraProperties();
     }
 
     /**
@@ -107,7 +177,33 @@ public final class CameraManager {
      * @see android.app.admin.DevicePolicyManager#setCameraDisabled
      */
     public CameraDevice openCamera(String cameraId) throws CameraAccessException {
-        throw new IllegalArgumentException();
+
+        try {
+            IProCameraUser cameraUser;
+
+            synchronized (mLock) {
+                // TODO: Use ICameraDevice or some such instead of this...
+                cameraUser = mCameraService.connectPro(null,
+                        Integer.parseInt(cameraId),
+                        mContext.getPackageName(), USE_CALLING_UID);
+
+            }
+
+            return new CameraDevice(cameraUser.asBinder());
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
+                    + cameraId);
+        } catch (CameraRuntimeException e) {
+            if (e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) {
+                throw new IllegalArgumentException("Invalid camera ID specified -- " +
+                        "perhaps the camera was physically disconnected", e);
+            } else {
+                throw e.asChecked();
+            }
+        } catch (RemoteException e) {
+            // impossible
+            return null;
+        }
     }
 
     /**
@@ -135,4 +231,135 @@ public final class CameraManager {
          */
         public void onCameraUnavailable(String cameraId);
     }
-}
+
+    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
+        if (mDeviceIdList == null) {
+            int numCameras = 0;
+
+            try {
+                numCameras = mCameraService.getNumberOfCameras();
+            } catch(CameraRuntimeException e) {
+                throw e.asChecked();
+            } catch (RemoteException e) {
+                // impossible
+                return null;
+            }
+
+            mDeviceIdList = new ArrayList<String>();
+            for (int i = 0; i < numCameras; ++i) {
+                // Non-removable cameras use integers starting at 0 for their
+                // identifiers
+                mDeviceIdList.add(String.valueOf(i));
+            }
+
+        }
+        return mDeviceIdList;
+    }
+
+    // TODO: this class needs unit tests
+    // TODO: extract class into top level
+    private class CameraServiceListener extends Binder implements ICameraServiceListener  {
+
+        // Keep up-to-date with ICameraServiceListener.h
+
+        // Device physically unplugged
+        public static final int STATUS_NOT_PRESENT = 0;
+        // Device physically has been plugged in
+        // and the camera can be used exclusively
+        public static final int STATUS_PRESENT = 1;
+        // Device physically has been plugged in
+        // but it will not be connect-able until enumeration is complete
+        public static final int STATUS_ENUMERATING = 2;
+        // Camera is in use by another app and cannot be used exclusively
+        public static final int STATUS_NOT_AVAILABLE = 0x80000000;
+
+        // Camera ID -> Status map
+        private final HashMap<String, Integer> mDeviceStatus = new HashMap<String, Integer>();
+
+        private static final String TAG = "CameraServiceListener";
+
+        @Override
+        public IBinder asBinder() {
+            return this;
+        }
+
+        private boolean isAvailable(int status) {
+            switch (status) {
+                case STATUS_PRESENT:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        private boolean validStatus(int status) {
+            switch (status) {
+                case STATUS_NOT_PRESENT:
+                case STATUS_PRESENT:
+                case STATUS_ENUMERATING:
+                case STATUS_NOT_AVAILABLE:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void onStatusChanged(int status, int cameraId) throws RemoteException {
+            synchronized(CameraManager.this) {
+
+                Log.v(TAG,
+                        String.format("Camera id %d has status changed to 0x%x", cameraId, status));
+
+                String id = String.valueOf(cameraId);
+
+                if (!validStatus(status)) {
+                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
+                            status));
+                    return;
+                }
+
+                Integer oldStatus = mDeviceStatus.put(id, status);
+
+                if (oldStatus == status) {
+                    Log.v(TAG, String.format(
+                            "Device status changed to 0x%x, which is what it already was",
+                            status));
+                    return;
+                }
+
+                // TODO: consider abstracting out this state minimization + transition
+                // into a separate
+                // more easily testable class
+                // i.e. (new State()).addState(STATE_AVAILABLE)
+                //                   .addState(STATE_NOT_AVAILABLE)
+                //                   .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
+                //                   .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
+                //                   .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
+                //                   .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
+
+                // Translate all the statuses to either 'available' or 'not available'
+                //  available -> available         => no new update
+                //  not available -> not available => no new update
+                if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
+
+                    Log.v(TAG,
+                            String.format(
+                                    "Device status was previously available (%d), " +
+                                            " and is now again available (%d)" +
+                                            "so no new client visible update will be sent",
+                                    isAvailable(status), isAvailable(status)));
+                    return;
+                }
+
+                for (CameraListener listener : mListenerSet) {
+                    if (isAvailable(status)) {
+                        listener.onCameraAvailable(id);
+                    } else {
+                        listener.onCameraUnavailable(id);
+                    }
+                } // for
+            } // synchronized
+        } // onStatusChanged
+    } // CameraServiceListener
+} // CameraManager
diff --git a/core/java/android/hardware/photography/utils/CameraBinderDecorator.java b/core/java/android/hardware/photography/utils/CameraBinderDecorator.java
new file mode 100644 (file)
index 0000000..99e7c78
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 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.photography.utils;
+
+import static android.hardware.photography.CameraAccessException.CAMERA_DISABLED;
+import static android.hardware.photography.CameraAccessException.CAMERA_DISCONNECTED;
+import static android.hardware.photography.CameraAccessException.CAMERA_IN_USE;
+
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+
+import java.lang.reflect.Method;
+
+/**
+ * Translate camera service status_t return values into exceptions.
+ *
+ * @see android.hardware.photography.utils.CameraBinderDecorator#newInstance
+ * @hide
+ */
+public class CameraBinderDecorator {
+
+    public static final int NO_ERROR = 0;
+    public static final int PERMISSION_DENIED = -1;
+    public static final int ALREADY_EXISTS = -17;
+    public static final int BAD_VALUE = -22;
+    public static final int DEAD_OBJECT = -32;
+
+    /**
+     * TODO: add as error codes in Errors.h
+     * - POLICY_PROHIBITS
+     * - RESOURCE_BUSY
+     * - NO_SUCH_DEVICE
+     */
+    public static final int EACCES = -13;
+    public static final int EBUSY = -16;
+    public static final int ENODEV = -19;
+
+    private static class CameraBinderDecoratorListener implements Decorator.DecoratorListener {
+
+        @Override
+        public void onBeforeInvocation(Method m, Object[] args) {
+        }
+
+        @Override
+        public void onAfterInvocation(Method m, Object[] args, Object result) {
+            // int return type => status_t => convert to exception
+            if (m.getReturnType() == Integer.TYPE) {
+                int returnValue = (Integer) result;
+
+                switch (returnValue) {
+                    case NO_ERROR:
+                        return;
+                    case PERMISSION_DENIED:
+                        throw new SecurityException("Lacking privileges to access camera service");
+                    case ALREADY_EXISTS:
+                        return;
+                    case BAD_VALUE:
+                        throw new IllegalArgumentException("Bad argument passed to camera service");
+                    case DEAD_OBJECT:
+                        UncheckedThrow.throwAnyException(new CameraRuntimeException(
+                                CAMERA_DISCONNECTED));
+                        // TODO: Camera service (native side) should return
+                        // EACCES error
+                        // when there's a policy manager disabled causing this
+                    case EACCES:
+                        UncheckedThrow.throwAnyException(new CameraRuntimeException(
+                                CAMERA_DISABLED));
+                    case EBUSY:
+                        UncheckedThrow.throwAnyException(new CameraRuntimeException(
+                                CAMERA_IN_USE));
+                    case ENODEV:
+                        UncheckedThrow.throwAnyException(new CameraRuntimeException(
+                                CAMERA_DISCONNECTED));
+                }
+
+                /**
+                 * Trap the rest of the negative return values. If we have known
+                 * error codes i.e. ALREADY_EXISTS that aren't really runtime
+                 * errors, then add them to the top switch statement
+                 */
+                if (returnValue < 0) {
+                    throw new UnsupportedOperationException(String.format("Unknown error %d",
+                            returnValue));
+                }
+            }
+        }
+
+        @Override
+        public boolean onCatchException(Method m, Object[] args, Throwable t) {
+
+            if (t instanceof DeadObjectException) {
+                UncheckedThrow.throwAnyException(new CameraRuntimeException(
+                        CAMERA_DISCONNECTED,
+                        "Process hosting the camera service has died unexpectedly",
+                        t));
+            } else if (t instanceof RemoteException) {
+                throw new UnsupportedOperationException("An unknown RemoteException was thrown" +
+                        " which should never happen.", t);
+            }
+
+            return false;
+        }
+
+        @Override
+        public void onFinally(Method m, Object[] args) {
+        }
+
+    }
+
+    /**
+     * <p>
+     * Wraps the type T with a proxy that will check 'status_t' return codes
+     * from the native side of the camera service, and throw Java exceptions
+     * automatically based on the code.
+     * </p>
+     * <p>
+     * In addition it also rewrites binder's RemoteException into either a
+     * CameraAccessException or an UnsupportedOperationException.
+     * </p>
+     * <p>
+     * As a result of calling any method on the proxy, RemoteException is
+     * guaranteed never to be thrown.
+     * </p>
+     *
+     * @param obj object that will serve as the target for all method calls
+     * @param <T> the type of the element you want to wrap. This must be an interface.
+     * @return a proxy that will intercept all invocations to obj
+     */
+    public static <T> T newInstance(T obj) {
+        return Decorator.<T> newInstance(obj, new CameraBinderDecoratorListener());
+    }
+}
diff --git a/core/java/android/hardware/photography/utils/CameraRuntimeException.java b/core/java/android/hardware/photography/utils/CameraRuntimeException.java
new file mode 100644 (file)
index 0000000..25dfc62
--- /dev/null
@@ -0,0 +1,63 @@
+package android.hardware.photography.utils;
+
+import android.hardware.photography.CameraAccessException;
+
+/**
+ * @hide
+ */
+public class CameraRuntimeException extends RuntimeException {
+
+    private final int mReason;
+    private String mMessage;
+    private Throwable mCause;
+
+    public final int getReason() {
+        return mReason;
+    }
+
+    public CameraRuntimeException(int problem) {
+        super();
+        mReason = problem;
+    }
+
+    public CameraRuntimeException(int problem, String message) {
+        super(message);
+        mReason = problem;
+        mMessage = message;
+    }
+
+    public CameraRuntimeException(int problem, String message, Throwable cause) {
+        super(message, cause);
+        mReason = problem;
+        mMessage = message;
+        mCause = cause;
+    }
+
+    public CameraRuntimeException(int problem, Throwable cause) {
+        super(cause);
+        mReason = problem;
+        mCause = cause;
+    }
+
+    /**
+     * Recreate this exception as the CameraAccessException equivalent.
+     * @return CameraAccessException
+     */
+    public CameraAccessException asChecked() {
+        CameraAccessException e;
+
+        if (mMessage != null && mCause != null) {
+            e = new CameraAccessException(mReason, mMessage, mCause);
+        } else if (mMessage != null) {
+            e = new CameraAccessException(mReason, mMessage);
+        } else if (mCause != null) {
+            e = new CameraAccessException(mReason, mCause);
+        } else {
+            e = new CameraAccessException(mReason);
+        }
+        // throw and catch, so java has a chance to fill out the stack trace
+        e.setStackTrace(this.getStackTrace());
+
+        return e;
+    }
+}
diff --git a/core/java/android/hardware/photography/utils/Decorator.java b/core/java/android/hardware/photography/utils/Decorator.java
new file mode 100644 (file)
index 0000000..ed05dd2
--- /dev/null
@@ -0,0 +1,92 @@
+
+package android.hardware.photography.utils;
+
+import java.lang.reflect.*;
+
+/**
+ * This is an implementation of the 'decorator' design pattern using Java's proxy mechanism.
+ *
+ * @see android.hardware.photography.utils.Decorator#newInstance
+ *
+ * @hide
+ */
+public class Decorator<T> implements InvocationHandler {
+
+    public interface DecoratorListener {
+        /**
+         * This method is called before the target method is invoked
+         * @param args arguments to target method
+         * @param m Method being called
+         */
+        void onBeforeInvocation(Method m, Object[] args);
+        /**
+         * This function is called after the target method is invoked
+         * if there were no uncaught exceptions
+         * @param args arguments to target method
+         * @param m Method being called
+         * @param result return value of target method
+         */
+        void onAfterInvocation(Method m, Object[] args, Object result);
+        /**
+         * This method is called only if there was an exception thrown by the target method
+         * during its invocation.
+         *
+         * @param args arguments to target method
+         * @param m Method being called
+         * @param t Throwable that was thrown
+         * @return false to rethrow exception, true if the exception was handled
+         */
+        boolean onCatchException(Method m, Object[] args, Throwable t);
+        /**
+         * This is called after the target method is invoked, regardless of whether or not
+         * there were any exceptions.
+         * @param args arguments to target method
+         * @param m Method being called
+         */
+        void onFinally(Method m, Object[] args);
+    }
+
+    private final T mObject;
+    private final DecoratorListener mListener;
+
+    /**
+     * Create a decorator wrapping the specified object's method calls.
+     *
+     * @param obj the object whose method calls you want to intercept
+     * @param listener the decorator handler for intercepted method calls
+     * @param <T> the type of the element you want to wrap. This must be an interface.
+     * @return a wrapped interface-compatible T
+     */
+    @SuppressWarnings("unchecked")
+    public static<T> T newInstance(T obj, DecoratorListener listener) {
+        return (T)java.lang.reflect.Proxy.newProxyInstance(
+                obj.getClass().getClassLoader(),
+                obj.getClass().getInterfaces(),
+                new Decorator<T>(obj, listener));
+    }
+
+    private Decorator(T obj, DecoratorListener listener) {
+        this.mObject = obj;
+        this.mListener = listener;
+    }
+
+    @Override
+    public Object invoke(Object proxy, Method m, Object[] args)
+            throws Throwable
+    {
+        Object result = null;
+        try {
+            mListener.onBeforeInvocation(m, args);
+            result = m.invoke(mObject, args);
+            mListener.onAfterInvocation(m, args, result);
+        } catch (InvocationTargetException e) {
+            Throwable t = e.getTargetException();
+            if (!mListener.onCatchException(m, args, t)) {
+                throw t;
+            }
+        } finally {
+            mListener.onFinally(m, args);
+        }
+        return result;
+    }
+}
diff --git a/core/java/android/hardware/photography/utils/UncheckedThrow.java b/core/java/android/hardware/photography/utils/UncheckedThrow.java
new file mode 100644 (file)
index 0000000..8eed6b1
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 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.photography.utils;
+
+/**
+ * @hide
+ */
+public class UncheckedThrow {
+
+    /**
+     * Throw any kind of exception without needing it to be checked
+     * @param e any instance of a Exception
+     */
+    public static void throwAnyException(Exception e) {
+        /**
+         *  Abuse type erasure by making the compiler think we are throwing RuntimeException,
+         *  which is unchecked, but then inserting any exception in there.
+         */
+        UncheckedThrow.<RuntimeException>throwAnyImpl(e);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static<T extends Exception> void throwAnyImpl(Exception e) throws T {
+        throw (T) e;
+    }
+}
index c9afa19..1e6b2e7 100644 (file)
@@ -7,7 +7,7 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files)
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
-LOCAL_STATIC_JAVA_LIBRARIES := easymocklib
+LOCAL_STATIC_JAVA_LIBRARIES := easymocklib mockito-target
 
 LOCAL_PACKAGE_NAME := mediaframeworktest
 
index 62af3f3..e9fbca7 100644 (file)
@@ -48,6 +48,7 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner {
         addMediaRecorderStateUnitTests(suite);
         addMediaPlayerStateUnitTests(suite);
         addMediaScannerUnitTests(suite);
+        addCameraUnitTests(suite);
         return suite;
     }
 
@@ -56,6 +57,13 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner {
         return MediaFrameworkUnitTestRunner.class.getClassLoader();
     }
 
+    private void addCameraUnitTests(TestSuite suite) {
+        suite.addTestSuite(CameraUtilsDecoratorTest.class);
+        suite.addTestSuite(CameraUtilsRuntimeExceptionTest.class);
+        suite.addTestSuite(CameraUtilsUncheckedThrowTest.class);
+        suite.addTestSuite(CameraUtilsBinderDecoratorTest.class);
+    }
+
     // Running all unit tests checking the state machine may be time-consuming.
     private void addMediaMetadataRetrieverStateUnitTests(TestSuite suite) {
         suite.addTestSuite(MediaMetadataRetrieverTest.class);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsBinderDecoratorTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsBinderDecoratorTest.java
new file mode 100644 (file)
index 0000000..7c48992
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 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 com.android.mediaframeworktest.unit;
+
+import android.hardware.photography.CameraAccessException;
+import android.hardware.photography.utils.CameraBinderDecorator;
+import android.hardware.photography.utils.CameraRuntimeException;
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static org.mockito.Mockito.*;
+import static android.hardware.photography.utils.CameraBinderDecorator.*;
+import static android.hardware.photography.CameraAccessException.*;
+
+import junit.framework.Assert;
+
+public class CameraUtilsBinderDecoratorTest extends junit.framework.TestCase {
+
+    private interface ICameraBinderStereotype {
+
+        double doNothing();
+
+        // int is a 'status_t'
+        int doSomethingPositive();
+
+        int doSomethingNoError();
+
+        int doSomethingPermissionDenied();
+
+        int doSomethingAlreadyExists();
+
+        int doSomethingBadValue();
+
+        int doSomethingDeadObject() throws CameraRuntimeException;
+
+        int doSomethingBadPolicy() throws CameraRuntimeException;
+
+        int doSomethingDeviceBusy() throws CameraRuntimeException;
+
+        int doSomethingNoSuchDevice() throws CameraRuntimeException;
+
+        int doSomethingUnknownErrorCode();
+
+        int doSomethingThrowDeadObjectException() throws RemoteException;
+
+        int doSomethingThrowTransactionTooLargeException() throws RemoteException;
+    }
+
+    private static final double SOME_ARBITRARY_DOUBLE = 1.0;
+    private static final int SOME_ARBITRARY_POSITIVE_INT = 5;
+    private static final int SOME_ARBITRARY_NEGATIVE_INT = -0xC0FFEE;
+
+    @SmallTest
+    public void testStereotypes() {
+
+        ICameraBinderStereotype mock = mock(ICameraBinderStereotype.class);
+        try {
+            when(mock.doNothing()).thenReturn(SOME_ARBITRARY_DOUBLE);
+            when(mock.doSomethingPositive()).thenReturn(SOME_ARBITRARY_POSITIVE_INT);
+            when(mock.doSomethingNoError()).thenReturn(NO_ERROR);
+            when(mock.doSomethingPermissionDenied()).thenReturn(PERMISSION_DENIED);
+            when(mock.doSomethingAlreadyExists()).thenReturn(ALREADY_EXISTS);
+            when(mock.doSomethingBadValue()).thenReturn(BAD_VALUE);
+            when(mock.doSomethingDeadObject()).thenReturn(DEAD_OBJECT);
+            when(mock.doSomethingBadPolicy()).thenReturn(EACCES);
+            when(mock.doSomethingDeviceBusy()).thenReturn(EBUSY);
+            when(mock.doSomethingNoSuchDevice()).thenReturn(ENODEV);
+            when(mock.doSomethingUnknownErrorCode()).thenReturn(SOME_ARBITRARY_NEGATIVE_INT);
+            when(mock.doSomethingThrowDeadObjectException()).thenThrow(new DeadObjectException());
+            when(mock.doSomethingThrowTransactionTooLargeException()).thenThrow(
+                    new TransactionTooLargeException());
+        } catch (RemoteException e) {
+            Assert.fail("Unreachable");
+        }
+
+        ICameraBinderStereotype decoratedMock = CameraBinderDecorator.newInstance(mock);
+
+        // ignored by decorator because return type is double, not int
+        assertEquals(SOME_ARBITRARY_DOUBLE, decoratedMock.doNothing());
+
+        // pass through for positive values
+        assertEquals(SOME_ARBITRARY_POSITIVE_INT, decoratedMock.doSomethingPositive());
+
+        // pass through NO_ERROR
+        assertEquals(NO_ERROR, decoratedMock.doSomethingNoError());
+
+        try {
+            decoratedMock.doSomethingPermissionDenied();
+            Assert.fail("Should've thrown SecurityException");
+        } catch (SecurityException e) {
+        }
+
+        assertEquals(ALREADY_EXISTS, decoratedMock.doSomethingAlreadyExists());
+
+        try {
+            decoratedMock.doSomethingBadValue();
+            Assert.fail("Should've thrown IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+        }
+
+        try {
+            decoratedMock.doSomethingDeadObject();
+            Assert.fail("Should've thrown CameraRuntimeException");
+        } catch (CameraRuntimeException e) {
+            assertEquals(CAMERA_DISCONNECTED, e.getReason());
+        }
+
+        try {
+            decoratedMock.doSomethingBadPolicy();
+            Assert.fail("Should've thrown CameraRuntimeException");
+        } catch (CameraRuntimeException e) {
+            assertEquals(CAMERA_DISABLED, e.getReason());
+        }
+
+        try {
+            decoratedMock.doSomethingDeviceBusy();
+            Assert.fail("Should've thrown CameraRuntimeException");
+        } catch (CameraRuntimeException e) {
+            assertEquals(CAMERA_IN_USE, e.getReason());
+        }
+
+        try {
+            decoratedMock.doSomethingNoSuchDevice();
+            Assert.fail("Should've thrown CameraRuntimeException");
+        } catch (CameraRuntimeException e) {
+            assertEquals(CAMERA_DISCONNECTED, e.getReason());
+        }
+
+        try {
+            decoratedMock.doSomethingUnknownErrorCode();
+            Assert.fail("Should've thrown UnsupportedOperationException");
+        } catch (UnsupportedOperationException e) {
+            assertEquals(String.format("Unknown error %d",
+                    SOME_ARBITRARY_NEGATIVE_INT), e.getMessage());
+        }
+
+        try {
+            decoratedMock.doSomethingThrowDeadObjectException();
+            Assert.fail("Should've thrown CameraRuntimeException");
+        } catch (CameraRuntimeException e) {
+            assertEquals(CAMERA_DISCONNECTED, e.getReason());
+        } catch (RemoteException e) {
+            Assert.fail("Should not throw a DeadObjectException directly, but rethrow");
+        }
+
+        try {
+            decoratedMock.doSomethingThrowTransactionTooLargeException();
+            Assert.fail("Should've thrown UnsupportedOperationException");
+        } catch (UnsupportedOperationException e) {
+            assertTrue(e.getCause() instanceof TransactionTooLargeException);
+        } catch (RemoteException e) {
+            Assert.fail("Should not throw a TransactionTooLargeException directly, but rethrow");
+        }
+    }
+
+}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsDecoratorTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsDecoratorTest.java
new file mode 100644 (file)
index 0000000..bae17fa
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 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 com.android.mediaframeworktest.unit;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.hardware.photography.utils.*;
+import android.hardware.photography.utils.Decorator.DecoratorListener;
+
+import junit.framework.Assert;
+
+import java.lang.reflect.Method;
+
+/**
+ * adb shell am instrument -e class 'com.android.mediaframeworktest.unit.CameraUtilsDecoratorTest' \
+ *      -w com.android.mediaframeworktest/.MediaFrameworkUnitTestRunner
+ */
+public class CameraUtilsDecoratorTest extends junit.framework.TestCase {
+    private DummyListener mDummyListener;
+    private DummyInterface mIface;
+
+    @Override
+    public void setUp() {
+        mDummyListener = new DummyListener();
+        mIface = Decorator.newInstance(new DummyImpl(), mDummyListener);
+    }
+
+    interface DummyInterface {
+        int addValues(int x, int y, int z);
+
+        void raiseException() throws Exception;
+
+        void raiseUnsupportedOperationException() throws UnsupportedOperationException;
+    }
+
+    class DummyImpl implements DummyInterface {
+        @Override
+        public int addValues(int x, int y, int z) {
+            return x + y + z;
+        }
+
+        @Override
+        public void raiseException() throws Exception {
+            throw new Exception("Test exception");
+        }
+
+        @Override
+        public void raiseUnsupportedOperationException() throws UnsupportedOperationException {
+            throw new UnsupportedOperationException("Test exception");
+        }
+    }
+
+    class DummyListener implements DecoratorListener {
+
+        public boolean beforeCalled = false;
+        public boolean afterCalled = false;
+        public boolean catchCalled = false;
+        public boolean finallyCalled = false;
+        public Object resultValue = null;
+
+        public boolean raiseException = false;
+
+        @Override
+        public void onBeforeInvocation(Method m, Object[] args) {
+            beforeCalled = true;
+        }
+
+        @Override
+        public void onAfterInvocation(Method m, Object[] args, Object result) {
+            afterCalled = true;
+            resultValue = result;
+
+            if (raiseException) {
+                throw new UnsupportedOperationException("Test exception");
+            }
+        }
+
+        @Override
+        public boolean onCatchException(Method m, Object[] args, Throwable t) {
+            catchCalled = true;
+            return false;
+        }
+
+        @Override
+        public void onFinally(Method m, Object[] args) {
+            finallyCalled = true;
+        }
+
+    };
+
+    @SmallTest
+    public void testDecorator() {
+
+        // TODO rewrite this using mocks
+
+        assertTrue(mIface.addValues(1, 2, 3) == 6);
+        assertTrue(mDummyListener.beforeCalled);
+        assertTrue(mDummyListener.afterCalled);
+
+        int resultValue = (Integer)mDummyListener.resultValue;
+        assertTrue(resultValue == 6);
+        assertTrue(mDummyListener.finallyCalled);
+        assertFalse(mDummyListener.catchCalled);
+    }
+
+    @SmallTest
+    public void testDecoratorExceptions() {
+
+        boolean gotExceptions = false;
+        try {
+            mIface.raiseException();
+        } catch (Exception e) {
+            gotExceptions = true;
+            assertTrue(e.getMessage() == "Test exception");
+        }
+        assertTrue(gotExceptions);
+        assertTrue(mDummyListener.beforeCalled);
+        assertFalse(mDummyListener.afterCalled);
+        assertTrue(mDummyListener.catchCalled);
+        assertTrue(mDummyListener.finallyCalled);
+    }
+
+    @SmallTest
+    public void testDecoratorUnsupportedOperationException() {
+
+        boolean gotExceptions = false;
+        try {
+            mIface.raiseUnsupportedOperationException();
+        } catch (UnsupportedOperationException e) {
+            gotExceptions = true;
+            assertTrue(e.getMessage() == "Test exception");
+        }
+        assertTrue(gotExceptions);
+        assertTrue(mDummyListener.beforeCalled);
+        assertFalse(mDummyListener.afterCalled);
+        assertTrue(mDummyListener.catchCalled);
+        assertTrue(mDummyListener.finallyCalled);
+    }
+
+    @SmallTest
+    public void testDecoratorRaisesException() {
+
+        boolean gotExceptions = false;
+        try {
+            mDummyListener.raiseException = true;
+            mIface.addValues(1, 2, 3);
+            Assert.fail("unreachable");
+        } catch (UnsupportedOperationException e) {
+            gotExceptions = true;
+            assertTrue(e.getMessage() == "Test exception");
+        }
+        assertTrue(gotExceptions);
+        assertTrue(mDummyListener.beforeCalled);
+        assertTrue(mDummyListener.afterCalled);
+        assertFalse(mDummyListener.catchCalled);
+        assertTrue(mDummyListener.finallyCalled);
+    }
+}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsRuntimeExceptionTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsRuntimeExceptionTest.java
new file mode 100644 (file)
index 0000000..8c2dd4d
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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 com.android.mediaframeworktest.unit;
+
+import android.hardware.photography.CameraAccessException;
+import android.hardware.photography.utils.CameraRuntimeException;
+import android.hardware.photography.utils.UncheckedThrow;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.Assert;
+
+public class CameraUtilsRuntimeExceptionTest extends junit.framework.TestCase {
+
+    @SmallTest
+    public void testCameraRuntimeException1() {
+        try {
+            CameraRuntimeException runtimeExc = new CameraRuntimeException(12345);
+            throw runtimeExc.asChecked();
+        } catch (CameraAccessException e) {
+            assertEquals(12345, e.getReason());
+            assertNull(e.getMessage());
+            assertNull(e.getCause());
+        }
+    }
+
+    @SmallTest
+    public void testCameraRuntimeException2() {
+        try {
+            CameraRuntimeException runtimeExc = new CameraRuntimeException(12345, "Hello");
+            throw runtimeExc.asChecked();
+        } catch (CameraAccessException e) {
+            assertEquals(12345, e.getReason());
+            assertEquals("Hello", e.getMessage());
+            assertNull(e.getCause());
+        }
+    }
+
+    @SmallTest
+    public void testCameraRuntimeException3() {
+        Throwable cause = new IllegalStateException("For great justice");
+        try {
+            CameraRuntimeException runtimeExc = new CameraRuntimeException(12345, cause);
+            throw runtimeExc.asChecked();
+        } catch (CameraAccessException e) {
+            assertEquals(12345, e.getReason());
+            assertNull(e.getMessage());
+            assertEquals(cause, e.getCause());
+        }
+    }
+
+    @SmallTest
+    public void testCameraRuntimeException4() {
+        Throwable cause = new IllegalStateException("For great justice");
+        try {
+            CameraRuntimeException runtimeExc = new CameraRuntimeException(12345, "Hello", cause);
+            throw runtimeExc.asChecked();
+        } catch (CameraAccessException e) {
+            assertEquals(12345, e.getReason());
+            assertEquals("Hello", e.getMessage());
+            assertEquals(cause, e.getCause());
+        }
+    }
+}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsUncheckedThrowTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraUtilsUncheckedThrowTest.java
new file mode 100644 (file)
index 0000000..cbe123c
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 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 com.android.mediaframeworktest.unit;
+
+import android.hardware.photography.CameraAccessException;
+import android.hardware.photography.utils.UncheckedThrow;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.Assert;
+
+public class CameraUtilsUncheckedThrowTest extends junit.framework.TestCase {
+
+    private void fakeNeverThrowsCameraAccess() throws CameraAccessException {
+    }
+
+    @SmallTest
+    public void testUncheckedThrow() {
+        try {
+            UncheckedThrow.throwAnyException(new CameraAccessException(
+                    CameraAccessException.CAMERA_DISCONNECTED));
+            Assert.fail("unreachable");
+            fakeNeverThrowsCameraAccess();
+        } catch (CameraAccessException e) {
+        }
+    }
+}