Merge "Brings back the details menu with most EXIF data in it. Some are still missing and will come back later." into gb-ub-photos-carlsbad
diff --git a/res/layout/details.xml b/res/layout/details.xml
new file mode 100644
index 0000000..dfda0ee
--- /dev/null
+++ b/res/layout/details.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:gravity="left"
+/>
diff --git a/res/layout/details_list.xml b/res/layout/details_list.xml
new file mode 100644
index 0000000..b80ab6c
--- /dev/null
+++ b/res/layout/details_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="16dp"
+    android:dividerHeight="8dp"
+/>
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index 099b503..98ea363 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -29,6 +29,7 @@
 import android.content.res.Configuration;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -54,10 +55,12 @@
 import com.android.camera.data.FixedLastDataAdapter;
 import com.android.camera.data.LocalData;
 import com.android.camera.data.LocalDataAdapter;
+import com.android.camera.data.MediaDetails;
 import com.android.camera.data.SimpleViewData;
 import com.android.camera.util.ApiHelper;
 import com.android.camera.ui.CameraSwitcher;
 import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.DetailsDialog;
 import com.android.camera.ui.FilmStripView;
 import com.android.camera.util.CameraUtil;
 import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
@@ -93,7 +96,7 @@
     private static final int SUPPORT_SHOW_ON_MAP = 1 << 8;
     private static final int SUPPORT_ALL = 0xffffffff;
 
-    /** This data adapter is used by FilmStirpView. */
+    /** This data adapter is used by FilmStripView. */
     private LocalDataAdapter mDataAdapter;
     /** This data adapter represents the real local camera data. */
     private LocalDataAdapter mWrappedDataAdapter;
@@ -412,6 +415,12 @@
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
+        int currentDataId = mFilmStripView.getCurrentId();
+        if (currentDataId < 0) {
+            return false;
+        }
+        final LocalData localData = mDataAdapter.getLocalData(currentDataId);
+
         // Handle presses on the action bar items
         switch (item.getItemId()) {
             case R.id.action_delete:
@@ -439,7 +448,17 @@
                 // TODO: add the functionality.
                 return true;
             case R.id.action_details:
-                // TODO: add the functionality.
+                (new AsyncTask<Void, Void, MediaDetails>() {
+                    @Override
+                    protected MediaDetails doInBackground(Void... params) {
+                        return localData.getMediaDetails(CameraActivity.this);
+                    }
+
+                    @Override
+                    protected void onPostExecute(MediaDetails mediaDetails) {
+                        DetailsDialog.create(CameraActivity.this, mediaDetails).show();
+                    }
+                }).execute();
                 return true;
             case R.id.action_show_on_map:
                 // TODO: add the functionality.
diff --git a/src/com/android/camera/data/DataUtils.java b/src/com/android/camera/data/DataUtils.java
new file mode 100644
index 0000000..0b074d8
--- /dev/null
+++ b/src/com/android/camera/data/DataUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.data;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+public class DataUtils {
+
+    /**
+     * Get the file path from a Media storage URI.
+     */
+    public static String getPathFromURI(ContentResolver contentResolver, Uri contentUri) {
+        String[] proj = {
+                MediaStore.Images.Media.DATA
+        };
+        Cursor cursor = contentResolver.query(contentUri, proj, null, null, null);
+        if (cursor == null) {
+            return null;
+        }
+        try {
+            int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+            if (!cursor.moveToFirst()) {
+                return null;
+            } else {
+                return cursor.getString(columnIndex);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+}
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
index 7d6dfef..61714e2 100644
--- a/src/com/android/camera/data/LocalData.java
+++ b/src/com/android/camera/data/LocalData.java
@@ -23,7 +23,6 @@
 import android.view.View;
 
 import com.android.camera.ui.FilmStripView;
-import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
 
 import java.util.Comparator;
 
@@ -103,6 +102,11 @@
     Uri getContentUri();
 
     /**
+     * Return media data (such as EXIF) for the item.
+     */
+    MediaDetails getMediaDetails(Context context);
+
+    /**
      * Returns the type of the local data defined by {@link LocalData}.
      *
      * @param dataID The ID of the data.
diff --git a/src/com/android/camera/data/LocalMediaData.java b/src/com/android/camera/data/LocalMediaData.java
index d5a929d..e55274f 100644
--- a/src/com/android/camera/data/LocalMediaData.java
+++ b/src/com/android/camera/data/LocalMediaData.java
@@ -35,12 +35,13 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
-import com.android.camera.util.CameraUtil;
 import com.android.camera.ui.FilmStripView;
+import com.android.camera.util.CameraUtil;
 import com.android.camera.util.PhotoSphereHelper;
 import com.android.camera2.R;
 
 import java.io.File;
+import java.text.DateFormat;
 import java.util.Date;
 
 /**
@@ -53,12 +54,13 @@
     protected long id;
     protected String title;
     protected String mimeType;
-    protected long dateTaken;
-    protected long dateModified;
+    protected long dateTakenInSeconds;
+    protected long dateModifiedInSeconds;
     protected String path;
     // width and height should be adjusted according to orientation.
     protected int width;
     protected int height;
+    protected long sizeInBytes;
 
     /** The panorama metadata information of this media data. */
     protected PhotoSphereHelper.PanoramaMetadata mPanoramaMetadata;
@@ -74,12 +76,12 @@
 
     @Override
     public long getDateTaken() {
-        return dateTaken;
+        return dateTakenInSeconds;
     }
 
     @Override
     public long getDateModified() {
-        return dateModified;
+        return dateModifiedInSeconds;
     }
 
     @Override
@@ -214,6 +216,7 @@
         public static final int COL_ORIENTATION = 6;
         public static final int COL_WIDTH = 7;
         public static final int COL_HEIGHT = 8;
+        public static final int COL_SIZE = 9;
 
         static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
 
@@ -232,6 +235,7 @@
             MediaStore.Images.ImageColumns.ORIENTATION,   // 6, int, 0, 90, 180, 270
             MediaStore.Images.ImageColumns.WIDTH,         // 7, int
             MediaStore.Images.ImageColumns.HEIGHT,        // 8, int
+            MediaStore.Images.ImageColumns.SIZE,          // 9, long
         };
 
         private static final int mSupportedUIActions =
@@ -251,8 +255,8 @@
             d.id = c.getLong(COL_ID);
             d.title = c.getString(COL_TITLE);
             d.mimeType = c.getString(COL_MIME_TYPE);
-            d.dateTaken = c.getLong(COL_DATE_TAKEN);
-            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
+            d.dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
             d.path = c.getString(COL_DATA);
             d.orientation = c.getInt(COL_ORIENTATION);
             d.width = c.getInt(COL_WIDTH);
@@ -281,6 +285,7 @@
                 d.width = d.height;
                 d.height = b;
             }
+            d.sizeInBytes = c.getLong(COL_SIZE);
             return d;
         }
 
@@ -288,7 +293,7 @@
         public String toString() {
             return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
                     + "," + width + "x" + height + ",orientation=" + orientation
-                    + ",date=" + new Date(dateTaken);
+                    + ",date=" + new Date(dateTakenInSeconds);
         }
 
         @Override
@@ -320,6 +325,23 @@
         }
 
         @Override
+        public MediaDetails getMediaDetails(Context context) {
+            DateFormat formater = DateFormat.getDateTimeInstance();
+            MediaDetails mediaDetails = new MediaDetails();
+            mediaDetails.addDetail(MediaDetails.INDEX_TITLE, title);
+            mediaDetails.addDetail(MediaDetails.INDEX_WIDTH, width);
+            mediaDetails.addDetail(MediaDetails.INDEX_HEIGHT, height);
+            mediaDetails.addDetail(MediaDetails.INDEX_PATH, path);
+            mediaDetails.addDetail(MediaDetails.INDEX_DATETIME,
+                    formater.format(new Date(dateModifiedInSeconds * 1000)));
+            if (sizeInBytes > 0)
+                mediaDetails.addDetail(MediaDetails.INDEX_SIZE, sizeInBytes);
+
+            MediaDetails.extractExifInfo(mediaDetails, path);
+            return mediaDetails;
+        }
+
+        @Override
         public int getLocalDataType(int dataID) {
             if (mPanoramaMetadata != null && mPanoramaMetadata.mUsePanoramaViewer) {
                 return LOCAL_PHOTO_SPHERE;
@@ -338,8 +360,8 @@
             id = newData.id;
             title = newData.title;
             mimeType = newData.mimeType;
-            dateTaken = newData.dateTaken;
-            dateModified = newData.dateModified;
+            dateTakenInSeconds = newData.dateTakenInSeconds;
+            dateModifiedInSeconds = newData.dateModifiedInSeconds;
             path = newData.path;
             orientation = newData.orientation;
             width = newData.width;
@@ -436,8 +458,8 @@
             d.id = c.getLong(COL_ID);
             d.title = c.getString(COL_TITLE);
             d.mimeType = c.getString(COL_MIME_TYPE);
-            d.dateTaken = c.getLong(COL_DATE_TAKEN);
-            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
+            d.dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
             d.path = c.getString(COL_DATA);
             d.width = c.getInt(COL_WIDTH);
             d.height = c.getInt(COL_HEIGHT);
@@ -475,7 +497,7 @@
         @Override
         public String toString() {
             return "Video:" + ",data=" + path + ",mimeType=" + mimeType
-                    + "," + width + "x" + height + ",date=" + new Date(dateTaken);
+                    + "," + width + "x" + height + ",date=" + new Date(dateTakenInSeconds);
         }
 
         @Override
@@ -507,6 +529,12 @@
         }
 
         @Override
+        public MediaDetails getMediaDetails(Context context) {
+            // TODO: Return valid MediaDetails for videos.
+            return new MediaDetails();
+        }
+
+        @Override
         public int getLocalDataType(int dataID) {
             return LOCAL_VIDEO;
         }
@@ -525,8 +553,8 @@
             id = newData.id;
             title = newData.title;
             mimeType = newData.mimeType;
-            dateTaken = newData.dateTaken;
-            dateModified = newData.dateModified;
+            dateTakenInSeconds = newData.dateTakenInSeconds;
+            dateModifiedInSeconds = newData.dateModifiedInSeconds;
             path = newData.path;
             width = newData.width;
             height = newData.height;
diff --git a/src/com/android/camera/data/MediaDetails.java b/src/com/android/camera/data/MediaDetails.java
new file mode 100644
index 0000000..cb75273
--- /dev/null
+++ b/src/com/android/camera/data/MediaDetails.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.data;
+
+import android.util.Log;
+
+import com.android.camera2.R;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+public class MediaDetails implements Iterable<Entry<Integer, Object>> {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MediaDetails";
+
+    private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>();
+    private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>();
+
+    public static final int INDEX_TITLE = 1;
+    public static final int INDEX_DESCRIPTION = 2;
+    public static final int INDEX_DATETIME = 3;
+    public static final int INDEX_LOCATION = 4;
+    public static final int INDEX_WIDTH = 5;
+    public static final int INDEX_HEIGHT = 6;
+    public static final int INDEX_ORIENTATION = 7;
+    public static final int INDEX_DURATION = 8;
+    public static final int INDEX_MIMETYPE = 9;
+    public static final int INDEX_SIZE = 10;
+
+    // for EXIF
+    public static final int INDEX_MAKE = 100;
+    public static final int INDEX_MODEL = 101;
+    public static final int INDEX_FLASH = 102;
+    public static final int INDEX_FOCAL_LENGTH = 103;
+    public static final int INDEX_WHITE_BALANCE = 104;
+    public static final int INDEX_APERTURE = 105;
+    public static final int INDEX_SHUTTER_SPEED = 106;
+    public static final int INDEX_EXPOSURE_TIME = 107;
+    public static final int INDEX_ISO = 108;
+
+    // Put this last because it may be long.
+    public static final int INDEX_PATH = 200;
+
+    public static class FlashState {
+        private static int FLASH_FIRED_MASK = 1;
+        private static int FLASH_RETURN_MASK = 2 | 4;
+        private static int FLASH_MODE_MASK = 8 | 16;
+        private static int FLASH_FUNCTION_MASK = 32;
+        private static int FLASH_RED_EYE_MASK = 64;
+        private int mState;
+
+        public FlashState(int state) {
+            mState = state;
+        }
+
+        public boolean isFlashFired() {
+            return (mState & FLASH_FIRED_MASK) != 0;
+        }
+    }
+
+    public void addDetail(int index, Object value) {
+        mDetails.put(index, value);
+    }
+
+    public Object getDetail(int index) {
+        return mDetails.get(index);
+    }
+
+    public int size() {
+        return mDetails.size();
+    }
+
+    @Override
+    public Iterator<Entry<Integer, Object>> iterator() {
+        return mDetails.entrySet().iterator();
+    }
+
+    public void setUnit(int index, int unit) {
+        mUnits.put(index, unit);
+    }
+
+    public boolean hasUnit(int index) {
+        return mUnits.containsKey(index);
+    }
+
+    public int getUnit(int index) {
+        return mUnits.get(index);
+    }
+
+    private static void setExifData(MediaDetails details, ExifTag tag,
+            int key) {
+        if (tag != null) {
+            String value = null;
+            int type = tag.getDataType();
+            if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) {
+                value = String.valueOf(tag.getValueAsRational(0).toDouble());
+            } else if (type == ExifTag.TYPE_ASCII) {
+                value = tag.getValueAsString();
+            } else {
+                value = String.valueOf(tag.forceGetValueAsLong(0));
+            }
+            if (key == MediaDetails.INDEX_FLASH) {
+                MediaDetails.FlashState state = new MediaDetails.FlashState(
+                        Integer.valueOf(value.toString()));
+                details.addDetail(key, state);
+            } else {
+                details.addDetail(key, value);
+            }
+        }
+    }
+
+    /**
+     * Extracts data from the EXIF of the given file and stores it in the
+     * MediaDetails instance.
+     */
+    public static void extractExifInfo(MediaDetails details, String filePath) {
+        ExifInterface exif = new ExifInterface();
+        try {
+            exif.readExif(filePath);
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Could not find file to read exif: " + filePath, e);
+        } catch (IOException e) {
+            Log.w(TAG, "Could not read exif from file: " + filePath, e);
+        }
+
+        setExifData(details, exif.getTag(ExifInterface.TAG_FLASH),
+                MediaDetails.INDEX_FLASH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH),
+                MediaDetails.INDEX_WIDTH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH),
+                MediaDetails.INDEX_HEIGHT);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MAKE),
+                MediaDetails.INDEX_MAKE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MODEL),
+                MediaDetails.INDEX_MODEL);
+        setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE),
+                MediaDetails.INDEX_APERTURE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS),
+                MediaDetails.INDEX_ISO);
+        setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE),
+                MediaDetails.INDEX_WHITE_BALANCE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME),
+                MediaDetails.INDEX_EXPOSURE_TIME);
+        ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH);
+        if (focalTag != null) {
+            details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
+                    focalTag.getValueAsRational(0).toDouble());
+            details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
+        }
+    }
+}
diff --git a/src/com/android/camera/data/SimpleViewData.java b/src/com/android/camera/data/SimpleViewData.java
index d6fc1c4..8801599 100644
--- a/src/com/android/camera/data/SimpleViewData.java
+++ b/src/com/android/camera/data/SimpleViewData.java
@@ -145,4 +145,9 @@
     public boolean canSwipeInFullScreen() {
         return true;
     }
+
+    @Override
+    public MediaDetails getMediaDetails(Context context) {
+        return null;
+    }
 }
diff --git a/src/com/android/camera/ui/DetailsDialog.java b/src/com/android/camera/ui/DetailsDialog.java
new file mode 100644
index 0000000..cc0130d
--- /dev/null
+++ b/src/com/android/camera/ui/DetailsDialog.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.camera.data.MediaDetails;
+import com.android.camera2.R;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+/**
+ * Displays details (such as Exif) of a local media item.
+ */
+public class DetailsDialog {
+
+    /**
+     * Creates a dialog for showing media data.
+     *
+     * @param context the Android context.
+     * @param mediaDetails the media details to display.
+     * @return A dialog that can be made visible to show the media details.
+     */
+    public static Dialog create(Context context, MediaDetails mediaDetails) {
+        ListView detailsList = (ListView) LayoutInflater.from(context).inflate(
+                R.layout.details_list, null, false);
+        detailsList.setAdapter(new DetailsAdapter(context, mediaDetails));
+
+        final AlertDialog.Builder builder =
+                new AlertDialog.Builder(context);
+        return builder.setTitle(R.string.details).setView(detailsList)
+                .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        dialog.dismiss();
+                    }
+                }).create();
+    }
+
+    /**
+     * An adapter for feeding a details list view with the contents of a
+     * {@link MediaDetails} instance.
+     */
+    private static class DetailsAdapter extends BaseAdapter {
+        private final Context mContext;
+        private final MediaDetails mMediaDetails;
+        private final ArrayList<String> mItems;
+        private int mLocationIndex;
+        private int mWidthIndex = -1;
+        private int mHeightIndex = -1;
+
+        public DetailsAdapter(Context context, MediaDetails details) {
+            mContext = context;
+            mMediaDetails = details;
+            mItems = new ArrayList<String>(details.size());
+            mLocationIndex = -1;
+            setDetails(context, details);
+        }
+
+        private void setDetails(Context context, MediaDetails details) {
+            boolean resolutionIsValid = true;
+            String path = null;
+            for (Entry<Integer, Object> detail : details) {
+                String value;
+                switch (detail.getKey()) {
+                    // TODO: Resolve address asynchronously.
+                    case MediaDetails.INDEX_SIZE: {
+                        value = Formatter.formatFileSize(
+                                context, (Long) detail.getValue());
+                        break;
+                    }
+                    case MediaDetails.INDEX_WHITE_BALANCE: {
+                        value = "1".equals(detail.getValue())
+                                ? context.getString(R.string.manual)
+                                : context.getString(R.string.auto);
+                        break;
+                    }
+                    case MediaDetails.INDEX_FLASH: {
+                        MediaDetails.FlashState flash =
+                                (MediaDetails.FlashState) detail.getValue();
+                        // TODO: camera doesn't fill in the complete values,
+                        // show more information when it is fixed.
+                        if (flash.isFlashFired()) {
+                            value = context.getString(R.string.flash_on);
+                        } else {
+                            value = context.getString(R.string.flash_off);
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_EXPOSURE_TIME: {
+                        value = (String) detail.getValue();
+                        double time = Double.valueOf(value);
+                        if (time < 1.0f) {
+                            value = String.format("1/%d", (int) (0.5f + 1 / time));
+                        } else {
+                            int integer = (int) time;
+                            time -= integer;
+                            value = String.valueOf(integer) + "''";
+                            if (time > 0.0001) {
+                                value += String.format(" 1/%d", (int) (0.5f + 1 / time));
+                            }
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_WIDTH:
+                        mWidthIndex = mItems.size();
+                        value = detail.getValue().toString();
+                        if (value.equalsIgnoreCase("0")) {
+                            value = context.getString(R.string.unknown);
+                            resolutionIsValid = false;
+                        }
+                        break;
+                    case MediaDetails.INDEX_HEIGHT: {
+                        mHeightIndex = mItems.size();
+                        value = detail.getValue().toString();
+                        if (value.equalsIgnoreCase("0")) {
+                            value = context.getString(R.string.unknown);
+                            resolutionIsValid = false;
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_PATH:
+                        // Get the path and then fall through to the default
+                        // case
+                        path = detail.getValue().toString();
+                    default: {
+                        Object valueObj = detail.getValue();
+                        // This shouldn't happen, log its key to help us
+                        // diagnose the problem.
+                        if (valueObj == null) {
+                            fail("%s's value is Null",
+                                    getDetailsName(context,
+                                            detail.getKey()));
+                        }
+                        value = valueObj.toString();
+                    }
+                }
+                int key = detail.getKey();
+                if (details.hasUnit(key)) {
+                    value = String.format("%s: %s %s", getDetailsName(
+                            context, key), value, context.getString(details.getUnit(key)));
+                } else {
+                    value = String.format("%s: %s", getDetailsName(
+                            context, key), value);
+                }
+                mItems.add(value);
+                if (!resolutionIsValid) {
+                    resolveResolution(path);
+                }
+            }
+        }
+
+        public void resolveResolution(String path) {
+            Bitmap bitmap = BitmapFactory.decodeFile(path);
+            if (bitmap == null)
+                return;
+            onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight());
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mMediaDetails.getDetail(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            TextView tv;
+            if (convertView == null) {
+                tv = (TextView) LayoutInflater.from(mContext).inflate(
+                        R.layout.details, parent, false);
+            } else {
+                tv = (TextView) convertView;
+            }
+            tv.setText(mItems.get(position));
+            return tv;
+        }
+
+        public void onResolutionAvailable(int width, int height) {
+            if (width == 0 || height == 0)
+                return;
+            // Update the resolution with the new width and height
+            String widthString = String.format("%s: %d",
+                    getDetailsName(
+                            mContext, MediaDetails.INDEX_WIDTH), width);
+            String heightString = String.format("%s: %d",
+                    getDetailsName(
+                            mContext, MediaDetails.INDEX_HEIGHT), height);
+            mItems.set(mWidthIndex, String.valueOf(widthString));
+            mItems.set(mHeightIndex, String.valueOf(heightString));
+            notifyDataSetChanged();
+        }
+    }
+
+    public static String getDetailsName(Context context, int key) {
+        switch (key) {
+            case MediaDetails.INDEX_TITLE:
+                return context.getString(R.string.title);
+            case MediaDetails.INDEX_DESCRIPTION:
+                return context.getString(R.string.description);
+            case MediaDetails.INDEX_DATETIME:
+                return context.getString(R.string.time);
+            case MediaDetails.INDEX_LOCATION:
+                return context.getString(R.string.location);
+            case MediaDetails.INDEX_PATH:
+                return context.getString(R.string.path);
+            case MediaDetails.INDEX_WIDTH:
+                return context.getString(R.string.width);
+            case MediaDetails.INDEX_HEIGHT:
+                return context.getString(R.string.height);
+            case MediaDetails.INDEX_ORIENTATION:
+                return context.getString(R.string.orientation);
+            case MediaDetails.INDEX_DURATION:
+                return context.getString(R.string.duration);
+            case MediaDetails.INDEX_MIMETYPE:
+                return context.getString(R.string.mimetype);
+            case MediaDetails.INDEX_SIZE:
+                return context.getString(R.string.file_size);
+            case MediaDetails.INDEX_MAKE:
+                return context.getString(R.string.maker);
+            case MediaDetails.INDEX_MODEL:
+                return context.getString(R.string.model);
+            case MediaDetails.INDEX_FLASH:
+                return context.getString(R.string.flash);
+            case MediaDetails.INDEX_APERTURE:
+                return context.getString(R.string.aperture);
+            case MediaDetails.INDEX_FOCAL_LENGTH:
+                return context.getString(R.string.focal_length);
+            case MediaDetails.INDEX_WHITE_BALANCE:
+                return context.getString(R.string.white_balance);
+            case MediaDetails.INDEX_EXPOSURE_TIME:
+                return context.getString(R.string.exposure_time);
+            case MediaDetails.INDEX_ISO:
+                return context.getString(R.string.iso);
+            default:
+                return "Unknown key" + key;
+        }
+    }
+
+    /**
+     * Throw an assertion error wit the given message.
+     *
+     * @param message the message, can contain placeholders.
+     * @param args if he message contains placeholders, these values will be
+     *            used to fill them.
+     */
+    private static void fail(String message, Object... args) {
+        throw new AssertionError(
+                args.length == 0 ? message : String.format(message, args));
+    }
+}