diff options
6 files changed, 604 insertions, 22 deletions
diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java index abd69c151efd..03dd3540370c 100644 --- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -17,7 +17,9 @@ package android.hardware.camera2.legacy; import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; @@ -25,7 +27,9 @@ import android.hardware.camera2.utils.LongParcelable; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; +import android.os.ConditionVariable; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; @@ -33,7 +37,6 @@ import android.view.Surface; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; /** * Compatibility implementation of the Camera2 API binder interface. @@ -53,6 +56,7 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { private static final String TAG = "CameraDeviceUserShim"; private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + private static final int OPEN_CAMERA_TIMEOUT_MS = 5000; // 5 sec (same as api1 cts timeout) private final LegacyCameraDevice mLegacyDevice; @@ -60,29 +64,143 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { private int mSurfaceIdCounter; private boolean mConfiguring; private final SparseArray<Surface> mSurfaces; + private final CameraCharacteristics mCameraCharacteristics; + private final CameraLooper mCameraInit; - protected CameraDeviceUserShim(int cameraId, LegacyCameraDevice legacyCamera) { + protected CameraDeviceUserShim(int cameraId, LegacyCameraDevice legacyCamera, + CameraCharacteristics characteristics, CameraLooper cameraInit) { mLegacyDevice = legacyCamera; mConfiguring = false; mSurfaces = new SparseArray<Surface>(); + mCameraCharacteristics = characteristics; + mCameraInit = cameraInit; mSurfaceIdCounter = 0; } + /** + * Create a separate looper/thread for the camera to run on; open the camera. + * + * <p>Since the camera automatically latches on to the current thread's looper, + * it's important that we have our own thread with our own looper to guarantee + * that the camera callbacks get correctly posted to our own thread.</p> + */ + private static class CameraLooper implements Runnable, AutoCloseable { + private final int mCameraId; + private Looper mLooper; + private volatile int mInitErrors; + private final Camera mCamera = Camera.openUninitialized(); + private final ConditionVariable mStartDone = new ConditionVariable(); + private final Thread mThread; + + /** + * Spin up a new thread, immediately open the camera in the background. + * + * <p>Use {@link #waitForOpen} to block until the camera is finished opening.</p> + * + * @param cameraId numeric camera Id + * + * @see #waitForOpen + */ + public CameraLooper(int cameraId) { + mCameraId = cameraId; + + mThread = new Thread(this); + mThread.start(); + } + + public Camera getCamera() { + return mCamera; + } + + @Override + public void run() { + // Set up a looper to be used by camera. + Looper.prepare(); + + // Save the looper so that we can terminate this thread + // after we are done with it. + mLooper = Looper.myLooper(); + mInitErrors = mCamera.cameraInitUnspecified(mCameraId); + + mStartDone.open(); + Looper.loop(); // Blocks forever until #close is called. + } + + /** + * Quit the looper safely; then join until the thread shuts down. + */ + @Override + public void close() { + if (mLooper == null) { + return; + } + + mLooper.quitSafely(); + try { + mThread.join(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + + mLooper = null; + } + + /** + * Block until the camera opens; then return its initialization error code (if any). + * + * @param timeoutMs timeout in milliseconds + * + * @return int error code + * + * @throws CameraRuntimeException if the camera open times out with ({@code CAMERA_ERROR}) + */ + public int waitForOpen(int timeoutMs) { + // Block until the camera is open asynchronously + if (!mStartDone.block(timeoutMs)) { + Log.e(TAG, "waitForOpen - Camera failed to open after timeout of " + + OPEN_CAMERA_TIMEOUT_MS + " ms"); + try { + mCamera.release(); + } catch (RuntimeException e) { + Log.e(TAG, "connectBinderShim - Failed to release camera after timeout ", e); + } + + throw new CameraRuntimeException(CameraAccessException.CAMERA_ERROR); + } + + return mInitErrors; + } + } + public static CameraDeviceUserShim connectBinderShim(ICameraDeviceCallbacks callbacks, int cameraId) { if (DEBUG) { Log.d(TAG, "Opening shim Camera device"); } - // TODO: Move open/init into LegacyCameraDevice thread when API is switched to async. - Camera legacyCamera = Camera.openUninitialized(); - int initErrors = legacyCamera.cameraInitUnspecified(cameraId); + + /* + * Put the camera open on a separate thread with its own looper; otherwise + * if the main thread is used then the callbacks might never get delivered + * (e.g. in CTS which run its own default looper only after tests) + */ + + CameraLooper init = new CameraLooper(cameraId); + + // TODO: Make this async instead of blocking + int initErrors = init.waitForOpen(OPEN_CAMERA_TIMEOUT_MS); + Camera legacyCamera = init.getCamera(); // Check errors old HAL initialization CameraBinderDecorator.throwOnError(initErrors); + CameraInfo info = new CameraInfo(); + Camera.getCameraInfo(cameraId, info); + + CameraCharacteristics characteristics = + LegacyMetadataMapper.createCharacteristics(legacyCamera.getParameters(), info); LegacyCameraDevice device = new LegacyCameraDevice(cameraId, legacyCamera, callbacks); - return new CameraDeviceUserShim(cameraId, device); + return new CameraDeviceUserShim(cameraId, device, characteristics, init); } @Override @@ -90,7 +208,12 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { if (DEBUG) { Log.d(TAG, "disconnect called."); } - mLegacyDevice.close(); + + try { + mLegacyDevice.close(); + } finally { + mCameraInit.close(); + } } @Override @@ -218,8 +341,17 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { if (DEBUG) { Log.d(TAG, "createDefaultRequest called."); } - // TODO: implement createDefaultRequest. - Log.e(TAG, "createDefaultRequest unimplemented."); + + CameraMetadataNative template; + try { + template = + LegacyMetadataMapper.createRequestTemplate(mCameraCharacteristics, templateId); + } catch (IllegalArgumentException e) { + Log.e(TAG, "createDefaultRequest - invalid templateId specified"); + return CameraBinderDecorator.BAD_VALUE; + } + + request.swap(template); return CameraBinderDecorator.NO_ERROR; } diff --git a/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java index cb951b320b6a..b6264dc54da7 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java +++ b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java @@ -29,12 +29,14 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.RemoteException; import android.util.Log; +import android.util.Size; import android.view.Surface; import java.util.ArrayList; import java.util.List; import static android.hardware.camera2.utils.CameraBinderDecorator.*; +import static com.android.internal.util.Preconditions.*; /** * This class emulates the functionality of a Camera2 device using a the old Camera class. @@ -367,9 +369,27 @@ public class LegacyCameraDevice implements AutoCloseable { } } + /** + * Query the surface for its currently configured default buffer size. + * @param surface a non-{@code null} {@code Surface} + * @return the width and height of the surface + * + * @throws NullPointerException if the {@code surface} was {@code null} + * @throws IllegalStateException if the {@code surface} was invalid + */ + static Size getSurfaceSize(Surface surface) { + checkNotNull(surface); + + int[] dimens = new int[2]; + nativeDetectSurfaceDimens(surface, /*out*/dimens); + + return new Size(dimens[0], dimens[1]); + } + protected static native int nativeDetectSurfaceType(Surface surface); - protected static native void nativeDetectSurfaceDimens(Surface surface, int[] dimens); + protected static native void nativeDetectSurfaceDimens(Surface surface, + /*out*/int[/*2*/] dimens); protected static native void nativeConfigureSurface(Surface surface, int width, int height, int pixelFormat); diff --git a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java index 1cde2c4df7d5..f702556bc14a 100644 --- a/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java +++ b/core/java/android/hardware/camera2/legacy/LegacyMetadataMapper.java @@ -21,6 +21,8 @@ import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.Size; import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.impl.CameraMetadataNative; @@ -32,6 +34,7 @@ import android.util.Range; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import static com.android.internal.util.Preconditions.*; import static android.hardware.camera2.CameraCharacteristics.*; @@ -56,6 +59,42 @@ public class LegacyMetadataMapper { private static final long APPROXIMATE_JPEG_ENCODE_TIME = 600; // ms private static final long NS_PER_MS = 1000000; + /* + * Development hijinks: Lie about not supporting certain capabilities + * + * - Unblock some CTS tests from running whose main intent is not the metadata itself + * + * TODO: Remove these constants and strip out any code that previously relied on them + * being set to true. + */ + private static final boolean LIE_ABOUT_FLASH = true; + private static final boolean LIE_ABOUT_AE = true; + private static final boolean LIE_ABOUT_AF = true; + private static final boolean LIE_ABOUT_AWB = true; + + /** + * Create characteristics for a legacy device by mapping the {@code parameters} + * and {@code info} + * + * @param parameters A non-{@code null} parameters set + * @param info Camera info with camera facing direction and angle of orientation + * + * @return static camera characteristics for a camera device + * + * @throws NullPointerException if any of the args were {@code null} + */ + public static CameraCharacteristics createCharacteristics(Camera.Parameters parameters, + CameraInfo info) { + checkNotNull(parameters, "parameters must not be null"); + checkNotNull(info, "info must not be null"); + + String paramStr = parameters.flatten(); + android.hardware.CameraInfo outerInfo = new android.hardware.CameraInfo(); + outerInfo.info = info; + + return createCharacteristics(paramStr, outerInfo); + } + /** * Create characteristics for a legacy device by mapping the {@code parameters} * and {@code info} @@ -99,7 +138,8 @@ public class LegacyMetadataMapper { private static void mapCameraParameters(CameraMetadataNative m, Camera.Parameters p) { m.set(INFO_SUPPORTED_HARDWARE_LEVEL, INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED); mapStreamConfigs(m, p); - mapAeConfig(m, p); + mapControlAe(m, p); + mapControlAwb(m, p); mapCapabilities(m, p); mapLens(m, p); mapFlash(m, p); @@ -164,8 +204,10 @@ public class LegacyMetadataMapper { } @SuppressWarnings({"unchecked"}) - private static void mapAeConfig(CameraMetadataNative m, Camera.Parameters p) { - + private static void mapControlAe(CameraMetadataNative m, Camera.Parameters p) { + /* + * control.aeAvailableTargetFpsRanges + */ List<int[]> fpsRanges = p.getSupportedPreviewFpsRange(); if (fpsRanges == null) { throw new AssertionError("Supported FPS ranges cannot be null."); @@ -182,6 +224,10 @@ public class LegacyMetadataMapper { } m.set(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, ranges); + /* + * control.aeAvailableAntiBandingModes + */ + List<String> antiBandingModes = p.getSupportedAntibanding(); int antiBandingModesSize = antiBandingModes.size(); if (antiBandingModesSize > 0) { @@ -198,6 +244,49 @@ public class LegacyMetadataMapper { } m.set(CONTROL_AE_AVAILABLE_ANTIBANDING_MODES, Arrays.copyOf(modes, j)); } + + /* + * control.aeAvailableModes + */ + List<String> flashModes = p.getSupportedFlashModes(); + + String[] flashModeStrings = new String[] { + Camera.Parameters.FLASH_MODE_AUTO, + Camera.Parameters.FLASH_MODE_ON, + Camera.Parameters.FLASH_MODE_RED_EYE, + // Map these manually + Camera.Parameters.FLASH_MODE_TORCH, + Camera.Parameters.FLASH_MODE_OFF, + }; + int[] flashModeInts = new int[] { + CONTROL_AE_MODE_ON, + CONTROL_AE_MODE_ON_AUTO_FLASH, + CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE + }; + int[] aeAvail = convertStringListToIntArray(flashModes, flashModeStrings, flashModeInts); + + // No flash control -> AE is always on + if (aeAvail == null || aeAvail.length == 0) { + aeAvail = new int[] { + CONTROL_AE_MODE_ON + }; + } + + if (LIE_ABOUT_FLASH) { + // TODO: Remove this branch + Log.w(TAG, "mapControlAe - lying; saying we only support CONTROL_AE_MODE_ON"); + aeAvail = new int[] { + CONTROL_AE_MODE_ON + }; + } + + m.set(CONTROL_AE_AVAILABLE_MODES, aeAvail); + } + + private static void mapControlAwb(CameraMetadataNative m, Camera.Parameters p) { + if (!LIE_ABOUT_AWB) { + throw new AssertionError("Not implemented yet"); + } } private static void mapCapabilities(CameraMetadataNative m, Camera.Parameters p) { @@ -226,6 +315,12 @@ public class LegacyMetadataMapper { } } + if (LIE_ABOUT_FLASH && flashAvailable) { + // TODO: remove this branch + Log.w(TAG, "mapFlash - lying; saying we never support flash"); + flashAvailable = false; + } + m.set(FLASH_INFO_AVAILABLE, flashAvailable); } @@ -320,7 +415,52 @@ public class LegacyMetadataMapper { CaptureRequest request, long timestamp) { CameraMetadataNative result = new CameraMetadataNative(); + + /* + * control + */ + // control.afState + if (LIE_ABOUT_AF) { + // TODO: Implement autofocus state machine + result.set(CaptureResult.CONTROL_AF_MODE, request.get(CaptureRequest.CONTROL_AF_MODE)); + } + + // control.aeState + if (LIE_ABOUT_AE) { + // Lie to pass CTS temporarily. + // TODO: Implement precapture trigger, after which we can report CONVERGED ourselves + result.set(CaptureResult.CONTROL_AE_STATE, + CONTROL_AE_STATE_CONVERGED); + + result.set(CaptureResult.CONTROL_AE_MODE, + request.get(CaptureRequest.CONTROL_AE_MODE)); + } + + // control.awbLock + result.set(CaptureResult.CONTROL_AWB_LOCK, params.getAutoWhiteBalanceLock()); + + // control.awbState + if (LIE_ABOUT_AWB) { + // Lie to pass CTS temporarily. + // TODO: CTS needs to be updated not to query this value + // for LIMITED devices unless its guaranteed to be available. + result.set(CaptureResult.CONTROL_AWB_STATE, + CameraMetadata.CONTROL_AWB_STATE_CONVERGED); + // TODO: Read the awb mode from parameters instead + result.set(CaptureResult.CONTROL_AWB_MODE, + request.get(CaptureRequest.CONTROL_AWB_MODE)); + } + + /* + * lens + */ + // lens.focalLength result.set(CaptureResult.LENS_FOCAL_LENGTH, params.getFocalLength()); + + /* + * sensor + */ + // sensor.timestamp result.set(CaptureResult.SENSOR_TIMESTAMP, timestamp); // TODO: Remaining result metadata tags conversions. @@ -335,17 +475,149 @@ public class LegacyMetadataMapper { */ public static void convertRequestMetadata(CaptureRequest request, /*out*/Camera.Parameters params) { + + /* + * control.ae* + */ + // control.aeAntibandingMode Integer antiBandingMode = request.get(CaptureRequest.CONTROL_AE_ANTIBANDING_MODE); if (antiBandingMode != null) { String legacyMode = convertAntiBandingModeToLegacy(antiBandingMode); if (legacyMode != null) params.setAntibanding(legacyMode); } + // control.aeTargetFpsRange Range<Integer> aeFpsRange = request.get(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE); if (aeFpsRange != null) { int[] legacyFps = convertAeFpsRangeToLegacy(aeFpsRange); params.setPreviewFpsRange(legacyFps[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], legacyFps[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); } + + /* + * control + */ + // control.awbLock + Boolean awbLock = request.get(CaptureRequest.CONTROL_AWB_LOCK); + params.setAutoWhiteBalanceLock(awbLock == null ? false : awbLock); + } + + /** + * Create an int[] from the List<> by using {@code convertFrom} and {@code convertTo} + * as a one-to-one map (via the index). + * + * <p>Strings not appearing in {@code convertFrom} are ignored (with a warning); + * strings appearing in {@code convertFrom} but not {@code convertTo} are silently + * dropped.</p> + * + * @param list Source list of strings + * @param convertFrom Conversion list of strings + * @param convertTo Conversion list of ints + * @return An array of ints where the values correspond to the ones in {@code convertTo} + * or {@code null} if {@code list} was {@code null} + */ + private static int[] convertStringListToIntArray( + List<String> list, String[] convertFrom, int[] convertTo) { + if (list == null) { + return null; + } + + List<Integer> convertedList = new ArrayList<>(list.size()); + + for (String str : list) { + int strIndex = getArrayIndex(convertFrom, str); + + // Guard against bad API1 values + if (strIndex < 0) { + Log.w(TAG, "Ignoring invalid parameter " + str); + continue; + } + + // Ignore values we can't map into (intentional) + if (strIndex < convertTo.length) { + convertedList.add(convertTo[strIndex]); + } + } + + int[] returnArray = new int[convertedList.size()]; + for (int i = 0; i < returnArray.length; ++i) { + returnArray[i] = convertedList.get(i); + } + + return returnArray; + } + + /** Return the index of {@code needle} in the {@code array}, or else {@code -1} */ + private static <T> int getArrayIndex(T[] array, T needle) { + if (needle == null) { + return -1; + } + + int index = 0; + for (T elem : array) { + if (Objects.equals(elem, needle)) { + return index; + } + index++; + } + + return -1; + } + + /** + * Create a request template + * + * @param c a non-{@code null} camera characteristics for this camera + * @param templateId a non-negative template ID + * + * @return a non-{@code null} request template + * + * @throws IllegalArgumentException if {@code templateId} was invalid + * + * @see android.hardware.camera2.CameraDevice#TEMPLATE_MANUAL + */ + public static CameraMetadataNative createRequestTemplate( + CameraCharacteristics c, int templateId) { + if (templateId < 0 || templateId > CameraDevice.TEMPLATE_MANUAL) { + throw new IllegalArgumentException("templateId out of range"); + } + + CameraMetadataNative m = new CameraMetadataNative(); + + /* + * NOTE: If adding new code here and it needs to query the static info, + * query the camera characteristics, so we can reuse this for api2 code later + * to create our own templates in the framework + */ + + if (LIE_ABOUT_AWB) { + m.set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_AUTO); + } else { + throw new AssertionError("Valid control.awbMode not implemented yet"); + } + + // control.aeMode + m.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON); + // AE is always unconditionally available in API1 devices + + // control.afMode + { + Float minimumFocusDistance = c.get(LENS_INFO_MINIMUM_FOCUS_DISTANCE); + + int afMode; + if (minimumFocusDistance != null && + minimumFocusDistance == LENS_INFO_MINIMUM_FOCUS_DISTANCE_FIXED_FOCUS) { + // Cannot control auto-focus with fixed-focus cameras + afMode = CameraMetadata.CONTROL_AF_MODE_OFF; + } else { + // If a minimum focus distance is reported; the camera must have AF + afMode = CameraMetadata.CONTROL_AF_MODE_AUTO; + } + + m.set(CaptureRequest.CONTROL_AF_MODE, afMode); + } + + // TODO: map other request template values + return m; } } diff --git a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java index a4b10997125d..efc2b0eec2b4 100644 --- a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java +++ b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java @@ -64,7 +64,7 @@ public class RequestThreadManager { private static final int MSG_CLEANUP = 3; private static final int PREVIEW_FRAME_TIMEOUT = 300; // ms - private static final int JPEG_FRAME_TIMEOUT = 1000; // ms + private static final int JPEG_FRAME_TIMEOUT = 3000; // ms (same as CTS for API2) private static final float ASPECT_RATIO_TOLERANCE = 0.01f; private boolean mPreviewRunning = false; @@ -105,11 +105,11 @@ public class RequestThreadManager { /** - * Comparator for {@link Size} objects. + * Comparator for {@link Size} objects by the area. * - * <p>This comparator compares by rectangle area. Tiebreaks on width.</p> + * <p>This comparator totally orders by rectangle area. Tiebreaks on width.</p> */ - private static class SizeComparator implements Comparator<Size> { + private static class SizeAreaComparator implements Comparator<Size> { @Override public int compare(Size size, Size size2) { if (size == null || size2 == null) { @@ -262,7 +262,11 @@ public class RequestThreadManager { } private void doJpegCapture(RequestHolder request) throws IOException { + if (DEBUG) Log.d(TAG, "doJpegCapture"); + if (!mPreviewRunning) { + if (DEBUG) Log.d(TAG, "doJpegCapture - create fake surface"); + createDummySurface(); mCamera.setPreviewTexture(mDummyTexture); startPreview(); @@ -373,6 +377,18 @@ public class RequestThreadManager { } } + Size smallestSupportedJpegSize = calculatePictureSize(mCallbackOutputs, mParams); + if (smallestSupportedJpegSize != null) { + /* + * Set takePicture size to the smallest supported JPEG size large enough + * to scale/crop out of for the bounding rectangle of the configured JPEG sizes. + */ + + Log.i(TAG, "configureOutputs - set take picture size to " + smallestSupportedJpegSize); + mParams.setPictureSize( + smallestSupportedJpegSize.getWidth(), smallestSupportedJpegSize.getHeight()); + } + // TODO: Detect and optimize single-output paths here to skip stream teeing. if (mGLThreadManager == null) { mGLThreadManager = new GLThreadManager(mCameraId); @@ -385,10 +401,105 @@ public class RequestThreadManager { if (mPreviewTexture != null) { mPreviewTexture.setOnFrameAvailableListener(mPreviewCallback); } + + // TODO: configure the JPEG surface with some arbitrary size + // using LegacyCameraDevice.nativeConfigureSurface + } + + /** + * Find a JPEG size (that is supported by the legacy camera device) which is equal to or larger + * than all of the configured {@code JPEG} outputs (by both width and height). + * + * <p>If multiple supported JPEG sizes are larger, select the smallest of them which + * still satisfies the above constraint.</p> + * + * <p>As a result, the returned size is guaranteed to be usable without needing + * to upscale any of the outputs. If only one {@code JPEG} surface is used, + * then no scaling/cropping is necessary between the taken picture and + * the {@code JPEG} output surface.</p> + * + * @param callbackOutputs a non-{@code null} list of {@code Surface}s with any image formats + * @param params api1 parameters (used for reading only) + * + * @return a size large enough to fit all of the configured {@code JPEG} outputs, or + * {@code null} if the {@code callbackOutputs} did not have any {@code JPEG} + * surfaces. + */ + private Size calculatePictureSize( + Collection<Surface> callbackOutputs, Camera.Parameters params) { + /* + * Find the largest JPEG size (if any), from the configured outputs: + * - the api1 picture size should be set to the smallest legal size that's at least as large + * as the largest configured JPEG size + */ + List<Size> configuredJpegSizes = new ArrayList<Size>(); + for (Surface callbackSurface : callbackOutputs) { + int format = LegacyCameraDevice.nativeDetectSurfaceType(callbackSurface); + + if (format != CameraMetadataNative.NATIVE_JPEG_FORMAT) { + continue; // Ignore non-JPEG callback formats + } + + Size jpegSize = LegacyCameraDevice.getSurfaceSize(callbackSurface); + configuredJpegSizes.add(jpegSize); + } + if (!configuredJpegSizes.isEmpty()) { + /* + * Find the largest configured JPEG width, and height, independently + * of the rest. + * + * The rest of the JPEG streams can be cropped out of this smallest bounding + * rectangle. + */ + int maxConfiguredJpegWidth = -1; + int maxConfiguredJpegHeight = -1; + for (Size jpegSize : configuredJpegSizes) { + maxConfiguredJpegWidth = jpegSize.getWidth() > maxConfiguredJpegWidth ? + jpegSize.getWidth() : maxConfiguredJpegWidth; + maxConfiguredJpegHeight = jpegSize.getHeight() > maxConfiguredJpegHeight ? + jpegSize.getHeight() : maxConfiguredJpegHeight; + } + Size smallestBoundJpegSize = new Size(maxConfiguredJpegWidth, maxConfiguredJpegHeight); + + List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes()); + + /* + * Find the smallest supported JPEG size that can fit the smallest bounding + * rectangle for the configured JPEG sizes. + */ + List<Size> candidateSupportedJpegSizes = new ArrayList<>(); + for (Size supportedJpegSize : supportedJpegSizes) { + if (supportedJpegSize.getWidth() >= maxConfiguredJpegWidth && + supportedJpegSize.getHeight() >= maxConfiguredJpegHeight) { + candidateSupportedJpegSizes.add(supportedJpegSize); + } + } + + if (candidateSupportedJpegSizes.isEmpty()) { + throw new AssertionError( + "Could not find any supported JPEG sizes large enough to fit " + + smallestBoundJpegSize); + } + + Size smallestSupportedJpegSize = Collections.min(candidateSupportedJpegSizes, + new SizeAreaComparator()); + + if (!smallestSupportedJpegSize.equals(smallestBoundJpegSize)) { + Log.w(TAG, + String.format( + "configureOutputs - Will need to crop picture %s into " + + "smallest bound size %s", + smallestSupportedJpegSize, smallestBoundJpegSize)); + } + + return smallestSupportedJpegSize; + } + + return null; } private static Size findLargestByArea(List<Size> sizes) { - return Collections.max(sizes, new SizeComparator()); + return Collections.max(sizes, new SizeAreaComparator()); } private static boolean checkAspectRatiosMatch(Size a, Size b) { diff --git a/core/jni/android_hardware_camera2_legacy_LegacyCameraDevice.cpp b/core/jni/android_hardware_camera2_legacy_LegacyCameraDevice.cpp index 004842649788..57058a6269a2 100644 --- a/core/jni/android_hardware_camera2_legacy_LegacyCameraDevice.cpp +++ b/core/jni/android_hardware_camera2_legacy_LegacyCameraDevice.cpp @@ -15,6 +15,7 @@ */ #define LOG_TAG "Legacy-CameraDevice-JNI" +// #define LOG_NDEBUG 0 #include <utils/Log.h> #include <utils/Errors.h> #include <utils/Trace.h> @@ -26,6 +27,7 @@ #include <ui/GraphicBuffer.h> #include <system/window.h> +#include <hardware/camera3.h> using namespace android; @@ -118,8 +120,9 @@ static status_t configureSurface(const sp<ANativeWindow>& anw, return err; } - ALOGV("%s: Setting buffer count to %d", __FUNCTION__, - maxBufferSlack + 1 + minUndequeuedBuffers); + ALOGV("%s: Setting buffer count to %d, size to (%dx%d), fmt (0x%x)", __FUNCTION__, + maxBufferSlack + 1 + minUndequeuedBuffers, + width, height, pixelFmt); err = native_window_set_buffer_count(anw.get(), maxBufferSlack + 1 + minUndequeuedBuffers); if (err != NO_ERROR) { ALOGE("%s: Failed to set native window buffer count, error %s (%d).", __FUNCTION__, @@ -148,11 +151,29 @@ static status_t produceFrame(const sp<ANativeWindow>& anw, int32_t width, // Width of the pixelBuffer int32_t height, // Height of the pixelBuffer int32_t pixelFmt, // Format of the pixelBuffer - int64_t bufSize) { + int32_t bufSize) { ATRACE_CALL(); status_t err = NO_ERROR; ANativeWindowBuffer* anb; - ALOGV("%s: Dequeue buffer from %p",__FUNCTION__, anw.get()); + ALOGV("%s: Dequeue buffer from %p %dx%d (fmt=%x, size=%x)", + __FUNCTION__, anw.get(), width, height, pixelFmt, bufSize); + + if (anw == 0) { + ALOGE("%s: anw must not be NULL", __FUNCTION__); + return BAD_VALUE; + } else if (pixelBuffer == NULL) { + ALOGE("%s: pixelBuffer must not be NULL", __FUNCTION__); + return BAD_VALUE; + } else if (width < 0) { + ALOGE("%s: width must be non-negative", __FUNCTION__); + return BAD_VALUE; + } else if (height < 0) { + ALOGE("%s: height must be non-negative", __FUNCTION__); + return BAD_VALUE; + } else if (bufSize < 0) { + ALOGE("%s: bufSize must be non-negative", __FUNCTION__); + return BAD_VALUE; + } if (width < 0 || height < 0 || bufSize < 0) { ALOGE("%s: Illegal argument, negative dimension passed to produceFrame", __FUNCTION__); @@ -163,6 +184,8 @@ static status_t produceFrame(const sp<ANativeWindow>& anw, err = native_window_dequeue_buffer_and_wait(anw.get(), &anb); if (err != NO_ERROR) return err; + // TODO: check anb is large enough to store the results + sp<GraphicBuffer> buf(new GraphicBuffer(anb, /*keepOwnership*/false)); switch(pixelFmt) { @@ -257,7 +280,12 @@ static status_t produceFrame(const sp<ANativeWindow>& anw, err); return err; } + struct camera3_jpeg_blob footer = { + jpeg_blob_id: CAMERA3_JPEG_BLOB_ID, + jpeg_size: (uint32_t)width + }; memcpy(img, pixelBuffer, width); + memcpy(img + anb->width - sizeof(footer), &footer, sizeof(footer)); break; } default: { diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index 41ed9e133706..ad7ee7ad091e 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -33,6 +33,9 @@ #include <jni.h> #include <JNIHelp.h> +#include <stdint.h> +#include <inttypes.h> + #define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) #define ANDROID_MEDIA_IMAGEREADER_CTX_JNI_ID "mNativeContext" @@ -300,6 +303,14 @@ static uint32_t Image_getJpegSize(CpuConsumer::LockedBuffer* buffer) // failed to find size, default to whole buffer if (size == 0) { + /* + * This is a problem because not including the JPEG header + * means that in certain rare situations a regular JPEG blob + * will be misidentified as having a header, in which case + * we will get a garbage size value. + */ + ALOGW("%s: No JPEG header detected, defaulting to size=width=%d", + __FUNCTION__, width); size = width; } @@ -848,6 +859,14 @@ static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx) // Create byteBuffer from native buffer Image_getLockedBufferInfo(env, buffer, idx, &base, &size); + + if (size > static_cast<uint32_t>(INT32_MAX)) { + // Byte buffer have 'int capacity', so check the range + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Size too large for bytebuffer capacity " PRIu32, size); + return NULL; + } + byteBuffer = env->NewDirectByteBuffer(base, size); // TODO: throw dvm exOutOfMemoryError? if ((byteBuffer == NULL) && (env->ExceptionCheck() == false)) { |