diff options
author | Igor Murashkin <iam@google.com> | 2013-06-25 20:26:06 +0000 |
---|---|---|
committer | Igor Murashkin <iam@google.com> | 2013-06-26 13:19:44 -0700 |
commit | e363fbb2647aeb5ef4c87160d84c6b9ae8d45598 (patch) | |
tree | ff832d9b46118be173d254ad269c93627ce9e6d1 /core/java/android/hardware | |
parent | 73d5fe9f2e6edc11327b4a211f7d077e1e52cbb2 (diff) | |
download | frameworks_base-e363fbb2647aeb5ef4c87160d84c6b9ae8d45598.zip frameworks_base-e363fbb2647aeb5ef4c87160d84c6b9ae8d45598.tar.gz frameworks_base-e363fbb2647aeb5ef4c87160d84c6b9ae8d45598.tar.bz2 |
Partial CameraManager implementation
Bug: 9213377
Change-Id: I8f89fb94d7081a71b38e5cd0ad89116d219b4c33
Diffstat (limited to 'core/java/android/hardware')
7 files changed, 614 insertions, 10 deletions
diff --git a/core/java/android/hardware/photography/CameraAccessException.java b/core/java/android/hardware/photography/CameraAccessException.java index 01114df..fac5086 100644 --- a/core/java/android/hardware/photography/CameraAccessException.java +++ b/core/java/android/hardware/photography/CameraAccessException.java @@ -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; + } } diff --git a/core/java/android/hardware/photography/CameraDevice.java b/core/java/android/hardware/photography/CameraDevice.java index 2062db2..e94e3a1 100644 --- a/core/java/android/hardware/photography/CameraDevice.java +++ b/core/java/android/hardware/photography/CameraDevice.java @@ -17,8 +17,10 @@ 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"); + } + } diff --git a/core/java/android/hardware/photography/CameraManager.java b/core/java/android/hardware/photography/CameraManager.java index 328ba4b..115f151 100644 --- a/core/java/android/hardware/photography/CameraManager.java +++ b/core/java/android/hardware/photography/CameraManager.java @@ -16,6 +16,22 @@ 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 index 0000000..99e7c78 --- /dev/null +++ b/core/java/android/hardware/photography/utils/CameraBinderDecorator.java @@ -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 index 0000000..25dfc62 --- /dev/null +++ b/core/java/android/hardware/photography/utils/CameraRuntimeException.java @@ -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 index 0000000..ed05dd2 --- /dev/null +++ b/core/java/android/hardware/photography/utils/Decorator.java @@ -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 index 0000000..8eed6b1 --- /dev/null +++ b/core/java/android/hardware/photography/utils/UncheckedThrow.java @@ -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; + } +} |