Merge "ExifInterface: add support for reading metadata from RAW images"
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 445ee6f..7fb67ee 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -17,7 +17,7 @@
 package android.media;
 
 import java.io.IOException;
-import java.util.regex.Matcher;
+import java.io.RandomAccessFile;
 import java.util.regex.Pattern;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
@@ -27,7 +27,9 @@
 import java.util.TimeZone;
 
 /**
- * This is a class for reading and writing Exif tags in a JPEG file.
+ * 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.
  */
 public class ExifInterface {
     // The Exif tag names
@@ -68,8 +70,6 @@
     /** Type is int. */
     public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
 
-
-
     /**
      * @hide
      */
@@ -98,15 +98,22 @@
     /** Type is String. Name of GPS processing method used for location finding. */
     public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
 
+    // Private tags used for thumbnail information.
+    private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
+    private static final String TAG_THUMBNAIL_OFFSET = "thumbnailOffset";
+    private static final String TAG_THUMBNAIL_LENGTH = "thumbnailLength";
+
     // Constants used for the Orientation Exif tag.
     public static final int ORIENTATION_UNDEFINED = 0;
     public static final int ORIENTATION_NORMAL = 1;
     public static final int ORIENTATION_FLIP_HORIZONTAL = 2;  // left right reversed mirror
     public static final int ORIENTATION_ROTATE_180 = 3;
     public static final int ORIENTATION_FLIP_VERTICAL = 4;  // upside down mirror
-    public static final int ORIENTATION_TRANSPOSE = 5;  // flipped about top-left <--> bottom-right axis
+    // flipped about top-left <--> bottom-right axis
+    public static final int ORIENTATION_TRANSPOSE = 5;
     public static final int ORIENTATION_ROTATE_90 = 6;  // rotate 90 cw to right it
-    public static final int ORIENTATION_TRANSVERSE = 7;  // flipped about top-right <--> bottom-left axis
+    // flipped about top-right <--> bottom-left axis
+    public static final int ORIENTATION_TRANSVERSE = 7;
     public static final int ORIENTATION_ROTATE_270 = 8;  // rotate 270 to right it
 
     // Constants used for white balance
@@ -116,13 +123,20 @@
 
     static {
         System.loadLibrary("jhead_jni");
+        System.loadLibrary("media_jni");
+        initRawNative();
+
         sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
         sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
     }
 
-    private String mFilename;
-    private HashMap<String, String> mAttributes;
+    private final String mFilename;
+    private final HashMap<String, String> mAttributes = new HashMap<>();
+    private boolean mIsRaw;
     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
@@ -134,19 +148,20 @@
     private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
 
     /**
-     * Reads Exif tags from the specified JPEG file.
+     * Reads Exif tags from the specified image file.
      */
     public ExifInterface(String filename) throws IOException {
         if (filename == null) {
             throw new IllegalArgumentException("filename cannot be null");
         }
         mFilename = filename;
+        // First test whether a given file is a one of RAW format or not.
         loadAttributes();
     }
 
     /**
      * Returns the value of the specified tag or {@code null} if there
-     * is no such tag in the JPEG file.
+     * is no such tag in the image file.
      *
      * @param tag the name of the tag.
      */
@@ -156,7 +171,7 @@
 
     /**
      * Returns the integer value of the specified tag. If there is no such tag
-     * in the JPEG file or the value cannot be parsed as integer, return
+     * in the image file or the value cannot be parsed as integer, return
      * <var>defaultValue</var>.
      *
      * @param tag the name of the tag.
@@ -174,7 +189,7 @@
 
     /**
      * Returns the double value of the specified rational tag. If there is no
-     * such tag in the JPEG file or the value cannot be parsed as double, return
+     * 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.
@@ -210,17 +225,42 @@
      *
      * mAttributes is a HashMap which stores the Exif attributes of the file.
      * The key is the standard tag name and the value is the tag's value: e.g.
-     * Model -> Nikon. Numeric values are stored as strings.
+     * Model -&gt; Nikon. Numeric values are stored as strings.
      *
      * This function also initialize mHasThumbnail to indicate whether the
      * 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();
+
+                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;
+                }
+            }
+            return;
+        }
+
         // 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"
-        mAttributes = new HashMap<String, String>();
 
         String attrStr;
         synchronized (sLock) {
@@ -248,7 +288,7 @@
             String attrValue = attrStr.substring(ptr, ptr + attrLen);
             ptr += attrLen;
 
-            if (attrName.equals("hasThumbnail")) {
+            if (attrName.equals(TAG_HAS_THUMBNAIL)) {
                 mHasThumbnail = attrValue.equalsIgnoreCase("true");
             } else {
                 mAttributes.put(attrName, attrValue);
@@ -257,32 +297,36 @@
     }
 
     /**
-     * Save the tag data into the JPEG file. This is expensive because it involves
-     * copying all the JPG data from one file to another and deleting the old file
-     * and renaming the other. It's best to use {@link #setAttribute(String,String)}
-     * to set all attributes to write and make a single call rather than multiple
-     * calls for each attribute.
+     * Save the tag data into the original image file. This is expensive because it involves
+     * copying all the data from one file to another and deleting the old file and renaming the
+     * other. It's best to use{@link #setAttribute(String,String)} to set all attributes to write
+     * and make a single call rather than multiple calls for each attribute.
      */
     public void saveAttributes() throws IOException {
+        if (mIsRaw) {
+            throw new UnsupportedOperationException(
+                    "ExifInterface does not support saving attributes on RAW formats.");
+        }
+
         // 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("hasThumbnail")) {
+        if (mAttributes.containsKey(TAG_HAS_THUMBNAIL)) {
             --size;
         }
-        sb.append(size + " ");
-        for (Map.Entry<String, String> iter : mAttributes.entrySet()) {
-            String key = iter.getKey();
-            if (key.equals("hasThumbnail")) {
+        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;
             }
-            String val = iter.getValue();
-            sb.append(key + "=");
-            sb.append(val.length() + " ");
+            String val = entry.getValue();
+            sb.append(key).append("=");
+            sb.append(val.length()).append(" ");
             sb.append(val);
         }
         String s = sb.toString();
@@ -293,25 +337,43 @@
     }
 
     /**
-     * Returns true if the JPEG file has a thumbnail.
+     * Returns true if the image file has a thumbnail.
      */
     public boolean hasThumbnail() {
         return mHasThumbnail;
     }
 
     /**
-     * Returns the thumbnail inside the JPEG file, or {@code null} if there is no thumbnail.
+     * Returns the thumbnail inside the image file, or {@code null} if there is no thumbnail.
      * The returned data is in JPEG format and can be decoded using
      * {@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.
+                }
+            }
+            return null;
+        }
+
         synchronized (sLock) {
             return getThumbnailNative(mFilename);
         }
     }
 
     /**
-     * Returns the offset and length of thumbnail inside the JPEG file, or
+     * Returns the offset and length of thumbnail inside the image file, or
      * {@code null} if there is no thumbnail.
      *
      * @return two-element array, the offset in the first value, and length in
@@ -319,6 +381,13 @@
      * @hide
      */
     public long[] getThumbnailRange() {
+        if (mIsRaw) {
+            long[] range = new long[2];
+            range[0] = mThumbnailOffset;
+            range[1] = mThumbnailLength;
+            return range;
+        }
+
         synchronized (sLock) {
             return getThumbnailRangeNative(mFilename);
         }
@@ -447,26 +516,28 @@
                 return (float) -result;
             }
             return (float) result;
-        } catch (NumberFormatException e) {
-            // Some of the nubmers are not valid
-            throw new IllegalArgumentException();
-        } catch (ArrayIndexOutOfBoundsException e) {
-            // Some of the rational does not follow the correct format
+        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+            // Not valid
             throw new IllegalArgumentException();
         }
     }
 
-    private native boolean appendThumbnailNative(String fileName,
+    // JNI methods for JPEG.
+    private static native boolean appendThumbnailNative(String fileName,
             String thumbnailFileName);
 
-    private native void saveAttributesNative(String fileName,
+    private static native void saveAttributesNative(String fileName,
             String compressedAttributes);
 
-    private native String getAttributesNative(String fileName);
+    private static native String getAttributesNative(String fileName);
 
-    private native void commitChangesNative(String fileName);
+    private static native void commitChangesNative(String fileName);
 
-    private native byte[] getThumbnailNative(String fileName);
+    private static native byte[] getThumbnailNative(String fileName);
 
-    private native long[] getThumbnailRangeNative(String fileName);
+    private static native long[] getThumbnailRangeNative(String fileName);
+
+    // JNI methods for RAW formats.
+    private static native void initRawNative();
+    private static native HashMap getRawAttributesNative(String filename);
 }
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index 79557bc..a326f6f 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -3,6 +3,7 @@
 
 LOCAL_SRC_FILES:= \
     android_media_AmrInputStream.cpp \
+    android_media_ExifInterface.cpp \
     android_media_ImageWriter.cpp \
     android_media_ImageReader.cpp \
     android_media_MediaCrypto.cpp \
@@ -44,6 +45,7 @@
     libusbhost \
     libjhead \
     libexif \
+    libpiex \
     libstagefright_amrnb_common
 
 LOCAL_REQUIRED_MODULES := \
@@ -54,6 +56,7 @@
 
 LOCAL_C_INCLUDES += \
     external/libexif/ \
+    external/piex/ \
     external/tremor/Tremor \
     frameworks/base/core/jni \
     frameworks/base/libs/hwui \
diff --git a/media/jni/android_media_ExifInterface.cpp b/media/jni/android_media_ExifInterface.cpp
new file mode 100644
index 0000000..463c17e
--- /dev/null
+++ b/media/jni/android_media_ExifInterface.cpp
@@ -0,0 +1,364 @@
+/*
+ * Copyright 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "ExifInterface_JNI"
+#include <utils/Log.h>
+#include <utils/String8.h>
+#include <utils/KeyedVector.h>
+
+#include <android_runtime/AndroidRuntime.h>
+
+#include <jni.h>
+#include <JNIHelp.h>
+
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "src/piex_types.h"
+#include "src/piex.h"
+
+// ----------------------------------------------------------------------------
+
+using namespace android;
+
+class FileStream : public piex::StreamInterface {
+private:
+    FILE *mFile;
+    size_t mPosition;
+    size_t mSize;
+
+public:
+    FileStream(const String8 filename)
+        : mPosition(0),
+          mSize(0) {
+        mFile = fopen(filename.string(), "r");
+        if (mFile == NULL) {
+            return;
+        }
+        // Get the size.
+        fseek(mFile, 0l, SEEK_END);
+        mSize = ftell(mFile);
+        fseek(mFile, 0l, SEEK_SET);
+    }
+
+    ~FileStream() {
+        if (mFile != NULL) {
+            fclose(mFile);
+            mFile = NULL;
+        }
+    }
+
+    // Reads 'length' amount of bytes from 'offset' to 'data'. The 'data' buffer
+    // provided by the caller, guaranteed to be at least "length" bytes long.
+    // On 'kOk' the 'data' pointer contains 'length' valid bytes beginning at
+    // 'offset' bytes from the start of the stream.
+    // Returns 'kFail' if 'offset' + 'length' exceeds the stream and does not
+    // change the contents of 'data'.
+    piex::Error GetData(
+            const size_t offset, const size_t length, std::uint8_t* data)
+            override {
+        if (mFile == NULL) {
+            return piex::Error::kFail;
+        }
+
+        // Seek first.
+        if (mPosition != offset) {
+            fseek(mFile, offset, SEEK_SET);
+        }
+
+        // Read bytes.
+        size_t size = fread((void*)data, sizeof(std::uint8_t), length, mFile);
+        mPosition += size;
+
+        // Handle errors.
+        if (ferror(mFile)) {
+            return piex::Error::kFail;
+        }
+        if (size == 0 && feof(mFile)) {
+            return piex::Error::kFail;
+        }
+        return piex::Error::kOk;
+    }
+
+    bool exists() {
+        return mFile != NULL;
+    }
+
+    size_t size() {
+        return mSize;
+    }
+};
+
+#define FIND_CLASS(var, className) \
+    var = env->FindClass(className); \
+    LOG_FATAL_IF(! var, "Unable to find class " className);
+
+#define GET_METHOD_ID(var, clazz, fieldName, fieldDescriptor) \
+    var = env->GetMethodID(clazz, fieldName, fieldDescriptor); \
+    LOG_FATAL_IF(! var, "Unable to find method " fieldName);
+
+struct HashMapFields {
+    jmethodID init;
+    jmethodID put;
+};
+
+struct fields_t {
+    HashMapFields hashMap;
+    jclass hashMapClassId;
+};
+
+static fields_t gFields;
+
+static jobject KeyedVectorToHashMap(JNIEnv *env, KeyedVector<String8, String8> const &map) {
+    jclass clazz = gFields.hashMapClassId;
+    jobject hashMap = env->NewObject(clazz, gFields.hashMap.init);
+    for (size_t i = 0; i < map.size(); ++i) {
+        jstring jkey = env->NewStringUTF(map.keyAt(i).string());
+        jstring jvalue = env->NewStringUTF(map.valueAt(i).string());
+        env->CallObjectMethod(hashMap, gFields.hashMap.put, jkey, jvalue);
+        env->DeleteLocalRef(jkey);
+        env->DeleteLocalRef(jvalue);
+    }
+    return hashMap;
+}
+
+extern "C" {
+
+// -------------------------- ExifInterface methods ---------------------------
+
+static void ExifInterface_initRaw(JNIEnv *env) {
+    jclass clazz;
+    FIND_CLASS(clazz, "java/util/HashMap");
+    gFields.hashMapClassId = static_cast<jclass>(env->NewGlobalRef(clazz));
+
+    GET_METHOD_ID(gFields.hashMap.init, clazz, "<init>", "()V");
+    GET_METHOD_ID(gFields.hashMap.put, clazz, "put",
+                  "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+}
+
+static jobject ExifInterface_getRawMetadata(
+        JNIEnv* env, jclass /* clazz */, jstring jfilename) {
+    const char* filenameChars = env->GetStringUTFChars(jfilename, NULL);
+    if (filenameChars == NULL) {
+        return NULL;
+    }
+    String8 filename(filenameChars);
+    env->ReleaseStringUTFChars(jfilename, filenameChars);
+
+    piex::PreviewImageData image_data;
+    memset(&image_data, 0, sizeof(image_data));
+    std::unique_ptr<FileStream> stream(new FileStream(filename));
+
+    if (!stream.get()->exists()) {
+        // File is not exists.
+        ALOGI("File is not exists: %s", filename.string());
+        return NULL;
+    }
+
+    if (!piex::IsRaw(stream.get())) {
+        // Format not supported.
+        ALOGI("Format not supported: %s", filename.string());
+        return NULL;
+    }
+
+    piex::Error err = piex::GetPreviewImageData(stream.get(), &image_data);
+    if (err != piex::Error::kOk) {
+        // The input data seems to be broken.
+        ALOGI("Raw image not detected: %s (error code: %d)", filename.string(), (int32_t)err);
+        return NULL;
+    }
+
+    if (image_data.thumbnail_offset + image_data.thumbnail_length > stream.get()->size()) {
+        // Corrupted file.
+        ALOGI("Corrupted file: %s", filename.string());
+        return NULL;
+    }
+
+    KeyedVector<String8, String8> map;
+
+    if (image_data.thumbnail_length > 0) {
+        map.add(String8("hasThumbnail"), String8("true"));
+        map.add(String8("thumbnailOffset"), String8::format("%d", image_data.thumbnail_offset));
+        map.add(String8("thumbnailLength"), String8::format("%d", image_data.thumbnail_length));
+    } else {
+        map.add(String8("hasThumbnail"), String8("false"));
+    }
+
+    map.add(
+            String8("Orientation"),
+            String8::format("%u", image_data.exif_orientation));
+    map.add(
+            String8("ImageWidth"),
+            String8::format("%u", image_data.full_width));
+    map.add(
+            String8("ImageLength"),
+            String8::format("%u", image_data.full_height));
+
+    // Current PIEX does not have LightSource information while JPEG version of
+    // EXIFInterface always declares the light source field. For the
+    // compatibility, it provides the default value of the light source field.
+    map.add(String8("LightSource"), String8("0"));
+
+    if (!image_data.maker.empty()) {
+        map.add(String8("Make"), String8(image_data.maker.c_str()));
+    }
+
+    if (!image_data.model.empty()) {
+        map.add(String8("Model"), String8(image_data.model.c_str()));
+    }
+
+    if (!image_data.date_time.empty()) {
+        map.add(String8("DateTime"), String8(image_data.date_time.c_str()));
+    }
+
+    if (image_data.iso) {
+        map.add(
+                String8("ISOSpeedRatings"),
+                String8::format("%u", image_data.iso));
+    }
+
+    if (image_data.exposure_time.numerator != 0
+            && image_data.exposure_time.denominator != 0) {
+        double exposureTime =
+            (double)image_data.exposure_time.numerator
+            / image_data.exposure_time.denominator;
+
+        const char* format;
+        if (exposureTime < 0.01) {
+            format = "%6.4f";
+        } else {
+            format = "%5.3f";
+        }
+        map.add(String8("ExposureTime"), String8::format(format, exposureTime));
+    }
+
+    if (image_data.fnumber.numerator != 0
+            && image_data.fnumber.denominator != 0) {
+        double fnumber =
+            (double)image_data.fnumber.numerator
+            / image_data.fnumber.denominator;
+        map.add(String8("FNumber"), String8::format("%5.3f", fnumber));
+    }
+
+    if (image_data.focal_length.numerator != 0
+            && image_data.focal_length.denominator != 0) {
+        map.add(
+                String8("FocalLength"),
+                String8::format(
+                        "%u/%u",
+                        image_data.focal_length.numerator,
+                        image_data.focal_length.denominator));
+    }
+
+    if (image_data.gps.is_valid) {
+        if (image_data.gps.latitude[0].denominator != 0
+                && image_data.gps.latitude[1].denominator != 0
+                && image_data.gps.latitude[2].denominator != 0) {
+            map.add(
+                    String8("GPSLatitude"),
+                    String8::format(
+                            "%u/%u,%u/%u,%u/%u",
+                            image_data.gps.latitude[0].numerator,
+                            image_data.gps.latitude[0].denominator,
+                            image_data.gps.latitude[1].numerator,
+                            image_data.gps.latitude[1].denominator,
+                            image_data.gps.latitude[2].numerator,
+                            image_data.gps.latitude[2].denominator));
+        }
+
+        if (image_data.gps.latitude_ref) {
+            char str[2];
+            str[0] = image_data.gps.latitude_ref;
+            str[1] = 0;
+            map.add(String8("GPSLatitudeRef"), String8(str));
+        }
+
+        if (image_data.gps.longitude[0].denominator != 0
+                && image_data.gps.longitude[1].denominator != 0
+                && image_data.gps.longitude[2].denominator != 0) {
+            map.add(
+                    String8("GPSLongitude"),
+                    String8::format(
+                            "%u/%u,%u/%u,%u/%u",
+                            image_data.gps.longitude[0].numerator,
+                            image_data.gps.longitude[0].denominator,
+                            image_data.gps.longitude[1].numerator,
+                            image_data.gps.longitude[1].denominator,
+                            image_data.gps.longitude[2].numerator,
+                            image_data.gps.longitude[2].denominator));
+        }
+
+        if (image_data.gps.longitude_ref) {
+            char str[2];
+            str[0] = image_data.gps.longitude_ref;
+            str[1] = 0;
+            map.add(String8("GPSLongitudeRef"), String8(str));
+        }
+
+        if (image_data.gps.altitude.denominator != 0) {
+            map.add(
+                    String8("GPSAltitude"),
+                    String8::format("%u/%u",
+                            image_data.gps.altitude.numerator,
+                            image_data.gps.altitude.denominator));
+
+            map.add(
+                    String8("GPSAltitudeRef"),
+                    String8(image_data.gps.altitude_ref ? "1" : "0"));
+        }
+
+        if (image_data.gps.time_stamp[0].denominator != 0
+                && image_data.gps.time_stamp[1].denominator != 0
+                && image_data.gps.time_stamp[2].denominator != 0) {
+            map.add(
+                    String8("GPSTimeStamp"),
+                    String8::format(
+                            "%2u:%2u:%2u",
+                            image_data.gps.time_stamp[0].numerator
+                            / image_data.gps.time_stamp[0].denominator,
+                            image_data.gps.time_stamp[1].numerator
+                            / image_data.gps.time_stamp[1].denominator,
+                            image_data.gps.time_stamp[2].numerator
+                            / image_data.gps.time_stamp[2].denominator));
+        }
+
+        if (!image_data.gps.date_stamp.empty()) {
+            map.add(
+                    String8("GPSDateStamp"),
+                    String8(image_data.gps.date_stamp.c_str()));
+        }
+    }
+
+    return KeyedVectorToHashMap(env, map);
+}
+
+} // extern "C"
+
+// ----------------------------------------------------------------------------
+
+static JNINativeMethod gMethods[] = {
+    { "initRawNative", "()V", (void *)ExifInterface_initRaw },
+    { "getRawAttributesNative", "(Ljava/lang/String;)Ljava/util/HashMap;",
+      (void*)ExifInterface_getRawMetadata },
+};
+
+int register_android_media_ExifInterface(JNIEnv *env) {
+    return AndroidRuntime::registerNativeMethods(
+            env,
+            "android/media/ExifInterface",
+            gMethods,
+            NELEM(gMethods));
+}
diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp
index be36729..e9d62de 100644
--- a/media/jni/android_media_MediaPlayer.cpp
+++ b/media/jni/android_media_MediaPlayer.cpp
@@ -1088,6 +1088,7 @@
     return AndroidRuntime::registerNativeMethods(env,
                 "android/media/MediaPlayer", gMethods, NELEM(gMethods));
 }
+extern int register_android_media_ExifInterface(JNIEnv *env);
 extern int register_android_media_ImageReader(JNIEnv *env);
 extern int register_android_media_ImageWriter(JNIEnv *env);
 extern int register_android_media_Crypto(JNIEnv *env);
@@ -1219,6 +1220,11 @@
         goto bail;
     }
 
+    if (register_android_media_ExifInterface(env) < 0) {
+        ALOGE("ERROR: ExifInterface native registration failed");
+        goto bail;
+    }
+
     /* success -- return valid version number */
     result = JNI_VERSION_1_4;