Merge "Add constructor for standalone exif data"
diff --git a/api/current.txt b/api/current.txt
index f1f011e..bba1938 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -23968,6 +23968,7 @@
     ctor public ExifInterface(@NonNull String) throws java.io.IOException;
     ctor public ExifInterface(@NonNull java.io.FileDescriptor) throws java.io.IOException;
     ctor public ExifInterface(@NonNull java.io.InputStream) throws java.io.IOException;
+    method @NonNull public static android.media.ExifInterface fromStandalone(@NonNull java.io.InputStream) throws java.io.IOException;
     method public double getAltitude(double);
     method @Nullable public String getAttribute(@NonNull String);
     method @Nullable public byte[] getAttributeBytes(@NonNull String);
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 53babcb..5341d07 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -1357,6 +1357,7 @@
     private AssetManager.AssetInputStream mAssetInputStream;
     private boolean mIsInputStream;
     private int mMimeType;
+    private boolean mIsStandalone;
     @UnsupportedAppUsage
     private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length];
     private Set<Integer> mHandledIfdOffsets = new HashSet<>(EXIF_TAGS.length);
@@ -1411,6 +1412,7 @@
         if (fileDescriptor == null) {
             throw new NullPointerException("fileDescriptor cannot be null");
         }
+
         mAssetInputStream = null;
         mFilename = null;
         // When FileDescriptor is duplicated and set to FileInputStream, ownership needs to be
@@ -1442,26 +1444,61 @@
 
     /**
      * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
-     * for input streams. The given input stream will proceed its current position. Developers
+     * for input streams. The given input stream will proceed from its current position. Developers
      * should close the input stream after use.
      */
     public ExifInterface(@NonNull InputStream inputStream) throws IOException {
+        this(inputStream, false);
+    }
+
+    /**
+     * Reads Exif tags from the specified standalone input stream. Standalone data refers to Exif
+     * data that exists by itself and is not contained in a file format such as jpeg or png.
+     * The format of the standalone data must follow the below structure:
+     *     Exif Identifier Code ("Exif\0\0") + TIFF header + IFD data
+     * See JEITA CP-3451C Section 4.5.2 and 4.5.4 specifications for more details.
+     * <p>
+     * Attribute mutation is not supported for this constructor. The given input stream will proceed
+     * from its current position. Developers should close the input stream after use. This
+     * constructor is not intended to be used with an input stream that performs any networking
+     * operations.
+     *
+     * @throws IOException if the data does not follow the aforementioned structure.
+     */
+    @NonNull
+    public static ExifInterface fromStandalone(@NonNull InputStream inputStream)
+            throws IOException {
+        if (isStandalone(inputStream)) {
+            return new ExifInterface(inputStream, true);
+        }
+        throw new IOException("Given data does not follow the structure of a standalone exif "
+                + "data.");
+    }
+
+    private ExifInterface(@NonNull InputStream inputStream, boolean isFromStandalone)
+            throws IOException {
         if (inputStream == null) {
             throw new NullPointerException("inputStream cannot be null");
         }
         mFilename = null;
-        if (inputStream instanceof AssetManager.AssetInputStream) {
-            mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
-            mSeekableFileDescriptor = null;
-        } else if (inputStream instanceof FileInputStream
-                && isSeekableFD(((FileInputStream) inputStream).getFD())) {
+
+        if (isFromStandalone) {
+            mIsStandalone = true;
             mAssetInputStream = null;
-            mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD();
+            mSeekableFileDescriptor = null;
         } else {
-            mAssetInputStream = null;
-            mSeekableFileDescriptor = null;
+            if (inputStream instanceof AssetManager.AssetInputStream) {
+                mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
+                mSeekableFileDescriptor = null;
+            } else if (inputStream instanceof FileInputStream
+                    && (isSeekableFD(((FileInputStream) inputStream).getFD()))) {
+                mAssetInputStream = null;
+                mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD();
+            } else {
+                mAssetInputStream = null;
+                mSeekableFileDescriptor = null;
+            }
         }
-        mIsInputStream = true;
         loadAttributes(inputStream);
     }
 
@@ -1798,51 +1835,57 @@
             }
 
             // Check file type
-            in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
-            mMimeType = getMimeType((BufferedInputStream) in);
+            if (!mIsStandalone) {
+                in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
+                mMimeType = getMimeType((BufferedInputStream) in);
+            }
 
             // Create byte-ordered input stream
             ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in);
 
-            switch (mMimeType) {
-                case IMAGE_TYPE_JPEG: {
-                    getJpegAttributes(inputStream, 0, IFD_TYPE_PRIMARY); // 0 is offset
-                    break;
+            if (!mIsStandalone) {
+                switch (mMimeType) {
+                    case IMAGE_TYPE_JPEG: {
+                        getJpegAttributes(inputStream, 0, IFD_TYPE_PRIMARY); // 0 is offset
+                        break;
+                    }
+                    case IMAGE_TYPE_RAF: {
+                        getRafAttributes(inputStream);
+                        break;
+                    }
+                    case IMAGE_TYPE_HEIF: {
+                        getHeifAttributes(inputStream);
+                        break;
+                    }
+                    case IMAGE_TYPE_ORF: {
+                        getOrfAttributes(inputStream);
+                        break;
+                    }
+                    case IMAGE_TYPE_RW2: {
+                        getRw2Attributes(inputStream);
+                        break;
+                    }
+                    case IMAGE_TYPE_PNG: {
+                        getPngAttributes(inputStream);
+                        break;
+                    }
+                    case IMAGE_TYPE_ARW:
+                    case IMAGE_TYPE_CR2:
+                    case IMAGE_TYPE_DNG:
+                    case IMAGE_TYPE_NEF:
+                    case IMAGE_TYPE_NRW:
+                    case IMAGE_TYPE_PEF:
+                    case IMAGE_TYPE_SRW:
+                    case IMAGE_TYPE_UNKNOWN: {
+                        getRawAttributes(inputStream);
+                        break;
+                    }
+                    default: {
+                        break;
+                    }
                 }
-                case IMAGE_TYPE_RAF: {
-                    getRafAttributes(inputStream);
-                    break;
-                }
-                case IMAGE_TYPE_HEIF: {
-                    getHeifAttributes(inputStream);
-                    break;
-                }
-                case IMAGE_TYPE_ORF: {
-                    getOrfAttributes(inputStream);
-                    break;
-                }
-                case IMAGE_TYPE_RW2: {
-                    getRw2Attributes(inputStream);
-                    break;
-                }
-                case IMAGE_TYPE_PNG: {
-                    getPngAttributes(inputStream);
-                    break;
-                }
-                case IMAGE_TYPE_ARW:
-                case IMAGE_TYPE_CR2:
-                case IMAGE_TYPE_DNG:
-                case IMAGE_TYPE_NEF:
-                case IMAGE_TYPE_NRW:
-                case IMAGE_TYPE_PEF:
-                case IMAGE_TYPE_SRW:
-                case IMAGE_TYPE_UNKNOWN: {
-                    getRawAttributes(inputStream);
-                    break;
-                }
-                default: {
-                    break;
-                }
+            } else {
+                getStandaloneAttributes(inputStream);
             }
             // Set thumbnail image offset and length
             setThumbnailData(inputStream);
@@ -2124,6 +2167,9 @@
         }
 
         if (mHasThumbnail) {
+            if (mIsStandalone) {
+                return new long[] { mThumbnailOffset + mExifOffset, mThumbnailLength };
+            }
             return new long[] { mThumbnailOffset, mThumbnailLength };
         } else {
             return null;
@@ -2368,6 +2414,7 @@
 
     // Checks the type of image file
     private int getMimeType(BufferedInputStream in) throws IOException {
+        // TODO (b/142218289): Need to handle case where input stream does not support mark
         in.mark(SIGNATURE_CHECK_SIZE);
         byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
         in.read(signatureCheckBytes);
@@ -2562,6 +2609,18 @@
         return true;
     }
 
+    private static boolean isStandalone(InputStream inputStream) throws IOException {
+        byte[] signatureCheckBytes = new byte[IDENTIFIER_EXIF_APP1.length];
+        inputStream.read(signatureCheckBytes);
+
+        for (int i = 0; i < IDENTIFIER_EXIF_APP1.length; i++) {
+            if (signatureCheckBytes[i] != IDENTIFIER_EXIF_APP1[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Loads EXIF attributes from a JPEG input stream.
      *
@@ -2633,7 +2692,6 @@
                         final long offset = start + IDENTIFIER_EXIF_APP1.length;
                         final byte[] value = Arrays.copyOfRange(bytes,
                                 IDENTIFIER_EXIF_APP1.length, bytes.length);
-
                         readExifSegment(value, imageType);
 
                         // Save offset values for handleThumbnailFromJfif() function
@@ -2953,6 +3011,16 @@
         }
     }
 
+    private void getStandaloneAttributes(ByteOrderedDataInputStream in) throws IOException {
+        // TODO: Need to handle potential OutOfMemoryError
+        byte[] data = new byte[in.available()];
+        in.readFully(data);
+        readExifSegment(data, IFD_TYPE_PRIMARY);
+
+        // Save offset values for handleThumbnailFromJfif() function
+        mExifOffset = IDENTIFIER_EXIF_APP1.length;
+    }
+
     /**
      * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail
      * images. Both data takes the form of IFDs and can therefore be read with the
@@ -3606,8 +3674,6 @@
             int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
             int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
 
-            // The following code limits the size of thumbnail size not to overflow EXIF data area.
-            thumbnailLength = Math.min(thumbnailLength, in.getLength() - thumbnailOffset);
             if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF
                     || mMimeType == IMAGE_TYPE_RW2) {
                 thumbnailOffset += mExifOffset;
@@ -3615,6 +3681,9 @@
                 // Update offset value since RAF files have IFD data preceding MakerNote data.
                 thumbnailOffset += mOrfMakerNoteOffset;
             }
+            // The following code limits the size of thumbnail size not to overflow EXIF data area.
+            thumbnailLength = Math.min(thumbnailLength, in.getLength() - thumbnailOffset);
+
             if (DEBUG) {
                 Log.d(TAG, "Setting thumbnail attributes with offset: " + thumbnailOffset
                         + ", length: " + thumbnailLength);
@@ -4123,6 +4192,7 @@
             mDataInputStream = new DataInputStream(in);
             mLength = mDataInputStream.available();
             mPosition = 0;
+            // TODO (b/142218289): Need to handle case where input stream does not support mark
             mDataInputStream.mark(mLength);
         }
 
@@ -4138,6 +4208,7 @@
             if (mPosition > byteCount) {
                 mPosition = 0;
                 mDataInputStream.reset();
+                // TODO (b/142218289): Need to handle case where input stream does not support mark
                 mDataInputStream.mark(mLength);
             } else {
                 byteCount -= mPosition;