Merge "ExifInterface: switch from using jhead to java implementation" into nyc-dev
diff --git a/api/current.txt b/api/current.txt
index 1421784..f19ac5c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20207,6 +20207,8 @@
 
   public class ExifInterface {
     ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+    ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+    ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
     method public double getAltitude(double);
     method public java.lang.String getAttribute(java.lang.String);
     method public double getAttributeDouble(java.lang.String, double);
@@ -20228,6 +20230,10 @@
     field public static final java.lang.String TAG_APERTURE = "FNumber";
     field public static final java.lang.String TAG_DATETIME = "DateTime";
     field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+    field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+    field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+    field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+    field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
     field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
     field public static final java.lang.String TAG_FLASH = "Flash";
     field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20243,9 +20249,12 @@
     field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
     field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
     field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+    field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
     field public static final java.lang.String TAG_MAKE = "Make";
+    field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
     field public static final java.lang.String TAG_MODEL = "Model";
     field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+    field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
     field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
     field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
     field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/system-current.txt b/api/system-current.txt
index 71b0aeb..b534b1e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -21715,6 +21715,8 @@
 
   public class ExifInterface {
     ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+    ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+    ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
     method public double getAltitude(double);
     method public java.lang.String getAttribute(java.lang.String);
     method public double getAttributeDouble(java.lang.String, double);
@@ -21736,6 +21738,10 @@
     field public static final java.lang.String TAG_APERTURE = "FNumber";
     field public static final java.lang.String TAG_DATETIME = "DateTime";
     field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+    field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+    field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+    field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+    field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
     field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
     field public static final java.lang.String TAG_FLASH = "Flash";
     field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -21751,9 +21757,12 @@
     field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
     field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
     field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+    field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
     field public static final java.lang.String TAG_MAKE = "Make";
+    field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
     field public static final java.lang.String TAG_MODEL = "Model";
     field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+    field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
     field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
     field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
     field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/test-current.txt b/api/test-current.txt
index 83e4642..0b1c208 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -20216,6 +20216,8 @@
 
   public class ExifInterface {
     ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+    ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+    ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
     method public double getAltitude(double);
     method public java.lang.String getAttribute(java.lang.String);
     method public double getAttributeDouble(java.lang.String, double);
@@ -20237,6 +20239,10 @@
     field public static final java.lang.String TAG_APERTURE = "FNumber";
     field public static final java.lang.String TAG_DATETIME = "DateTime";
     field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+    field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+    field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+    field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+    field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
     field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
     field public static final java.lang.String TAG_FLASH = "Flash";
     field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20252,9 +20258,12 @@
     field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
     field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
     field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+    field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
     field public static final java.lang.String TAG_MAKE = "Make";
+    field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
     field public static final java.lang.String TAG_MODEL = "Model";
     field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+    field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
     field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
     field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
     field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 7fb67ee..1a387be 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -16,22 +16,51 @@
 
 package android.media;
 
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.regex.Pattern;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
 
 /**
  * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
  * <p>
  * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF and RAF.
+ * <p>
+ * Attribute mutation is supported for JPEG image files.
  */
 public class ExifInterface {
+    private static final String TAG = "ExifInterface";
+    private static final boolean DEBUG = false;
+
     // The Exif tag names
     /** Type is int. */
     public static final String TAG_ORIENTATION = "Orientation";
@@ -97,6 +126,20 @@
     public static final String TAG_FOCAL_LENGTH = "FocalLength";
     /** Type is String. Name of GPS processing method used for location finding. */
     public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
+    /** Type is double. */
+    public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+    /** Type is double. */
+    public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
+    /** Type is double. */
+    public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+    /** Type is int. */
+    public static final String TAG_LIGHT_SOURCE = "LightSource";
+    /** Type is int. */
+    public static final String TAG_METERING_MODE = "MeteringMode";
+    /** Type is int. */
+    public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
+    /** Type is int. */
+    public static final String TAG_EXPOSURE_MODE = "ExposureMode";
 
     // Private tags used for thumbnail information.
     private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
@@ -119,30 +162,321 @@
     // Constants used for white balance
     public static final int WHITEBALANCE_AUTO = 0;
     public static final int WHITEBALANCE_MANUAL = 1;
+
     private static SimpleDateFormat sFormatter;
 
+    // See Exchangeable image file format for digital still cameras: Exif version 2.2.
+    // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
+    // They are called "Image File Directory". They have multiple data formats to cover various
+    // image metadata from GPS longitude to camera model name.
+
+    // Types of Exif byte alignments (see JEITA CP-3451 page 10)
+    private static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
+    private static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order
+
+    // Formats for the value in IFD entry (See TIFF 6.0 spec Types page 15).
+    private static final int IFD_FORMAT_BYTE = 1;
+    private static final int IFD_FORMAT_STRING = 2;
+    private static final int IFD_FORMAT_USHORT = 3;
+    private static final int IFD_FORMAT_ULONG = 4;
+    private static final int IFD_FORMAT_URATIONAL = 5;
+    private static final int IFD_FORMAT_SBYTE = 6;
+    private static final int IFD_FORMAT_UNDEFINED = 7;
+    private static final int IFD_FORMAT_SSHORT = 8;
+    private static final int IFD_FORMAT_SLONG = 9;
+    private static final int IFD_FORMAT_SRATIONAL = 10;
+    private static final int IFD_FORMAT_SINGLE = 11;
+    private static final int IFD_FORMAT_DOUBLE = 12;
+    // Sizes of the components of each IFD value format
+    private static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+            0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8
+    };
+    private static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+            0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+    };
+
+    // A class for indicating EXIF tag.
+    private static class ExifTag {
+        public final int number;
+        public final String name;
+
+        private ExifTag(String name, int number) {
+            this.name = name;
+            this.number = number;
+        }
+    }
+
+    // Primary image IFD TIFF tags (See JEITA CP-3451 Table 14. page 54).
+    private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
+            new ExifTag("ImageWidth", 256),
+            new ExifTag("ImageLength", 257),
+            new ExifTag("BitsPerSample", 258),
+            new ExifTag("Compression", 259),
+            new ExifTag("PhotometricInterpretation", 262),
+            new ExifTag("ImageDescription", 270),
+            new ExifTag("Make", 271),
+            new ExifTag("Model", 272),
+            new ExifTag("StripOffsets", 273),
+            new ExifTag("Orientation", 274),
+            new ExifTag("SamplesPerPixel", 277),
+            new ExifTag("RowsPerStrip", 278),
+            new ExifTag("StripByteCounts", 279),
+            new ExifTag("XResolution", 282),
+            new ExifTag("YResolution", 283),
+            new ExifTag("PlanarConfiguration", 284),
+            new ExifTag("ResolutionUnit", 296),
+            new ExifTag("TransferFunction", 301),
+            new ExifTag("Software", 305),
+            new ExifTag("DateTime", 306),
+            new ExifTag("Artist", 315),
+            new ExifTag("WhitePoint", 318),
+            new ExifTag("PrimaryChromaticities", 319),
+            new ExifTag("JPEGInterchangeFormat", 513),
+            new ExifTag("JPEGInterchangeFormatLength", 514),
+            new ExifTag("YCbCrCoefficients", 529),
+            new ExifTag("YCbCrSubSampling", 530),
+            new ExifTag("YCbCrPositioning", 531),
+            new ExifTag("ReferenceBlackWhite", 532),
+            new ExifTag("Copyright", 33432),
+            new ExifTag("ExifIFDPointer", 34665),
+            new ExifTag("GPSInfoIFDPointer", 34853),
+    };
+    // Primary image IFD Exif Private tags (See JEITA CP-3451 Table 15. page 55).
+    private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
+            new ExifTag("ExposureTime", 33434),
+            new ExifTag("FNumber", 33437),
+            new ExifTag("ExposureProgram", 34850),
+            new ExifTag("SpectralSensitivity", 34852),
+            new ExifTag("ISOSpeedRatings", 34855),
+            new ExifTag("OECF", 34856),
+            new ExifTag("ExifVersion", 36864),
+            new ExifTag("DateTimeOriginal", 36867),
+            new ExifTag("DateTimeDigitized", 36868),
+            new ExifTag("ComponentsConfiguration", 37121),
+            new ExifTag("CompressedBitsPerPixel", 37122),
+            new ExifTag("ShutterSpeedValue", 37377),
+            new ExifTag("ApertureValue", 37378),
+            new ExifTag("BrightnessValue", 37379),
+            new ExifTag("ExposureBiasValue", 37380),
+            new ExifTag("MaxApertureValue", 37381),
+            new ExifTag("SubjectDistance", 37382),
+            new ExifTag("MeteringMode", 37383),
+            new ExifTag("LightSource", 37384),
+            new ExifTag("Flash", 37385),
+            new ExifTag("FocalLength", 37386),
+            new ExifTag("SubjectArea", 37396),
+            new ExifTag("MakerNote", 37500),
+            new ExifTag("UserComment", 37510),
+            new ExifTag("SubSecTime", 37520),
+            new ExifTag("SubSecTimeOriginal", 37521),
+            new ExifTag("SubSecTimeDigitized", 37522),
+            new ExifTag("FlashpixVersion", 40960),
+            new ExifTag("ColorSpace", 40961),
+            new ExifTag("PixelXDimension", 40962),
+            new ExifTag("PixelYDimension", 40963),
+            new ExifTag("RelatedSoundFile", 40964),
+            new ExifTag("InteroperabilityIFDPointer", 40965),
+            new ExifTag("FlashEnergy", 41483),
+            new ExifTag("SpatialFrequencyResponse", 41484),
+            new ExifTag("FocalPlaneXResolution", 41486),
+            new ExifTag("FocalPlaneYResolution", 41487),
+            new ExifTag("FocalPlaneResolutionUnit", 41488),
+            new ExifTag("SubjectLocation", 41492),
+            new ExifTag("ExposureIndex", 41493),
+            new ExifTag("SensingMethod", 41495),
+            new ExifTag("FileSource", 41728),
+            new ExifTag("SceneType", 41729),
+            new ExifTag("CFAPattern", 41730),
+            new ExifTag("CustomRendered", 41985),
+            new ExifTag("ExposureMode", 41986),
+            new ExifTag("WhiteBalance", 41987),
+            new ExifTag("DigitalZoomRatio", 41988),
+            new ExifTag("FocalLengthIn35mmFilm", 41989),
+            new ExifTag("SceneCaptureType", 41990),
+            new ExifTag("GainControl", 41991),
+            new ExifTag("Contrast", 41992),
+            new ExifTag("Saturation", 41993),
+            new ExifTag("Sharpness", 41994),
+            new ExifTag("DeviceSettingDescription", 41995),
+            new ExifTag("SubjectDistanceRange", 41996),
+            new ExifTag("ImageUniqueID", 42016),
+    };
+    // Primary image IFD GPS Info tags (See JEITA CP-3451 Table 16. page 56).
+    private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
+            new ExifTag("GPSVersionID", 0),
+            new ExifTag("GPSLatitudeRef", 1),
+            new ExifTag("GPSLatitude", 2),
+            new ExifTag("GPSLongitudeRef", 3),
+            new ExifTag("GPSLongitude", 4),
+            new ExifTag("GPSAltitudeRef", 5),
+            new ExifTag("GPSAltitude", 6),
+            new ExifTag("GPSTimeStamp", 7),
+            new ExifTag("GPSSatellites", 8),
+            new ExifTag("GPSStatus", 9),
+            new ExifTag("GPSMeasureMode", 10),
+            new ExifTag("GPSDOP", 11),
+            new ExifTag("GPSSpeedRef", 12),
+            new ExifTag("GPSSpeed", 13),
+            new ExifTag("GPSTrackRef", 14),
+            new ExifTag("GPSTrack", 15),
+            new ExifTag("GPSImgDirectionRef", 16),
+            new ExifTag("GPSImgDirection", 17),
+            new ExifTag("GPSMapDatum", 18),
+            new ExifTag("GPSDestLatitudeRef", 19),
+            new ExifTag("GPSDestLatitude", 20),
+            new ExifTag("GPSDestLongitudeRef", 21),
+            new ExifTag("GPSDestLongitude", 22),
+            new ExifTag("GPSDestBearingRef", 23),
+            new ExifTag("GPSDestBearing", 24),
+            new ExifTag("GPSDestDistanceRef", 25),
+            new ExifTag("GPSDestDistance", 26),
+            new ExifTag("GPSProcessingMethod", 27),
+            new ExifTag("GPSAreaInformation", 28),
+            new ExifTag("GPSDateStamp", 29),
+            new ExifTag("GPSDifferential", 30),
+    };
+    // Primary image IFD Interoperability tag (See JEITA CP-3451 Table 17. page 56).
+    private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
+            new ExifTag("InteroperabilityIndex", 1),
+    };
+    // IFD Thumbnail tags (See JEITA CP-3451 Table 18. page 57).
+    private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
+            new ExifTag("ThumbnailImageWidth", 256),
+            new ExifTag("ThumbnailImageLength", 257),
+            new ExifTag("BitsPerSample", 258),
+            new ExifTag("Compression", 259),
+            new ExifTag("PhotometricInterpretation", 262),
+            new ExifTag("ImageDescription", 270),
+            new ExifTag("Make", 271),
+            new ExifTag("Model", 272),
+            new ExifTag("StripOffsets", 273),
+            new ExifTag("Orientation", 274),
+            new ExifTag("SamplesPerPixel", 277),
+            new ExifTag("RowsPerStrip", 278),
+            new ExifTag("StripByteCounts", 279),
+            new ExifTag("XResolution", 282),
+            new ExifTag("YResolution", 283),
+            new ExifTag("PlanarConfiguration", 284),
+            new ExifTag("ResolutionUnit", 296),
+            new ExifTag("TransferFunction", 301),
+            new ExifTag("Software", 305),
+            new ExifTag("DateTime", 306),
+            new ExifTag("Artist", 315),
+            new ExifTag("WhitePoint", 318),
+            new ExifTag("PrimaryChromaticities", 319),
+            new ExifTag("JPEGInterchangeFormat", 513),
+            new ExifTag("JPEGInterchangeFormatLength", 514),
+            new ExifTag("YCbCrCoefficients", 529),
+            new ExifTag("YCbCrSubSampling", 530),
+            new ExifTag("YCbCrPositioning", 531),
+            new ExifTag("ReferenceBlackWhite", 532),
+            new ExifTag("Copyright", 33432),
+            new ExifTag("ExifIFDPointer", 34665),
+            new ExifTag("GPSInfoIFDPointer", 34853),
+    };
+
+    // See JEITA CP-3451 Figure 5. page 9.
+    // The following values are used for indicating pointers to the other Image File Directorys.
+
+    // Indices of Exif Ifd tag groups
+    private static final int IFD_TIFF_HINT = 0;
+    private static final int IFD_EXIF_HINT = 1;
+    private static final int IFD_GPS_HINT = 2;
+    private static final int IFD_INTEROPERABILITY_HINT = 3;
+    private static final int IFD_THUMBNAIL_HINT = 4;
+    // List of Exif tag groups
+    private static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
+            IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
+            IFD_THUMBNAIL_TAGS
+    };
+    // List of tags for pointing to the other image file directory offset.
+    private static final ExifTag[] IFD_POINTER_TAGS = new ExifTag[] {
+            new ExifTag("ExifIFDPointer", 34665),
+            new ExifTag("GPSInfoPointer", 34853),
+            new ExifTag("InteroperabilityIFDPointer", 40965),
+    };
+    // List of indices of the indicated tag groups according to the IFD_POINTER_TAGS
+    private static final int[] IFD_POINTER_TAG_HINTS = new int[] {
+            IFD_EXIF_HINT, IFD_GPS_HINT, IFD_INTEROPERABILITY_HINT
+    };
+    // Tags for indicating the thumbnail offset and length
+    private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
+            new ExifTag("JPEGInterchangeFormat", 513);
+    private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
+            new ExifTag("JPEGInterchangeFormatLength", 514);
+
+    // Mappings from tag number to tag name and each item represents one IFD tag group.
+    private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length];
+    // Mapping from tag name to tag number and the corresponding tag group.
+    private static final HashMap<String, Pair<Integer, Integer>> sExifTagMapForWriting
+            = new HashMap<>();
+
+    // See JPEG File Interchange Format Version 1.02.
+    // The following values are defined for handling JPEG streams. In this implementation, we are
+    // not only getting information from EXIF but also from some JPEG special segments such as
+    // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+
+    // Identifier for APP1 segment in JPEG
+    private static final byte[] IDENTIFIER_APP1 = "Exif\0\0".getBytes(Charset.forName("US-ASCII"));
+    // JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
+    // the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
+    // of frame(baseline DCT) and the image size info exists in its beginning part.
+    private static final byte MARKER = (byte) 0xff;
+    private static final byte MARKER_SOI = (byte) 0xd8;
+    private static final byte MARKER_SOF0 = (byte) 0xc0;
+    private static final byte MARKER_SOF1 = (byte) 0xc1;
+    private static final byte MARKER_SOF2 = (byte) 0xc2;
+    private static final byte MARKER_SOF3 = (byte) 0xc3;
+    private static final byte MARKER_SOF5 = (byte) 0xc5;
+    private static final byte MARKER_SOF6 = (byte) 0xc6;
+    private static final byte MARKER_SOF7 = (byte) 0xc7;
+    private static final byte MARKER_SOF9 = (byte) 0xc9;
+    private static final byte MARKER_SOF10 = (byte) 0xca;
+    private static final byte MARKER_SOF11 = (byte) 0xcb;
+    private static final byte MARKER_SOF13 = (byte) 0xcd;
+    private static final byte MARKER_SOF14 = (byte) 0xce;
+    private static final byte MARKER_SOF15 = (byte) 0xcf;
+    private static final byte MARKER_SOS = (byte) 0xda;
+    private static final byte MARKER_APP1 = (byte) 0xe1;
+    private static final byte MARKER_COM = (byte) 0xfe;
+    private static final byte MARKER_EOI = (byte) 0xd9;
+
     static {
-        System.loadLibrary("jhead_jni");
         System.loadLibrary("media_jni");
         initRawNative();
-
         sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
         sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        // Build up the hash tables to look up Exif tags for reading Exif tags.
+        for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+            sExifTagMapsForReading[hint] = new HashMap();
+            for (ExifTag tag : EXIF_TAGS[hint]) {
+                sExifTagMapsForReading[hint].put(tag.number, tag.name);
+            }
+        }
+
+        // Build up the hash tables to look up Exif tags for writing Exif tags.
+        // There are some tags that have the same tag name in the different group. For that tags,
+        // Primary image TIFF IFD and Exif private IFD have a higher priority to map than the other
+        // tag groups. For the same tags, it writes one tag in the only one IFD group, which has the
+        // higher priority group.
+        for (int hint = EXIF_TAGS.length - 1; hint >= 0; --hint) {
+            for (ExifTag tag : EXIF_TAGS[hint]) {
+                sExifTagMapForWriting.put(tag.name, new Pair<>(tag.number, hint));
+            }
+        }
     }
 
     private final String mFilename;
-    private final HashMap<String, String> mAttributes = new HashMap<>();
+    private final FileDescriptor mFileDescriptor;
+    private final InputStream mInputStream;
     private boolean mIsRaw;
+    private final HashMap<String, String> mAttributes = new HashMap<>();
     private boolean mHasThumbnail;
     // The following values used for indicating a thumbnail position.
     private int mThumbnailOffset;
     private int mThumbnailLength;
-
-    // Because the underlying implementation (jhead) uses static variables,
-    // there can only be one user at a time for the native functions (and
-    // they cannot keep state in the native code across function calls). We
-    // use sLock to serialize the accesses.
-    private static final Object sLock = new Object();
+    private byte[] mThumbnailBytes;
 
     // Pattern to check non zero timestamp
     private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
@@ -155,7 +489,35 @@
             throw new IllegalArgumentException("filename cannot be null");
         }
         mFilename = filename;
-        // First test whether a given file is a one of RAW format or not.
+        mFileDescriptor = null;
+        mInputStream = new FileInputStream(filename);
+        loadAttributes();
+    }
+
+    /**
+     * Reads Exif tags from the specified image file descriptor.
+     */
+    public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
+        if (fileDescriptor == null) {
+            throw new IllegalArgumentException("parcelFileDescriptor cannot be null");
+        }
+        mFilename = null;
+        mFileDescriptor = fileDescriptor;
+        mInputStream = new FileInputStream(fileDescriptor);
+        loadAttributes();
+    }
+
+    /**
+     * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
+     * for input streams.
+     */
+    public ExifInterface(InputStream inputStream) throws IOException {
+        if (inputStream == null) {
+            throw new IllegalArgumentException("inputStream cannot be null");
+        }
+        mFilename = null;
+        mFileDescriptor = null;
+        mInputStream = inputStream;
         loadAttributes();
     }
 
@@ -188,9 +550,9 @@
     }
 
     /**
-     * Returns the double value of the specified rational tag. If there is no
-     * such tag in the image file or the value cannot be parsed as double, return
-     * <var>defaultValue</var>.
+     * Returns the double value of the tag that is specified as rational or contains a
+     * double-formatted value. If there is no such tag in the image file or the value cannot be
+     * parsed as double, return <var>defaultValue</var>.
      *
      * @param tag the name of the tag.
      * @param defaultValue the value to return if the tag is not available.
@@ -200,7 +562,7 @@
         if (value == null) return defaultValue;
         try {
             int index = value.indexOf("/");
-            if (index == -1) return defaultValue;
+            if (index == -1) return Double.parseDouble(value);
             double denom = Double.parseDouble(value.substring(index + 1));
             if (denom == 0) return defaultValue;
             double num = Double.parseDouble(value.substring(0, index));
@@ -217,6 +579,10 @@
      * @param value the value of the tag.
      */
     public void setAttribute(String tag, String value) {
+        if (value == null) {
+            mAttributes.remove(tag);
+            return;
+        }
         mAttributes.put(tag, value);
     }
 
@@ -231,68 +597,74 @@
      * file has a thumbnail inside.
      */
     private void loadAttributes() throws IOException {
-        HashMap map = getRawAttributesNative(mFilename);
-        mIsRaw = map != null;
-        if (mIsRaw) {
-            for (Object o : map.entrySet()) {
-                Map.Entry entry = (Map.Entry) o;
-                String attrName = (String) entry.getKey();
-                String attrValue = (String) entry.getValue();
+        FileInputStream in = null;
+        try {
+            if (mFilename != null) {
+                in = new FileInputStream(mFilename);
+            }
+            if (mFileDescriptor != null) {
+                in = new FileInputStream(mFileDescriptor);
+            }
+            if (in != null) {
+                // First test whether a given file is a one of RAW format or not.
+                HashMap map = getRawAttributesNative(Os.dup(in.getFD()));
+                mIsRaw = map != null;
+                if (mIsRaw) {
+                    for (Object obj : map.entrySet()) {
+                        Map.Entry entry = (Map.Entry) obj;
+                        String attrName = (String) entry.getKey();
+                        String attrValue = (String) entry.getValue();
 
-                switch (attrName) {
-                    case TAG_HAS_THUMBNAIL:
-                        mHasThumbnail = attrValue.equalsIgnoreCase("true");
-                        break;
-                    case TAG_THUMBNAIL_OFFSET:
-                        mThumbnailOffset = Integer.parseInt(attrValue);
-                        break;
-                    case TAG_THUMBNAIL_LENGTH:
-                        mThumbnailLength = Integer.parseInt(attrValue);
-                        break;
-                    default:
-                        mAttributes.put(attrName, attrValue);
-                        break;
+                        switch (attrName) {
+                            case TAG_HAS_THUMBNAIL:
+                                mHasThumbnail = attrValue.equalsIgnoreCase("true");
+                                break;
+                            case TAG_THUMBNAIL_OFFSET:
+                                mThumbnailOffset = Integer.parseInt(attrValue);
+                                break;
+                            case TAG_THUMBNAIL_LENGTH:
+                                mThumbnailLength = Integer.parseInt(attrValue);
+                                break;
+                            default:
+                                mAttributes.put(attrName, attrValue);
+                                break;
+                        }
+                    }
+
+                    if (DEBUG) {
+                        printAttributes();
+                    }
+                    return;
                 }
             }
-            return;
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        } finally {
+            IoUtils.closeQuietly(in);
         }
 
-        // format of string passed from native C code:
-        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
-        // example:
-        // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
-
-        String attrStr;
-        synchronized (sLock) {
-            attrStr = getAttributesNative(mFilename);
-        }
-
-        // get count
-        int ptr = attrStr.indexOf(' ');
-        int count = Integer.parseInt(attrStr.substring(0, ptr));
-        // skip past the space between item count and the rest of the attributes
-        ++ptr;
-
-        for (int i = 0; i < count; i++) {
-            // extract the attribute name
-            int equalPos = attrStr.indexOf('=', ptr);
-            String attrName = attrStr.substring(ptr, equalPos);
-            ptr = equalPos + 1;     // skip past =
-
-            // extract the attribute value length
-            int lenPos = attrStr.indexOf(' ', ptr);
-            int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
-            ptr = lenPos + 1;       // skip pas the space
-
-            // extract the attribute value
-            String attrValue = attrStr.substring(ptr, ptr + attrLen);
-            ptr += attrLen;
-
-            if (attrName.equals(TAG_HAS_THUMBNAIL)) {
-                mHasThumbnail = attrValue.equalsIgnoreCase("true");
-            } else {
-                mAttributes.put(attrName, attrValue);
+        try {
+            if (mFileDescriptor != null) {
+                Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
             }
+
+            getJpegAttributes(mInputStream);
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        } finally {
+            IoUtils.closeQuietly(mInputStream);
+        }
+
+        if (DEBUG) {
+            printAttributes();
+        }
+    }
+
+    // Prints out attributes for debugging.
+    private void printAttributes() {
+        Log.d(TAG, "The size of tags: " + mAttributes.size());
+        for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+            Log.d(TAG, "tagName: " + entry.getKey() + ", tagValue: " + entry.getValue());
         }
     }
 
@@ -307,33 +679,63 @@
             throw new UnsupportedOperationException(
                     "ExifInterface does not support saving attributes on RAW formats.");
         }
+        if (mFileDescriptor == null && mFilename == null) {
+            throw new UnsupportedOperationException(
+                    "ExifInterface does not support saving attributes for input streams.");
+        }
 
-        // format of string passed to native C code:
-        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
-        // example:
-        // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
-        StringBuilder sb = new StringBuilder();
-        int size = mAttributes.size();
-        if (mAttributes.containsKey(TAG_HAS_THUMBNAIL)) {
-            --size;
-        }
-        sb.append(size).append(" ");
-        for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
-            String key = entry.getKey();
-            if (key.equals(TAG_HAS_THUMBNAIL)) {
-                // this is a fake attribute not saved as an exif tag
-                continue;
+        // Keep the thumbnail in memory
+        mThumbnailBytes = getThumbnail();
+
+        FileInputStream in = null;
+        FileOutputStream out = null;
+        File tempFile = null;
+        try {
+            // Move the original file to temporary file.
+            if (mFilename != null) {
+                tempFile = new File(mFilename + ".tmp");
+                File originalFile = new File(mFilename);
+                if (!originalFile.renameTo(tempFile)) {
+                    throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath());
+                }
             }
-            String val = entry.getValue();
-            sb.append(key).append("=");
-            sb.append(val.length()).append(" ");
-            sb.append(val);
+            if (mFileDescriptor != null) {
+                tempFile = File.createTempFile("temp", "jpg");
+                Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+                in = new FileInputStream(mFileDescriptor);
+                out = new FileOutputStream(tempFile);
+                Streams.copy(in, out);
+            }
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
         }
-        String s = sb.toString();
-        synchronized (sLock) {
-            saveAttributesNative(mFilename, s);
-            commitChangesNative(mFilename);
+
+        in = null;
+        out = null;
+        try {
+            // Save the new file.
+            in = new FileInputStream(tempFile);
+            if (mFilename != null) {
+                out = new FileOutputStream(mFilename);
+            }
+            if (mFileDescriptor != null) {
+                Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+                out = new FileOutputStream(mFileDescriptor);
+            }
+            saveJpegAttributes(in, out);
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+            tempFile.delete();
         }
+
+        // Discard the thumbnail in memory
+        mThumbnailBytes = null;
     }
 
     /**
@@ -349,27 +751,41 @@
      * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
      */
     public byte[] getThumbnail() {
-        if (mIsRaw) {
-            if (mHasThumbnail) {
-                try (RandomAccessFile file = new RandomAccessFile(mFilename, "r")) {
-                    if (file.length() < mThumbnailLength + mThumbnailOffset) {
-                        throw new IOException("Corrupted image.");
-                    }
-                    file.seek(mThumbnailOffset);
-
-                    byte[] buffer = new byte[mThumbnailLength];
-                    file.readFully(buffer);
-                    return buffer;
-                } catch (IOException e) {
-                    // Couldn't get a thumbnail image.
-                }
-            }
+        if (!mHasThumbnail) {
             return null;
         }
-
-        synchronized (sLock) {
-            return getThumbnailNative(mFilename);
+        if (mThumbnailBytes != null) {
+            return mThumbnailBytes;
         }
+
+        // Read the thumbnail.
+        FileInputStream in = null;
+        try  {
+            if (mFileDescriptor != null) {
+                Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+                in = new FileInputStream(mFileDescriptor);
+            }
+            if (mFilename != null) {
+                in = new FileInputStream(mFilename);
+            }
+            if (in == null) {
+                // Should not be reached this.
+                throw new FileNotFoundException();
+            }
+            if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
+                throw new IOException("Corrupted image");
+            }
+            byte[] buffer = new byte[mThumbnailLength];
+            if (in.read(buffer) != mThumbnailLength) {
+                throw new IOException("Corrupted image");
+            }
+            return buffer;
+        } catch (IOException | ErrnoException e) {
+            // Couldn't get a thumbnail image.
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+        return null;
     }
 
     /**
@@ -381,16 +797,10 @@
      * @hide
      */
     public long[] getThumbnailRange() {
-        if (mIsRaw) {
-            long[] range = new long[2];
-            range[0] = mThumbnailOffset;
-            range[1] = mThumbnailLength;
-            return range;
-        }
-
-        synchronized (sLock) {
-            return getThumbnailRangeNative(mFilename);
-        }
+        long[] range = new long[2];
+        range[0] = mThumbnailOffset;
+        range[1] = mThumbnailLength;
+        return range;
     }
 
     /**
@@ -399,10 +809,10 @@
      * Exif tags are not available.
      */
     public boolean getLatLong(float output[]) {
-        String latValue = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE);
-        String latRef = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE_REF);
-        String lngValue = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE);
-        String lngRef = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
+        String latValue = mAttributes.get(TAG_GPS_LATITUDE);
+        String latRef = mAttributes.get(TAG_GPS_LATITUDE_REF);
+        String lngValue = mAttributes.get(TAG_GPS_LONGITUDE);
+        String lngRef = mAttributes.get(TAG_GPS_LONGITUDE_REF);
 
         if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
             try {
@@ -428,7 +838,7 @@
         int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
 
         if (altitude >= 0 && ref >= 0) {
-            return (double) (altitude * ((ref == 1) ? -1 : 1));
+            return (altitude * ((ref == 1) ? -1 : 1));
         } else {
             return defaultValue;
         }
@@ -461,6 +871,7 @@
                     }
                     msecs += sub;
                 } catch (NumberFormatException e) {
+                    // Ignored
                 }
             }
             return msecs;
@@ -493,8 +904,7 @@
         }
     }
 
-    private static float convertRationalLatLonToFloat(
-            String rationalString, String ref) {
+    private static float convertRationalLatLonToFloat(String rationalString, String ref) {
         try {
             String [] parts = rationalString.split(",");
 
@@ -522,22 +932,1062 @@
         }
     }
 
-    // JNI methods for JPEG.
-    private static native boolean appendThumbnailNative(String fileName,
-            String thumbnailFileName);
+    // Loads EXIF attributes from a JPEG input stream.
+    private void getJpegAttributes(InputStream inputStream) throws IOException {
+        // See JPEG File Interchange Format Specification page 5.
+        if (DEBUG) {
+            Log.d(TAG, "getJpegAttributes starting with: " + inputStream);
+        }
+        DataInputStream dataInputStream = new DataInputStream(inputStream);
+        byte marker;
+        int bytesRead = 0;
+        ++bytesRead;
+        if ((marker = dataInputStream.readByte()) != MARKER) {
+            throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+        }
+        ++bytesRead;
+        if (dataInputStream.readByte() != MARKER_SOI) {
+            throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+        }
+        while (true) {
+            ++bytesRead;
+            marker = dataInputStream.readByte();
+            if (marker != MARKER) {
+                throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
+            }
+            ++bytesRead;
+            marker = dataInputStream.readByte();
+            if (DEBUG) {
+                Log.d(TAG, "Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
+            }
 
-    private static native void saveAttributesNative(String fileName,
-            String compressedAttributes);
+            // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
+            // the image data will terminate right after.
+            if (marker == MARKER_EOI || marker == MARKER_SOS) {
+                break;
+            }
+            bytesRead += 2;
+            int length = dataInputStream.readUnsignedShort() - 2;
+            if (length < 0)
+                throw new IOException("Invalid length");
+            bytesRead += length;
+            switch (marker) {
+                case MARKER_APP1: {
+                    if (DEBUG) {
+                        Log.d(TAG, "MARKER_APP1");
+                    }
+                    bytesRead -= length;
+                    if (length < 6) {
+                        throw new IOException("Invalid exif");
+                    }
+                    byte[] identifier = new byte[6];
+                    if (inputStream.read(identifier) != 6) {
+                        throw new IOException("Invalid exif");
+                    }
+                    if (!Arrays.equals(identifier, IDENTIFIER_APP1)) {
+                        throw new IOException("Invalid app1 identifier");
+                    }
+                    bytesRead += 6;
+                    length -= 6;
+                    if (length <= 0) {
+                        throw new IOException("Invalid exif");
+                    }
+                    byte[] bytes = new byte[length];
+                    if (dataInputStream.read(bytes) != length) {
+                        throw new IOException("Invalid exif");
+                    }
+                    readExifSegment(bytes, bytesRead);
+                    bytesRead += length;
+                    length = 0;
+                    break;
+                }
 
-    private static native String getAttributesNative(String fileName);
+                case MARKER_COM: {
+                    byte[] bytes = new byte[length];
+                    if (dataInputStream.read(bytes) != length) {
+                        throw new IOException("Invalid exif");
+                    }
+                    mAttributes.put("UserComment",
+                            new String(bytes, Charset.forName("US-ASCII")));
+                    break;
+                }
 
-    private static native void commitChangesNative(String fileName);
+                case MARKER_SOF0:
+                case MARKER_SOF1:
+                case MARKER_SOF2:
+                case MARKER_SOF3:
+                case MARKER_SOF5:
+                case MARKER_SOF6:
+                case MARKER_SOF7:
+                case MARKER_SOF9:
+                case MARKER_SOF10:
+                case MARKER_SOF11:
+                case MARKER_SOF13:
+                case MARKER_SOF14:
+                case MARKER_SOF15: {
+                    dataInputStream.skipBytes(1);
+                    mAttributes.put("ImageLength",
+                            String.valueOf(dataInputStream.readUnsignedShort()));
+                    mAttributes.put("ImageWidth",
+                            String.valueOf(dataInputStream.readUnsignedShort()));
+                    length -= 5;
+                    break;
+                }
 
-    private static native byte[] getThumbnailNative(String fileName);
+                default: {
+                    break;
+                }
+            }
+            if (length < 0) {
+                throw new IOException("Invalid length");
+            }
+            dataInputStream.skipBytes(length);
+        }
+    }
 
-    private static native long[] getThumbnailRangeNative(String fileName);
+    // Stores a new JPEG image with EXIF attributes into a given output stream.
+    private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
+            throws IOException {
+        // See JPEG File Interchange Format Specification page 5.
+        if (DEBUG) {
+            Log.d(TAG, "saveJpegAttributes starting with (inputStream: " + inputStream
+                    + ", outputStream: " + outputStream + ")");
+        }
+        DataInputStream dataInputStream = new DataInputStream(inputStream);
+        ExifDataOutputStream dataOutputStream = new ExifDataOutputStream(outputStream);
+        int bytesRead = 0;
+        ++bytesRead;
+        if (dataInputStream.readByte() != MARKER) {
+            throw new IOException("Invalid marker");
+        }
+        dataOutputStream.writeByte(MARKER);
+        ++bytesRead;
+        if (dataInputStream.readByte() != MARKER_SOI) {
+            throw new IOException("Invalid marker");
+        }
+        dataOutputStream.writeByte(MARKER_SOI);
+
+        byte[] bytes = new byte[4096];
+
+        while (true) {
+            ++bytesRead;
+            if (dataInputStream.readByte() != MARKER) {
+                throw new IOException("Invalid marker");
+            }
+            dataOutputStream.writeByte(MARKER);
+            ++bytesRead;
+            byte marker = dataInputStream.readByte();
+            dataOutputStream.writeByte(marker);
+            switch (marker) {
+                case MARKER_APP1: {
+                    // Rewrite EXIF segment
+                    int length = dataInputStream.readUnsignedShort() - 2;
+                    if (length < 0)
+                        throw new IOException("Invalid length");
+                    bytesRead += 2;
+                    int read;
+                    while ((read = dataInputStream.read(
+                            bytes, 0, Math.min(length, bytes.length))) > 0) {
+                        length -= read;
+                    }
+                    bytesRead += length;
+                    writeExifSegment(dataOutputStream, bytesRead);
+                    break;
+                }
+                case MARKER_EOI:
+                case MARKER_SOS: {
+                    // Copy all the remaining data
+                    Streams.copy(dataInputStream, dataOutputStream);
+                    return;
+                }
+                default: {
+                    // Copy JPEG segment
+                    int length = dataInputStream.readUnsignedShort();
+                    dataOutputStream.writeUnsignedShort(length);
+                    if (length < 0)
+                        throw new IOException("Invalid length");
+                    length -= 2;
+                    bytesRead += 2;
+                    int read;
+                    while ((read = dataInputStream.read(
+                            bytes, 0, Math.min(length, bytes.length))) > 0) {
+                        dataOutputStream.write(bytes, 0, read);
+                        length -= read;
+                    }
+                    bytesRead += length;
+                    break;
+                }
+            }
+        }
+    }
+
+    // Reads the given EXIF byte area and save its tag data into attributes.
+    private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning) throws IOException {
+        // Parse TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+        ByteOrderAwarenessDataInputStream dataInputStream =
+                new ByteOrderAwarenessDataInputStream(exifBytes);
+
+        // Read byte align
+        short byteOrder = dataInputStream.readShort();
+        switch (byteOrder) {
+            case BYTE_ALIGN_II:
+                if (DEBUG) {
+                    Log.d(TAG, "readExifSegment: Byte Align II");
+                }
+                dataInputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+                break;
+            case BYTE_ALIGN_MM:
+                if (DEBUG) {
+                    Log.d(TAG, "readExifSegment: Byte Align MM");
+                }
+                dataInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+                break;
+            default:
+                throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
+        }
+
+        int startCode = dataInputStream.readUnsignedShort();
+        if (startCode != 0x2a) {
+            throw new IOException("Invalid exif start: " + Integer.toHexString(startCode));
+        }
+
+        // Read first ifd offset
+        long firstIfdOffset = dataInputStream.readUnsignedInt();
+        if (firstIfdOffset < 8 || firstIfdOffset >= exifBytes.length) {
+            throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
+        }
+        firstIfdOffset -= 8;
+        if (firstIfdOffset > 0) {
+            if (dataInputStream.skip(firstIfdOffset) != firstIfdOffset)
+                throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset);
+        }
+
+        // Read primary image TIFF image file directory.
+        readImageFileDirectory(dataInputStream, IFD_TIFF_HINT);
+
+        // Process thumbnail.
+        try {
+            int jpegInterchangeFormat = Integer.parseInt(
+                    mAttributes.get(JPEG_INTERCHANGE_FORMAT_TAG.name));
+            int jpegInterchangeFormatLength = Integer.parseInt(
+                    mAttributes.get(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name));
+            // The following code limits the size of thumbnail size not to overflow EXIF data area.
+            jpegInterchangeFormatLength = Math.min(jpegInterchangeFormat
+                    + jpegInterchangeFormatLength, exifOffsetFromBeginning + exifBytes.length)
+                    - jpegInterchangeFormat;
+            if (jpegInterchangeFormat > 0 && jpegInterchangeFormatLength > 0) {
+                mHasThumbnail = true;
+                mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat;
+                mThumbnailLength = jpegInterchangeFormatLength;
+
+                // Do not store a thumbnail in memory if the given input can be re-read.
+                if (mFileDescriptor == null && mFilename == null) {
+                    byte[] thumbnailBytes = new byte[jpegInterchangeFormatLength];
+                    dataInputStream.seek(jpegInterchangeFormat);
+                    dataInputStream.readFully(thumbnailBytes);
+                    mThumbnailBytes = thumbnailBytes;
+
+                    if (DEBUG) {
+                        Bitmap bitmap = BitmapFactory.decodeByteArray(
+                                thumbnailBytes, 0, thumbnailBytes.length);
+                        Log.d(TAG, "Thumbnail offset: " + mThumbnailOffset + ", length: "
+                                + mThumbnailLength + ", width: " + bitmap.getWidth() + ", height: "
+                                + bitmap.getHeight());
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            // Ignored the corrupted image.
+        }
+
+        // For compatibility, keep data formats as follows.
+        convertToInt(TAG_IMAGE_WIDTH);
+        convertToInt(TAG_IMAGE_LENGTH);
+        convertToInt(TAG_ORIENTATION);
+        convertToInt(TAG_FLASH);
+        convertToRational(TAG_FOCAL_LENGTH);
+        convertToDouble(TAG_DIGITAL_ZOOM_RATIO);
+        convertToDouble(TAG_EXPOSURE_TIME);
+        convertToDouble(TAG_APERTURE);
+        convertToDouble(TAG_SUBJECT_DISTANCE);
+        convertToInt(TAG_ISO);
+        convertToDouble(TAG_EXPOSURE_BIAS_VALUE);
+        convertToInt(TAG_WHITE_BALANCE);
+        convertToInt(TAG_LIGHT_SOURCE);
+        convertToInt(TAG_METERING_MODE);
+        convertToInt(TAG_EXPOSURE_PROGRAM);
+        convertToInt(TAG_EXPOSURE_MODE);
+        convertToRational(TAG_GPS_ALTITUDE);
+        convertToInt(TAG_GPS_ALTITUDE_REF);
+        convertToRational(TAG_GPS_LONGITUDE);
+        convertToRational(TAG_GPS_LATITUDE);
+        convertToTimetamp(TAG_GPS_TIMESTAMP);
+
+        // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag.
+        String valueOfDateTimeOriginal = mAttributes.get("DateTimeOriginal");
+        if (valueOfDateTimeOriginal != null) {
+            mAttributes.put(TAG_DATETIME, valueOfDateTimeOriginal);
+        }
+
+        // Add the default value.
+        if (!mAttributes.containsKey(TAG_IMAGE_WIDTH)) {
+            mAttributes.put(TAG_IMAGE_WIDTH, "0");
+        }
+        if (!mAttributes.containsKey(TAG_IMAGE_LENGTH)) {
+            mAttributes.put(TAG_IMAGE_LENGTH, "0");
+        }
+        if (!mAttributes.containsKey(TAG_ORIENTATION)) {
+            mAttributes.put(TAG_ORIENTATION, "0");
+        }
+        if (!mAttributes.containsKey(TAG_LIGHT_SOURCE)) {
+            mAttributes.put(TAG_LIGHT_SOURCE, "0");
+        }
+    }
+
+    // Converts the tag value to timestamp; Otherwise deletes the given tag.
+    private void convertToTimetamp(String tagName) {
+        String entryValue = mAttributes.get(tagName);
+        if (entryValue == null) return;
+        int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+        String[] components = entryValue.split(",");
+        if (dataFormat == IFD_FORMAT_SRATIONAL && components.length == 3) {
+            StringBuilder stringBuilder = new StringBuilder();
+            for (String component : components) {
+                if (stringBuilder.length() > 0) {
+                    stringBuilder.append(":");
+                }
+                String[] rationalNumber = component.split("/");
+                int numerator = Integer.parseInt(rationalNumber[0]);
+                int denominator = Integer.parseInt(rationalNumber[1]);
+                if (denominator == 0) {
+                    numerator = 0;
+                    denominator = 1;
+                }
+                int value = numerator / denominator;
+                stringBuilder.append(String.format("%02d", value));
+            }
+            mAttributes.put(tagName, stringBuilder.toString());
+        } else if (dataFormat != IFD_FORMAT_STRING) {
+            mAttributes.remove(tagName);
+        }
+    }
+
+    // Checks the tag value of a given tag formatted in double type; Otherwise try to convert it to
+    // double type or delete it.
+    private void convertToDouble(String tagName) {
+        String entryValue = mAttributes.get(tagName);
+        if (entryValue == null) return;
+        int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+        switch (dataFormat) {
+            case IFD_FORMAT_SRATIONAL: {
+                StringBuilder stringBuilder = new StringBuilder();
+                String[] components = entryValue.split(",");
+                for (String component : components) {
+                    if (stringBuilder.length() > 0) {
+                        stringBuilder.append(",");
+                    }
+                    String[] rationalNumber = component.split("/");
+                    int numerator = Integer.parseInt(rationalNumber[0]);
+                    int denominator = Integer.parseInt(rationalNumber[1]);
+                    if (denominator == 0) {
+                        numerator = 0;
+                        denominator = 1;
+                    }
+                    stringBuilder.append((double) numerator / denominator);
+                }
+                mAttributes.put(tagName, stringBuilder.toString());
+                break;
+            }
+            case IFD_FORMAT_DOUBLE:
+                // Keep it as is.
+                break;
+            default:
+                mAttributes.remove(tagName);
+                break;
+        }
+    }
+
+    // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+    private void convertToRational(String tagName) {
+        String entryValue = mAttributes.get(tagName);
+        if (entryValue == null) return;
+        int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+        switch (dataFormat) {
+            case IFD_FORMAT_SLONG:
+            case IFD_FORMAT_DOUBLE: {
+                StringBuilder stringBuilder = new StringBuilder();
+                String[] components = entryValue.split(",");
+                for (String component : components) {
+                    if (stringBuilder.length() > 0) {
+                        stringBuilder.append(",");
+                    }
+                    double doubleValue = Double.parseDouble(component);
+                    stringBuilder.append((int) (doubleValue * 10000.0)).append("/").append(10000);
+                }
+                mAttributes.put(tagName, stringBuilder.toString());
+                break;
+            }
+            case IFD_FORMAT_SRATIONAL:
+                // Keep it as is.
+                break;
+            default:
+                mAttributes.remove(tagName);
+                break;
+        }
+    }
+
+    // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+    private void convertToInt(String tagName) {
+        String entryValue = mAttributes.get(tagName);
+        if (entryValue == null) return;
+        int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+        if (dataFormat != IFD_FORMAT_SLONG) {
+            mAttributes.remove(tagName);
+        }
+    }
+
+    // Reads image file directory, which is a tag group in EXIF.
+    private void readImageFileDirectory(ByteOrderAwarenessDataInputStream dataInputStream, int hint)
+            throws IOException {
+        // See JEITA CP-3451 Figure 5. page 9.
+        short numberOfDirectoryEntry = dataInputStream.readShort();
+
+        if (DEBUG) {
+            Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry);
+        }
+
+        for (short i = 0; i < numberOfDirectoryEntry; ++i) {
+            int tagNumber = dataInputStream.readUnsignedShort();
+            int dataFormat = dataInputStream.readUnsignedShort();
+            int numberOfComponents = dataInputStream.readInt();
+            long nextEntryOffset = dataInputStream.peek() + 4;  // next four bytes is for data
+                                                                // offset or value.
+
+            if (DEBUG) {
+                Log.d(TAG, String.format("tagNumber: %d, dataFormat: %d, numberOfComponents: %d",
+                        tagNumber, dataFormat, numberOfComponents));
+            }
+
+            // Read a value from data field or seek to the value offset which is stored in data
+            // field if the size of the entry value is bigger than 4.
+            int byteCount = numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+            if (byteCount > 4) {
+                long offset = dataInputStream.readUnsignedInt();
+                if (DEBUG) {
+                    Log.d(TAG, "seek to data offset: " + offset);
+                }
+                dataInputStream.seek(offset);
+            }
+
+            // Look up a corresponding tag from tag number
+            String tagName = (String) sExifTagMapsForReading[hint].get(tagNumber);
+            // Skip if the parsed tag number is not defined.
+            if (tagName == null) {
+                dataInputStream.seek(nextEntryOffset);
+                continue;
+            }
+
+            // Recursively parse IFD when a IFD pointer tag appears.
+            int innerIfdHint = getIfdHintFromTagNumber(tagNumber);
+            if (innerIfdHint >= 0) {
+                long offset = -1L;
+                // Get offset from data field
+                switch (dataFormat) {
+                    case IFD_FORMAT_USHORT: {
+                        offset = dataInputStream.readUnsignedShort();
+                        break;
+                    }
+                    case IFD_FORMAT_SSHORT: {
+                        offset = dataInputStream.readShort();
+                        break;
+                    }
+                    case IFD_FORMAT_ULONG: {
+                        offset = dataInputStream.readUnsignedInt();
+                        break;
+                    }
+                    case IFD_FORMAT_SLONG: {
+                        offset = dataInputStream.readInt();
+                        break;
+                    }
+                    default: {
+                        // Nothing to do
+                        break;
+                    }
+                }
+                if (DEBUG) {
+                    Log.d(TAG, String.format("Offset: %d, tagName: %s", offset, tagName));
+                }
+                if (offset > 0L) {
+                    dataInputStream.seek(offset);
+                    readImageFileDirectory(dataInputStream, innerIfdHint);
+                }
+
+                dataInputStream.seek(nextEntryOffset);
+                continue;
+            }
+
+            if (numberOfComponents == 1 || dataFormat == IFD_FORMAT_STRING
+                    || dataFormat == IFD_FORMAT_UNDEFINED) {
+                String entryValue = readExifEntryValue(
+                        dataInputStream, dataFormat, numberOfComponents);
+                if (entryValue != null) {
+                    mAttributes.put(tagName, entryValue);
+                }
+            } else {
+                StringBuilder entryValueBuilder = new StringBuilder();
+                for (int c = 0; c < numberOfComponents; ++c) {
+                    if (entryValueBuilder.length() > 0) {
+                        entryValueBuilder.append(",");
+                    }
+                    entryValueBuilder.append(readExifEntryValue(
+                            dataInputStream, dataFormat, numberOfComponents));
+                }
+                mAttributes.put(tagName, entryValueBuilder.toString());
+            }
+
+            if (dataInputStream.peek() != nextEntryOffset) {
+                dataInputStream.seek(nextEntryOffset);
+            }
+        }
+
+        long nextIfdOffset = dataInputStream.readUnsignedInt();
+        if (DEBUG) {
+            Log.d(TAG, String.format("nextIfdOffset: %d", nextIfdOffset));
+        }
+        // The next IFD offset needs to be bigger than 8 since the first IFD offset is at least 8.
+        if (nextIfdOffset > 8) {
+            dataInputStream.seek(nextIfdOffset);
+            readImageFileDirectory(dataInputStream, IFD_THUMBNAIL_HINT);
+        }
+    }
+
+    // Reads a value from where the entry value are stored.
+    private String readExifEntryValue(ByteOrderAwarenessDataInputStream dataInputStream,
+            int dataFormat, int numberOfComponents) throws IOException {
+        // See TIFF 6.0 spec Types. page 15.
+        switch (dataFormat) {
+            case IFD_FORMAT_BYTE: {
+                return String.valueOf(dataInputStream.readByte());
+            }
+            case IFD_FORMAT_SBYTE: {
+                return String.valueOf(dataInputStream.readByte() & 0xff);
+            }
+            case IFD_FORMAT_USHORT: {
+                return String.valueOf(dataInputStream.readUnsignedShort());
+            }
+            case IFD_FORMAT_SSHORT: {
+                return String.valueOf(dataInputStream.readUnsignedInt());
+            }
+            case IFD_FORMAT_ULONG: {
+                return String.valueOf(dataInputStream.readInt());
+            }
+            case IFD_FORMAT_SLONG: {
+                return String.valueOf(dataInputStream.readInt());
+            }
+            case IFD_FORMAT_URATIONAL:
+            case IFD_FORMAT_SRATIONAL: {
+                int numerator = dataInputStream.readInt();
+                int denominator = dataInputStream.readInt();
+                return numerator + "/" + denominator;
+            }
+            case IFD_FORMAT_SINGLE: {
+                return String.valueOf(dataInputStream.readFloat());
+            }
+            case IFD_FORMAT_DOUBLE: {
+                return String.valueOf(dataInputStream.readDouble());
+            }
+            case IFD_FORMAT_UNDEFINED:  // Usually UNDEFINED format is ASCII.
+            case IFD_FORMAT_STRING: {
+                byte[] bytes = new byte[numberOfComponents];
+                dataInputStream.readFully(bytes);
+                int index = 0;
+                if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
+                    boolean same = true;
+                    for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
+                        if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
+                            same = false;
+                            break;
+                        }
+                    }
+                    if (same) {
+                        index = EXIF_ASCII_PREFIX.length;
+                    }
+                }
+
+                StringBuilder stringBuilder = new StringBuilder();
+                while (true) {
+                    int ch = bytes[index];
+                    if (ch < 0)
+                        throw new EOFException();
+                    if (ch == 0)
+                        break;
+                    if (ch >= 32)
+                        stringBuilder.append((char) ch);
+                    else
+                        stringBuilder.append('?');
+                    ++index;
+                    if (index == numberOfComponents)
+                        break;
+                }
+                return stringBuilder.toString();
+            }
+            default: {
+                // Nothing to do
+                return null;
+            }
+        }
+    }
+
+    // Gets the corresponding IFD group index of the given tag number for writing Exif Tags.
+    private static int getIfdHintFromTagNumber(int tagNumber) {
+        for (int i = 0; i < IFD_POINTER_TAG_HINTS.length; ++i) {
+            if (IFD_POINTER_TAGS[i].number == tagNumber)
+                return IFD_POINTER_TAG_HINTS[i];
+        }
+        return -1;
+    }
+
+    // Writes an Exif segment into the given output stream.
+    private int writeExifSegment(ExifDataOutputStream dataOutputStream, int exifOffsetFromBeginning)
+            throws IOException {
+        // The following variables are for calculating each IFD tag group size in bytes.
+        int[] ifdOffsets = new int[EXIF_TAGS.length];
+        int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+        // Maps to store tags per IFD tag group
+        HashMap[] ifdTags = new HashMap[EXIF_TAGS.length];
+        for (int i = 0; i < EXIF_TAGS.length; ++i) {
+            ifdTags[i] = new HashMap();
+        }
+
+        // Remove IFD pointer tags (we'll re-add it later.)
+        for (ExifTag tag : IFD_POINTER_TAGS) {
+            mAttributes.remove(tag.name);
+        }
+
+        // Assign tags to the corresponding group
+        for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+            Pair<Integer, Integer> pair = sExifTagMapForWriting.get(entry.getKey());
+            if (pair != null) {
+                int tagNumber = pair.first;
+                int hint = pair.second;
+                ifdTags[hint].put(tagNumber, entry.getValue());
+            }
+        }
+
+        // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+        // offset when there is one or more tags in the thumbnail IFD.
+        if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+            ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number, "0");
+        }
+        if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+            ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number, "0");
+        }
+        if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+            ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number, "0");
+        }
+        if (mHasThumbnail) {
+            ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number, "0");
+            ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+                    String.valueOf(mThumbnailLength));
+        }
+
+        // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+        // value which has a bigger size than 4 bytes.
+        for (int i = 0; i < 5; ++i) {
+            int sum = 0;
+            for (Object entry : ifdTags[i].entrySet()) {
+                String entryValue = (String) ((Map.Entry) entry).getValue();
+                int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+                int size = getSizeOfExifEntryValue(dataFormat, entryValue);
+                if (size > 4) {
+                    sum += size;
+                }
+            }
+            ifdDataSizes[i] += sum;
+        }
+
+        // Calculate IFD offsets.
+        int position = 8;
+        for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+            if (!ifdTags[hint].isEmpty()) {
+                ifdOffsets[hint] = position;
+                position += 2 + ifdTags[hint].size() * 12 + 4 + ifdDataSizes[hint];
+            }
+        }
+        if (mHasThumbnail) {
+            int thumbnailOffset = position;
+            ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number,
+                    String.valueOf(thumbnailOffset));
+            ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+                    String.valueOf(mThumbnailLength));
+            mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
+            position += mThumbnailLength;
+        }
+
+        // Calculate the total size
+        int totalSize = position + 8;  // eight bytes is for header part.
+        if (DEBUG) {
+            Log.d(TAG, "totalSize length: " + totalSize);
+            for (int i = 0; i < 5; ++i) {
+                Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
+                        i, ifdOffsets[i], ifdTags[i].size(), ifdDataSizes[i]));
+            }
+        }
+
+        // Update IFD pointer tags with the calculated offsets.
+        if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+            ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number,
+                    String.valueOf(ifdOffsets[IFD_EXIF_HINT]));
+        }
+        if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+            ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number,
+                    String.valueOf(ifdOffsets[IFD_GPS_HINT]));
+        }
+        if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+            ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number,
+                    String.valueOf(ifdOffsets[IFD_INTEROPERABILITY_HINT]));
+        }
+
+        // Write TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+        dataOutputStream.writeUnsignedShort(totalSize);
+        dataOutputStream.write(IDENTIFIER_APP1);
+        dataOutputStream.writeShort(BYTE_ALIGN_MM);
+        dataOutputStream.writeUnsignedShort(0x2a);
+        dataOutputStream.writeUnsignedInt(8);
+
+        // Write IFD groups. See JEITA CP-3451C Figure 7. page 12.
+        for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+            if (!ifdTags[hint].isEmpty()) {
+                // See JEITA CP-3451C 4.6.2 IFD structure. page 13.
+                // Write entry count
+                dataOutputStream.writeUnsignedShort(ifdTags[hint].size());
+
+                // Write entry info
+                int dataOffset = ifdOffsets[hint] + 2 + ifdTags[hint].size() * 12 + 4;
+                for (Object obj : ifdTags[hint].entrySet()) {
+                    Map.Entry entry = (Map.Entry) obj;
+                    int tagNumber = (int) entry.getKey();
+                    String entryValue = (String) entry.getValue();
+
+                    int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+                    int numberOfComponents = getNumberOfComponentsInExifEntryValue(dataFormat,
+                            entryValue);
+                    int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+
+                    dataOutputStream.writeUnsignedShort(tagNumber);
+                    dataOutputStream.writeUnsignedShort(dataFormat);
+                    dataOutputStream.writeInt(numberOfComponents);
+                    if (byteCount > 4) {
+                        dataOutputStream.writeUnsignedInt(dataOffset);
+                        dataOffset += byteCount;
+                    } else {
+                        int bytesWritten = writeExifEntryValue(dataOutputStream, entryValue);
+                        // Fill zero up to 4 bytes
+                        if (bytesWritten < 4) {
+                            for (int i = bytesWritten; i < 4; ++i) {
+                                dataOutputStream.write(0);
+                            }
+                        }
+                    }
+                }
+
+                // Write the next offset. It writes the offset of thumbnail IFD if there is one or
+                // more tags in the thumbnail IFD when the current IFD is the primary image TIFF
+                // IFD; Otherwise 0.
+                if (hint == 0 && !ifdTags[IFD_THUMBNAIL_HINT].isEmpty()) {
+                    dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_THUMBNAIL_HINT]);
+                } else {
+                    dataOutputStream.writeUnsignedInt(0);
+                }
+
+                // Write values of data field exceeding 4 bytes after the next offset.
+                for (Object obj : ifdTags[hint].entrySet()) {
+                    Map.Entry entry = (Map.Entry) obj;
+                    String entryValue = (String) entry.getValue();
+
+                    int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+                    int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+                    if (byteCount > 4) {
+                        writeExifEntryValue(dataOutputStream, entryValue);
+                    }
+                }
+            }
+        }
+
+        // Write thumbnail
+        if (mHasThumbnail) {
+            dataOutputStream.write(getThumbnail());
+        }
+
+        return totalSize;
+    }
+
+    // Writes EXIF entry value and its entry value type will be automatically determined.
+    private static int writeExifEntryValue(ExifDataOutputStream dataOutputStream, String entryValue)
+            throws IOException {
+        int bytesWritten = 0;
+        int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+
+        // Values can be composed of several components. Each component is separated by char ','.
+        String[] components = entryValue.split(",");
+        for (String component : components) {
+            switch (dataFormat) {
+                case IFD_FORMAT_SLONG:
+                    dataOutputStream.writeInt(Integer.parseInt(component));
+                    bytesWritten += 4;
+                    break;
+                case IFD_FORMAT_DOUBLE:
+                    dataOutputStream.writeDouble(Double.parseDouble(component));
+                    bytesWritten += 8;
+                    break;
+                case IFD_FORMAT_STRING:
+                    byte[] asciiArray = (component + '\0').getBytes(Charset.forName("US-ASCII"));
+                    dataOutputStream.write(asciiArray);
+                    bytesWritten += asciiArray.length;
+                    break;
+                case IFD_FORMAT_SRATIONAL:
+                    String[] rationalNumber = component.split("/");
+                    dataOutputStream.writeInt(Integer.parseInt(rationalNumber[0]));
+                    dataOutputStream.writeInt(Integer.parseInt(rationalNumber[1]));
+                    bytesWritten += 8;
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+        return bytesWritten;
+    }
+
+    // Determines the data format of EXIF entry value.
+    private static int getDataFormatOfExifEntryValue(String entryValue) {
+        // See TIFF 6.0 spec Types. page 15.
+        // Take the first component if there are more than one component.
+        if (entryValue.contains(",")) {
+            entryValue = entryValue.split(",")[0];
+        }
+
+        if (entryValue.contains("/")) {
+            return IFD_FORMAT_SRATIONAL;
+        }
+        try {
+            Integer.parseInt(entryValue);
+            return IFD_FORMAT_SLONG;
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        try {
+            Double.parseDouble(entryValue);
+            return IFD_FORMAT_DOUBLE;
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        return IFD_FORMAT_STRING;
+    }
+
+    // Determines the size of EXIF entry value.
+    private static int getSizeOfExifEntryValue(int dataFormat, String entryValue) {
+        // See TIFF 6.0 spec Types page 15.
+        int bytesEstimated = 0;
+        String[] components = entryValue.split(",");
+        for (String component : components) {
+            switch (dataFormat) {
+                case IFD_FORMAT_SLONG:
+                    bytesEstimated += 4;
+                    break;
+                case IFD_FORMAT_DOUBLE:
+                    bytesEstimated += 8;
+                    break;
+                case IFD_FORMAT_STRING:
+                    bytesEstimated
+                            += (component + '\0').getBytes(Charset.forName("US-ASCII")).length;
+                    break;
+                case IFD_FORMAT_SRATIONAL:
+                    bytesEstimated += 8;
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+        return bytesEstimated;
+    }
+
+    // Determines the number of components of EXIF entry value.
+    private static int getNumberOfComponentsInExifEntryValue(int dataFormat, String entryValue) {
+        if (dataFormat == IFD_FORMAT_STRING) {
+            return (entryValue + '\0').getBytes(Charset.forName("US-ASCII")).length;
+        }
+        int count = 1;
+        for (int i = 0; i < entryValue.length(); ++i) {
+            if (entryValue.charAt(i) == ',') {
+                ++count;
+            }
+        }
+        return count;
+    }
+
+    // An input stream to parse EXIF data area, which can be written in either little or big endian
+    // order.
+    private static class ByteOrderAwarenessDataInputStream extends ByteArrayInputStream {
+        private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
+        private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
+
+        private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+        private final long mLength;
+        private long mPosition;
+
+        public ByteOrderAwarenessDataInputStream(byte[] bytes) {
+            super(bytes);
+            mLength = bytes.length;
+            mPosition = 0L;
+        }
+
+        public void setByteOrder(ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+        }
+
+        public void seek(long byteCount) throws IOException {
+            mPosition = 0L;
+            reset();
+            if (skip(byteCount) != byteCount)
+                throw new IOException("Couldn't seek up to the byteCount");
+        }
+
+        public long peek() {
+            return mPosition;
+        }
+
+        public void readFully(byte[] buffer) throws IOException {
+            mPosition += buffer.length;
+            if (mPosition > mLength)
+                throw new EOFException();
+            if (super.read(buffer, 0, buffer.length) != buffer.length) {
+                throw new IOException("Couldn't read up to the length of buffer");
+            }
+        }
+
+        public byte readByte() throws IOException {
+            ++mPosition;
+            if (mPosition > mLength)
+                throw new EOFException();
+            int ch = super.read();
+            if (ch < 0)
+                throw new EOFException();
+            return (byte) ch;
+        }
+
+        public short readShort() throws IOException {
+            mPosition += 2;
+            if (mPosition > mLength)
+                throw new EOFException();
+            int ch1 = super.read();
+            int ch2 = super.read();
+            if ((ch1 | ch2) < 0)
+                throw new EOFException();
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return (short) ((ch2 << 8) + (ch1));
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return (short) ((ch1 << 8) + (ch2));
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        public int readInt() throws IOException {
+            mPosition += 4;
+            if (mPosition > mLength)
+                throw new EOFException();
+            int ch1 = super.read();
+            int ch2 = super.read();
+            int ch3 = super.read();
+            int ch4 = super.read();
+            if ((ch1 | ch2 | ch3 | ch4) < 0)
+                throw new EOFException();
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        @Override
+        public long skip(long byteCount) {
+            long skipped = super.skip(Math.min(byteCount, mLength - mPosition));
+            mPosition += skipped;
+            return skipped;
+        }
+
+        public int readUnsignedShort() throws IOException {
+            mPosition += 2;
+            if (mPosition > mLength)
+                throw new EOFException();
+            int ch1 = super.read();
+            int ch2 = super.read();
+            if ((ch1 | ch2) < 0)
+                throw new EOFException();
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return ((ch2 << 8) + (ch1));
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return ((ch1 << 8) + (ch2));
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        public long readUnsignedInt() throws IOException {
+            return readInt() & 0xffffffffL;
+        }
+
+        public long readLong() throws IOException {
+            mPosition += 8;
+            if (mPosition > mLength)
+                throw new EOFException();
+            int ch1 = super.read();
+            int ch2 = super.read();
+            int ch3 = super.read();
+            int ch4 = super.read();
+            int ch5 = super.read();
+            int ch6 = super.read();
+            int ch7 = super.read();
+            int ch8 = super.read();
+            if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0)
+                throw new EOFException();
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+                        + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+                        + ((long) ch2 << 8) + (long) ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+                        + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+                        + ((long) ch7 << 8) + (long) ch8);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        public float readFloat() throws IOException {
+            return Float.intBitsToFloat(readInt());
+        }
+
+        public double readDouble() throws IOException {
+            return Double.longBitsToDouble(readLong());
+        }
+    }
+
+    // An output stream to write EXIF data area, that will be written in big endian byte order.
+    private static class ExifDataOutputStream extends DataOutputStream {
+        public ExifDataOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        public void writeUnsignedShort(int val) throws IOException {
+            writeShort((short) val);
+        }
+
+        public void writeUnsignedInt(long val) throws IOException {
+            writeInt((int) val);
+        }
+    }
 
     // JNI methods for RAW formats.
     private static native void initRawNative();
-    private static native HashMap getRawAttributesNative(String filename);
+    private static native HashMap getRawAttributesNative(FileDescriptor fileDescriptor);
 }
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index a326f6f..fa9c48c 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -43,14 +43,10 @@
     libcamera_client \
     libmtp \
     libusbhost \
-    libjhead \
     libexif \
     libpiex \
     libstagefright_amrnb_common
 
-LOCAL_REQUIRED_MODULES := \
-    libjhead_jni
-
 LOCAL_STATIC_LIBRARIES := \
     libstagefright_amrnbenc
 
diff --git a/media/jni/android_media_ExifInterface.cpp b/media/jni/android_media_ExifInterface.cpp
index ba38569..a1fcb07 100644
--- a/media/jni/android_media_ExifInterface.cpp
+++ b/media/jni/android_media_ExifInterface.cpp
@@ -83,19 +83,18 @@
 }
 
 static jobject ExifInterface_getRawMetadata(
-        JNIEnv* env, jclass /* clazz */, jstring jfilename) {
-    const char* filenameChars = env->GetStringUTFChars(jfilename, NULL);
-    if (filenameChars == NULL) {
+        JNIEnv* env, jclass /* clazz */, jobject jfileDescriptor) {
+    int fd = jniGetFDFromFileDescriptor(env, jfileDescriptor);
+    if (fd < 0) {
+        ALOGI("Invalid file descriptor");
         return NULL;
     }
-    String8 filename(filenameChars);
-    env->ReleaseStringUTFChars(jfilename, filenameChars);
 
     piex::PreviewImageData image_data;
-    std::unique_ptr<FileStream> stream(new FileStream(filename));
+    std::unique_ptr<FileStream> stream(new FileStream(fd));
 
-    if (!GetExifFromRawImage(stream.get(), filename, image_data)) {
-        ALOGI("Raw image not detected: %s", filename.string());
+    if (!GetExifFromRawImage(stream.get(), String8("[file descriptor]"), image_data)) {
+        ALOGI("Raw image not detected");
         return NULL;
     }
 
@@ -263,7 +262,7 @@
 
 static JNINativeMethod gMethods[] = {
     { "initRawNative", "()V", (void *)ExifInterface_initRaw },
-    { "getRawAttributesNative", "(Ljava/lang/String;)Ljava/util/HashMap;",
+    { "getRawAttributesNative", "(Ljava/io/FileDescriptor;)Ljava/util/HashMap;",
       (void*)ExifInterface_getRawMetadata },
 };
 
diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp
index c08a5e3..9c4f7c4 100644
--- a/media/jni/android_media_Utils.cpp
+++ b/media/jni/android_media_Utils.cpp
@@ -28,6 +28,19 @@
 
 namespace android {
 
+FileStream::FileStream(const int fd)
+    : mPosition(0),
+      mSize(0) {
+    mFile = fdopen(fd, "r");
+    if (mFile == NULL) {
+        return;
+    }
+    // Get the size.
+    fseek(mFile, 0l, SEEK_END);
+    mSize = ftell(mFile);
+    fseek(mFile, 0l, SEEK_SET);
+}
+
 FileStream::FileStream(const String8 filename)
     : mPosition(0),
       mSize(0) {
diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h
index 762c904..a30e1be 100644
--- a/media/jni/android_media_Utils.h
+++ b/media/jni/android_media_Utils.h
@@ -35,6 +35,7 @@
     size_t mSize;
 
 public:
+    FileStream(const int fd);
     FileStream(const String8 filename);
     ~FileStream();
 
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
new file mode 100644
index 0000000..477cd3a
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
new file mode 100644
index 0000000..78ac703
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
new file mode 100644
index 0000000..5fcc720
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/values/exifinterface.xml b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
new file mode 100644
index 0000000..8fc6adc
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<resources>
+    <array name="exifbyteorderii_jpg">
+        <item>true</item>
+        <item>512</item>
+        <item>288</item>
+        <item>false</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>SAMSUNG</item>
+        <item>SM-N900S</item>
+        <item>2.200</item>
+        <item>2016:01:29 18:32:27</item>
+        <item>0.033</item>
+        <item>0</item>
+        <item>413/100</item>
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item>480</item>
+        <item>640</item>
+        <item>50</item>
+        <item>6</item>
+        <item>0</item>
+    </array>
+    <array name="exifbyteordermm_jpg">
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>true</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>LGE</item>
+        <item>Nexus 5</item>
+        <item>2.400</item>
+        <item>2016:01:29 15:44:58</item>
+        <item>0.017</item>
+        <item>0</item>
+        <item>3970/1000</item>
+        <item>0/1000</item>
+        <item>0</item>
+        <item>1970:01:01</item>
+        <item>0/1,0/1,0/10000</item>
+        <item>N</item>
+        <item>0/1,0/1,0/10000</item>
+        <item>E</item>
+        <item>GPS</item>
+        <item>00:00:00</item>
+        <item>176</item>
+        <item>144</item>
+        <item>146</item>
+        <item>0</item>
+        <item>0</item>
+    </array>
+    <array name="lg_g4_iso_800_dng">
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>true</item>
+        <item>53.834507</item>
+        <item>10.69585</item>
+        <item>0.0</item>
+        <item>LGE</item>
+        <item>LG-H815</item>
+        <item>1.800</item>
+        <item>2015:11:12 16:46:18</item>
+        <item>0.0040</item>
+        <item>0.0</item>
+        <item>442/100</item>
+        <item>0/1</item>
+        <item>0</item>
+        <item>1970:01:17</item>
+        <item>53/1,50/1,423/100</item>
+        <item>N</item>
+        <item>10/1,41/1,4506/100</item>
+        <item>E</item>
+        <item />
+        <item>18:08:10</item>
+        <item>337</item>
+        <item>600</item>
+        <item>800</item>
+        <item>1</item>
+        <item />
+    </array>
+</resources>
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
index 11d9070..61dede3 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
@@ -50,6 +50,7 @@
         addMediaScannerUnitTests(suite);
         addCameraUnitTests(suite);
         addImageReaderTests(suite);
+        addExifInterfaceTests(suite);
         return suite;
     }
 
@@ -109,4 +110,8 @@
     private void addMediaScannerUnitTests(TestSuite suite) {
         suite.addTestSuite(MediaInserterTest.class);
     }
+
+    private void addExifInterfaceTests(TestSuite suite) {
+        suite.addTestSuite(ExifInterfaceTest.class);
+    }
 }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
new file mode 100644
index 0000000..1c80746
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2016 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 com.android.mediaframeworktest.R;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+public class ExifInterfaceTest extends AndroidTestCase {
+    private static final String TAG = ExifInterface.class.getSimpleName();
+    private static final boolean VERBOSE = false;  // lots of logging
+
+    private static final double DIFFERENCE_TOLERANCE = .005;
+    private static final int BUFFER_SIZE = 32768;
+
+    // List of files.
+    private static final String EXIF_BYTE_ORDER_II_JPEG = "ExifByteOrderII.jpg";
+    private static final String EXIF_BYTE_ORDER_MM_JPEG = "ExifByteOrderMM.jpg";
+    private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
+    private static final int[] IMAGE_RESOURCES = new int[] {
+            R.raw.image_exif_byte_order_ii,  R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800 };
+    private static final String[] IMAGE_FILENAMES = new String[] {
+            EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG };
+
+    private static final String[] EXIF_TAGS = {
+            ExifInterface.TAG_MAKE,
+            ExifInterface.TAG_MODEL,
+            ExifInterface.TAG_APERTURE,
+            ExifInterface.TAG_DATETIME,
+            ExifInterface.TAG_EXPOSURE_TIME,
+            ExifInterface.TAG_FLASH,
+            ExifInterface.TAG_FOCAL_LENGTH,
+            ExifInterface.TAG_GPS_ALTITUDE,
+            ExifInterface.TAG_GPS_ALTITUDE_REF,
+            ExifInterface.TAG_GPS_DATESTAMP,
+            ExifInterface.TAG_GPS_LATITUDE,
+            ExifInterface.TAG_GPS_LATITUDE_REF,
+            ExifInterface.TAG_GPS_LONGITUDE,
+            ExifInterface.TAG_GPS_LONGITUDE_REF,
+            ExifInterface.TAG_GPS_PROCESSING_METHOD,
+            ExifInterface.TAG_GPS_TIMESTAMP,
+            ExifInterface.TAG_IMAGE_LENGTH,
+            ExifInterface.TAG_IMAGE_WIDTH,
+            ExifInterface.TAG_ISO,
+            ExifInterface.TAG_ORIENTATION,
+            ExifInterface.TAG_WHITE_BALANCE
+    };
+
+    private static class ExpectedValue {
+        // Thumbnail information.
+        public final boolean hasThumbnail;
+        public final int thumbnailWidth;
+        public final int thumbnailHeight;
+
+        // GPS information.
+        public final boolean hasLatLong;
+        public final float latitude;
+        public final float longitude;
+        public final float altitude;
+
+        // Values.
+        public final String make;
+        public final String model;
+        public final float aperture;
+        public final String datetime;
+        public final float exposureTime;
+        public final float flash;
+        public final String focalLength;
+        public final String gpsAltitude;
+        public final String gpsAltitudeRef;
+        public final String gpsDatestamp;
+        public final String gpsLatitude;
+        public final String gpsLatitudeRef;
+        public final String gpsLongitude;
+        public final String gpsLongitudeRef;
+        public final String gpsProcessingMethod;
+        public final String gpsTimestamp;
+        public final String imageLength;
+        public final String imageWidth;
+        public final String iso;
+        public final String whiteBalance;
+        public final String orientation;
+
+        private static String getString(TypedArray typedArray, int index) {
+            String stringValue = typedArray.getString(index);
+            if (stringValue == null || stringValue.equals("")) {
+                return null;
+            }
+            return stringValue.trim();
+        }
+
+        public ExpectedValue(TypedArray typedArray) {
+            // Reads thumbnail information.
+            hasThumbnail = typedArray.getBoolean(0, false);
+            thumbnailWidth = typedArray.getInt(1, 0);
+            thumbnailHeight = typedArray.getInt(2, 0);
+
+            // Reads GPS information.
+            hasLatLong = typedArray.getBoolean(3, false);
+            latitude = typedArray.getFloat(4, 0f);
+            longitude = typedArray.getFloat(5, 0f);
+            altitude = typedArray.getFloat(6, 0f);
+
+            // Read values.
+            make = getString(typedArray, 7);
+            model = getString(typedArray, 8);
+            aperture = typedArray.getFloat(9, 0f);
+            datetime = getString(typedArray, 10);
+            exposureTime = typedArray.getFloat(11, 0f);
+            flash = typedArray.getFloat(12, 0f);
+            focalLength = getString(typedArray, 13);
+            gpsAltitude = getString(typedArray, 14);
+            gpsAltitudeRef = getString(typedArray, 15);
+            gpsDatestamp = getString(typedArray, 16);
+            gpsLatitude = getString(typedArray, 17);
+            gpsLatitudeRef = getString(typedArray, 18);
+            gpsLongitude = getString(typedArray, 19);
+            gpsLongitudeRef = getString(typedArray, 20);
+            gpsProcessingMethod = getString(typedArray, 21);
+            gpsTimestamp = getString(typedArray, 22);
+            imageLength = getString(typedArray, 23);
+            imageWidth = getString(typedArray, 24);
+            iso = getString(typedArray, 25);
+            orientation = getString(typedArray, 26);
+            whiteBalance = getString(typedArray, 27);
+
+            typedArray.recycle();
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        byte[] buffer = new byte[BUFFER_SIZE];
+
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String outputPath = new File(Environment.getExternalStorageDirectory(),
+                    IMAGE_FILENAMES[i]).getAbsolutePath();
+            try (InputStream inputStream = getContext().getResources().openRawResource(
+                    IMAGE_RESOURCES[i])) {
+                try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
+                    Streams.copy(inputStream, outputStream);
+                }
+            }
+        }
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String imageFilePath = new File(Environment.getExternalStorageDirectory(),
+                    IMAGE_FILENAMES[i]).getAbsolutePath();
+            File imageFile = new File(imageFilePath);
+            if (imageFile.exists()) {
+                imageFile.delete();
+            }
+        }
+
+        super.tearDown();
+    }
+
+    private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
+        // Prints thumbnail information.
+        if (exifInterface.hasThumbnail()) {
+            byte[] thumbnailBytes = exifInterface.getThumbnail();
+            if (thumbnailBytes != null) {
+                Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length);
+                Bitmap bitmap = BitmapFactory.decodeByteArray(
+                        thumbnailBytes, 0, thumbnailBytes.length);
+                if (bitmap == null) {
+                    Log.e(TAG, fileName + " Corrupted thumbnail!");
+                } else {
+                    Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", "
+                            + bitmap.getHeight());
+                }
+            } else {
+                Log.e(TAG, fileName + " Corrupted image (no thumbnail)");
+            }
+        } else {
+            if (exifInterface.getThumbnail() != null) {
+                Log.e(TAG, fileName + " Corrupted image (a thumbnail exists)");
+            } else {
+                Log.v(TAG, fileName + " No thumbnail");
+            }
+        }
+
+        // Prints GPS information.
+        Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
+
+        float[] latLong = new float[2];
+        if (exifInterface.getLatLong(latLong)) {
+            Log.v(TAG, fileName + " Latitude = " + latLong[0]);
+            Log.v(TAG, fileName + " Longitude = " + latLong[1]);
+        } else {
+            Log.v(TAG, fileName + "No latlong data");
+        }
+
+        // Prints values.
+        for (String tagKey : EXIF_TAGS) {
+            String tagValue = exifInterface.getAttribute(tagKey);
+            Log.v(TAG, fileName + "Key{" + tagKey + "} = '" + tagValue + "'");
+        }
+    }
+
+    private void compareFloatTag(ExifInterface exifInterface, String tag, float expectedValue) {
+        String stringValue = exifInterface.getAttribute(tag);
+        float floatValue = 0f;
+
+        if (stringValue != null) {
+            floatValue = Float.parseFloat(stringValue);
+        }
+
+        assertEquals(expectedValue, floatValue, DIFFERENCE_TOLERANCE);
+    }
+
+    private void compareStringTag(ExifInterface exifInterface, String tag, String expectedValue) {
+        String stringValue = exifInterface.getAttribute(tag);
+        if (stringValue != null) {
+            stringValue = stringValue.trim();
+        }
+
+        assertEquals(expectedValue, stringValue);
+    }
+
+    private void compareWithExpectedValue(ExifInterface exifInterface,
+            ExpectedValue expectedValue) {
+        // Checks a thumbnail image.
+        assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
+        if (expectedValue.hasThumbnail) {
+            byte[] thumbnailBytes = exifInterface.getThumbnail();
+            assertNotNull(thumbnailBytes);
+            Bitmap thumbnailBitmap =
+                    BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.length);
+            assertNotNull(thumbnailBitmap);
+            assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
+            assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
+        } else {
+            assertNull(exifInterface.getThumbnail());
+        }
+
+        // Checks GPS information.
+        float[] latLong = new float[2];
+        assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+        if (expectedValue.hasLatLong) {
+            assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
+            assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
+        }
+        assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
+
+        // Checks values.
+        compareStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make);
+        compareStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model);
+        compareFloatTag(exifInterface, ExifInterface.TAG_APERTURE, expectedValue.aperture);
+        compareStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime);
+        compareFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime);
+        compareFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash);
+        compareStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF,
+                expectedValue.gpsAltitudeRef);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP,
+                expectedValue.gpsDatestamp);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF,
+                expectedValue.gpsLatitudeRef);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE,
+                expectedValue.gpsLongitude);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF,
+                expectedValue.gpsLongitudeRef);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD,
+                expectedValue.gpsProcessingMethod);
+        compareStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP,
+                expectedValue.gpsTimestamp);
+        compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength);
+        compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth);
+        compareStringTag(exifInterface, ExifInterface.TAG_ISO, expectedValue.iso);
+        compareStringTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation);
+        compareStringTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE,
+                expectedValue.whiteBalance);
+    }
+
+    private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
+            throws IOException {
+        ExpectedValue expectedValue = new ExpectedValue(
+                getContext().getResources().obtainTypedArray(typedArrayResourceId));
+        File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+        // Created via path.
+        ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+        if (VERBOSE) {
+            printExifTagsAndValues(fileName, exifInterface);
+        }
+        compareWithExpectedValue(exifInterface, expectedValue);
+
+        // Created via InputStream.
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(imageFile.getAbsolutePath());
+            exifInterface = new ExifInterface(in);
+            if (VERBOSE) {
+                printExifTagsAndValues(fileName, exifInterface);
+            }
+            compareWithExpectedValue(exifInterface, expectedValue);
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+
+        // Created via FileDescriptor.
+        try {
+            FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600);
+            exifInterface = new ExifInterface(fd);
+            if (VERBOSE) {
+                printExifTagsAndValues(fileName, exifInterface);
+            }
+            compareWithExpectedValue(exifInterface, expectedValue);
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        }
+
+        // Test for saving attributes.
+        try {
+            FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
+            exifInterface = new ExifInterface(fd);
+            exifInterface.saveAttributes();
+            exifInterface = new ExifInterface(fd);
+            if (VERBOSE) {
+                printExifTagsAndValues(fileName, exifInterface);
+            }
+            compareWithExpectedValue(exifInterface, expectedValue);
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        }
+
+        // Test for modifying one attribute.
+        exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+        exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+        exifInterface.saveAttributes();
+        exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+        if (VERBOSE) {
+            printExifTagsAndValues(fileName, exifInterface);
+        }
+        assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
+    }
+
+    private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId)
+            throws IOException {
+        ExpectedValue expectedValue = new ExpectedValue(
+                getContext().getResources().obtainTypedArray(typedArrayResourceId));
+        File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+        // Created via path.
+        ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+        if (VERBOSE) {
+            printExifTagsAndValues(fileName, exifInterface);
+        }
+        compareWithExpectedValue(exifInterface, expectedValue);
+
+        // Created via FileDescriptor.
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(imageFile);
+            exifInterface = new ExifInterface(in.getFD());
+            if (VERBOSE) {
+                printExifTagsAndValues(fileName, exifInterface);
+            }
+            compareWithExpectedValue(exifInterface, expectedValue);
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
+        testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
+    }
+
+    public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
+        testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
+    }
+
+    public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
+        testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
+    }
+}