Merge "Make sure startAnimation won't be called after onPause." into ics-mr1
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
index c34e896..2192cf8 100644
--- a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -31,8 +31,8 @@
 
 public class BitmapUtils {
     private static final String TAG = "BitmapUtils";
-    public static final int UNCONSTRAINED = -1;
     private static final int COMPRESS_JPEG_QUALITY = 90;
+    public static final int UNCONSTRAINED = -1;
 
     private BitmapUtils(){}
 
diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java
index 32f9023..569e5a4 100644
--- a/src/com/android/gallery3d/data/ClusterAlbum.java
+++ b/src/com/android/gallery3d/data/ClusterAlbum.java
@@ -24,6 +24,7 @@
     private String mName = "";
     private DataManager mDataManager;
     private MediaSet mClusterAlbumSet;
+    private MediaItem mCover;
 
     public ClusterAlbum(Path path, DataManager dataManager,
             MediaSet clusterAlbumSet) {
@@ -33,6 +34,15 @@
         mClusterAlbumSet.addContentListener(this);
     }
 
+    public void setCoverMediaItem(MediaItem cover) {
+        mCover = cover;
+    }
+
+    @Override
+    public MediaItem getCoverMediaItem() {
+        return mCover != null ? mCover : super.getCoverMediaItem();
+    }
+
     void setMediaItems(ArrayList<Path> paths) {
         mPaths = paths;
     }
diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java
index 5b0569a..16501c2 100644
--- a/src/com/android/gallery3d/data/ClusterAlbumSet.java
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -117,6 +117,7 @@
             }
             album.setMediaItems(clustering.getCluster(i));
             album.setName(childName);
+            album.setCoverMediaItem(clustering.getClusterCover(i));
             mAlbums.add(album);
         }
     }
diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java
index 542dda2..4072bf5 100644
--- a/src/com/android/gallery3d/data/Clustering.java
+++ b/src/com/android/gallery3d/data/Clustering.java
@@ -23,4 +23,7 @@
     public abstract int getNumberOfClusters();
     public abstract ArrayList<Path> getCluster(int index);
     public abstract String getClusterName(int index);
+    public MediaItem getClusterCover(int index) {
+        return null;
+    }
 }
diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java
index cc1a2d3..c5fd131 100644
--- a/src/com/android/gallery3d/data/Face.java
+++ b/src/com/android/gallery3d/data/Face.java
@@ -16,16 +16,41 @@
 
 package com.android.gallery3d.data;
 
+import android.graphics.Rect;
+
 import com.android.gallery3d.common.Utils;
 
+import java.util.StringTokenizer;
+
 public class Face implements Comparable<Face> {
     private String mName;
     private String mPersonId;
+    private Rect mPosition;
 
-    public Face(String name, String personId) {
+    public Face(String name, String personId, String rect) {
         mName = name;
         mPersonId = personId;
-        Utils.assertTrue(mName != null && mPersonId != null);
+        Utils.assertTrue(mName != null && mPersonId != null && rect != null);
+        StringTokenizer tokenizer = new StringTokenizer(rect);
+        mPosition = new Rect();
+        while (tokenizer.hasMoreElements()) {
+            mPosition.left = Integer.parseInt(tokenizer.nextToken());
+            mPosition.top = Integer.parseInt(tokenizer.nextToken());
+            mPosition.right = Integer.parseInt(tokenizer.nextToken());
+            mPosition.bottom = Integer.parseInt(tokenizer.nextToken());
+        }
+    }
+
+    public Rect getPosition() {
+        return mPosition;
+    }
+
+    public int getWidth() {
+        return mPosition.right - mPosition.left;
+    }
+
+    public int getHeight() {
+        return mPosition.bottom - mPosition.top;
     }
 
     public String getName() {
@@ -45,12 +70,7 @@
         return false;
     }
 
-    @Override
-    public int hashCode() {
-        return mPersonId.hashCode();
-    }
-
     public int compareTo(Face another) {
-        return mPersonId.compareTo(another.mPersonId);
+        return mName.compareTo(another.mName);
     }
 }
diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java
index 6ed73b5..126286c 100644
--- a/src/com/android/gallery3d/data/FaceClustering.java
+++ b/src/com/android/gallery3d/data/FaceClustering.java
@@ -16,79 +16,126 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.picasasource.PicasaSource;
 
 import java.util.ArrayList;
-import java.util.Map;
 import java.util.TreeMap;
 
 public class FaceClustering extends Clustering {
     @SuppressWarnings("unused")
     private static final String TAG = "FaceClustering";
 
-    private ArrayList<ArrayList<Path>> mClusters;
-    private String[] mNames;
+    private FaceCluster[] mClusters;
     private String mUntaggedString;
+    private Context mContext;
+
+    private class FaceCluster {
+        ArrayList<Path> mPaths = new ArrayList<Path>();
+        String mName;
+        MediaItem mCoverItem;
+        Rect mCoverRegion;
+        int mCoverFaceIndex;
+
+        public FaceCluster(String name) {
+            mName = name;
+        }
+
+        public void add(MediaItem item, int faceIndex) {
+            Path path = item.getPath();
+            mPaths.add(path);
+            Face[] faces = item.getFaces();
+            if (faces != null) {
+                Face face = faces[faceIndex];
+                if (mCoverItem == null) {
+                    mCoverItem = item;
+                    mCoverRegion = face.getPosition();
+                    mCoverFaceIndex = faceIndex;
+                } else {
+                    Rect region = face.getPosition();
+                    if (mCoverRegion.width() < region.width() &&
+                            mCoverRegion.height() < region.height()) {
+                        mCoverItem = item;
+                        mCoverRegion = face.getPosition();
+                        mCoverFaceIndex = faceIndex;
+                    }
+                }
+            }
+        }
+
+        public int size() {
+            return mPaths.size();
+        }
+
+        public MediaItem getCover() {
+            if (mCoverItem != null) {
+                if (PicasaSource.isPicasaImage(mCoverItem)) {
+                    return PicasaSource.getFaceItem(mContext, mCoverItem, mCoverFaceIndex);
+                } else {
+                    return mCoverItem;
+                }
+            }
+            return null;
+        }
+    }
 
     public FaceClustering(Context context) {
         mUntaggedString = context.getResources().getString(R.string.untagged);
+        mContext = context;
     }
 
     @Override
     public void run(MediaSet baseSet) {
-        final TreeMap<Face, ArrayList<Path>> map =
-                new TreeMap<Face, ArrayList<Path>>();
-        final ArrayList<Path> untagged = new ArrayList<Path>();
+        final TreeMap<Face, FaceCluster> map =
+                new TreeMap<Face, FaceCluster>();
+        final FaceCluster untagged = new FaceCluster(mUntaggedString);
 
         baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
             public void consume(int index, MediaItem item) {
-                Path path = item.getPath();
-
                 Face[] faces = item.getFaces();
                 if (faces == null || faces.length == 0) {
-                    untagged.add(path);
+                    untagged.add(item, -1);
                     return;
                 }
                 for (int j = 0; j < faces.length; j++) {
-                    Face key = faces[j];
-                    ArrayList<Path> list = map.get(key);
-                    if (list == null) {
-                        list = new ArrayList<Path>();
-                        map.put(key, list);
+                    Face face = faces[j];
+                    FaceCluster cluster = map.get(face);
+                    if (cluster == null) {
+                        cluster = new FaceCluster(face.getName());
+                        map.put(face, cluster);
                     }
-                    list.add(path);
+                    cluster.add(item, j);
                 }
             }
         });
 
         int m = map.size();
-        mClusters = new ArrayList<ArrayList<Path>>();
-        mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)];
-        int i = 0;
-        for (Map.Entry<Face, ArrayList<Path>> entry : map.entrySet()) {
-            mNames[i++] = entry.getKey().getName();
-            mClusters.add(entry.getValue());
-        }
+        mClusters = map.values().toArray(new FaceCluster[m + ((untagged.size() > 0) ? 1 : 0)]);
         if (untagged.size() > 0) {
-            mNames[i++] = mUntaggedString;
-            mClusters.add(untagged);
+            mClusters[m] = untagged;
         }
     }
 
     @Override
     public int getNumberOfClusters() {
-        return mClusters.size();
+        return mClusters.length;
     }
 
     @Override
     public ArrayList<Path> getCluster(int index) {
-        return mClusters.get(index);
+        return mClusters[index].mPaths;
     }
 
     @Override
     public String getClusterName(int index) {
-        return mNames[index];
+        return mClusters[index].mName;
+    }
+
+    @Override
+    public MediaItem getClusterCover(int index) {
+        return mClusters[index].getCover();
     }
 }
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index e70b2ee..fa3ece3 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -40,9 +40,6 @@
 
 // LocalImage represents an image in the local storage.
 public class LocalImage extends LocalMediaItem {
-    private static final int THUMBNAIL_TARGET_SIZE = 640;
-    private static final int MICROTHUMBNAIL_TARGET_SIZE = 200;
-
     private static final String TAG = "LocalImage";
 
     static final Path ITEM_PATH = Path.fromString("/local/image/item");
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
index a0c6d8c..1361232 100644
--- a/src/com/android/gallery3d/data/MediaItem.java
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -28,10 +28,16 @@
     public static final int TYPE_THUMBNAIL = 1;
     public static final int TYPE_MICROTHUMBNAIL = 2;
 
+    public static final int THUMBNAIL_TARGET_SIZE = 640;
+    public static final int MICROTHUMBNAIL_TARGET_SIZE = 200;
+    public static final int CACHED_IMAGE_QUALITY = 95;
+
     public static final int IMAGE_READY = 0;
     public static final int IMAGE_WAIT = 1;
     public static final int IMAGE_ERROR = -1;
 
+    public static final String MIME_TYPE_JPEG = "image/jpeg";
+
     // TODO: fix default value for latlng and change this.
     public static final double INVALID_LATLNG = 0f;
 
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
index b8691df..8f91cc0 100644
--- a/src/com/android/gallery3d/data/UriImage.java
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -70,7 +70,7 @@
             String extension =
                     MimeTypeMap.getFileExtensionFromUrl(uri.toString());
             String type = MimeTypeMap.getSingleton()
-                    .getMimeTypeFromExtension(extension);
+                    .getMimeTypeFromExtension(extension.toLowerCase());
             if (type != null) return type;
         }
         return mApplication.getContentResolver().getType(uri);
@@ -106,7 +106,7 @@
                 || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
                 || ContentResolver.SCHEME_FILE.equals(scheme)) {
             try {
-                if (mContentType.equalsIgnoreCase("image/jpeg")) {
+                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
                     InputStream is = mApplication.getContentResolver()
                             .openInputStream(mUri);
                     mRotation = Exif.getOrientation(is);
@@ -129,7 +129,7 @@
                     Log.w(TAG, "download failed " + url);
                     return STATE_ERROR;
                 }
-                if (mContentType.equalsIgnoreCase("image/jpeg")) {
+                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
                     InputStream is = new FileInputStream(mCacheEntry.cacheFile);
                     mRotation = Exif.getOrientation(is);
                     Utils.closeSilently(is);
@@ -253,7 +253,9 @@
             details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
             details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
         }
-        details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
+        if (mContentType != null) {
+            details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
+        }
         if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
             String filePath = mUri.getPath();
             details.addDetail(MediaDetails.INDEX_PATH, filePath);
diff --git a/src/com/android/gallery3d/photoeditor/EffectsBar.java b/src/com/android/gallery3d/photoeditor/EffectsBar.java
index b4857e6..4075404 100644
--- a/src/com/android/gallery3d/photoeditor/EffectsBar.java
+++ b/src/com/android/gallery3d/photoeditor/EffectsBar.java
@@ -128,13 +128,12 @@
 
     private boolean exitActiveEffect(final Runnable runnableOnDone) {
         if (activeEffect != null) {
-            final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(
-                    (ViewGroup) getRootView().findViewById(R.id.toolbar));
+            SpinnerProgressDialog.showDialog();
             activeEffect.end(new Runnable() {
 
                 @Override
                 public void run() {
-                    progressDialog.dismiss();
+                    SpinnerProgressDialog.dismissDialog();
                     View fullscreenTool = getRootView().findViewById(R.id.fullscreen_effect_tool);
                     if (fullscreenTool != null) {
                         ((ViewGroup) fullscreenTool.getParent()).removeView(fullscreenTool);
diff --git a/src/com/android/gallery3d/photoeditor/PhotoEditor.java b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
index dba7e62..8f3990b 100644
--- a/src/com/android/gallery3d/photoeditor/PhotoEditor.java
+++ b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
@@ -42,6 +42,7 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.photoeditor_main);
+        SpinnerProgressDialog.initialize((ViewGroup) findViewById(R.id.toolbar));
 
         Intent intent = getIntent();
         if (Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction())) {
@@ -74,12 +75,8 @@
         actionBar.setClickRunnable(R.id.action_bar_back, createBackRunnable());
     }
 
-    private SpinnerProgressDialog createProgressDialog() {
-        return SpinnerProgressDialog.show((ViewGroup) findViewById(R.id.toolbar));
-    }
-
     private void openPhoto() {
-        final SpinnerProgressDialog progressDialog = createProgressDialog();
+        SpinnerProgressDialog.showDialog();
         LoadScreennailTask.Callback callback = new LoadScreennailTask.Callback() {
 
             @Override
@@ -88,7 +85,7 @@
 
                     @Override
                     public void onDone() {
-                        progressDialog.dismiss();
+                        SpinnerProgressDialog.dismissDialog();
                         effectsBar.setEnabled(result != null);
                     }
                 });
@@ -106,12 +103,12 @@
 
                     @Override
                     public void run() {
-                        final SpinnerProgressDialog progressDialog = createProgressDialog();
+                        SpinnerProgressDialog.showDialog();
                         OnDoneCallback callback = new OnDoneCallback() {
 
                             @Override
                             public void onDone() {
-                                progressDialog.dismiss();
+                                SpinnerProgressDialog.dismissDialog();
                             }
                         };
                         if (undo) {
@@ -134,7 +131,7 @@
 
                     @Override
                     public void run() {
-                        final SpinnerProgressDialog progressDialog = createProgressDialog();
+                        SpinnerProgressDialog.showDialog();
                         filterStack.getOutputBitmap(new OnDoneBitmapCallback() {
 
                             @Override
@@ -143,7 +140,7 @@
 
                                     @Override
                                     public void onComplete(Uri result) {
-                                        progressDialog.dismiss();
+                                        SpinnerProgressDialog.dismissDialog();
                                         saveUri = result;
                                         actionBar.updateSave(saveUri == null);
                                     }
@@ -223,9 +220,10 @@
 
     @Override
     protected void onPause() {
-        // TODO: Close running progress dialogs as all pending operations will be paused.
         super.onPause();
         filterStack.onPause();
+        // Dismiss any running progress dialog as all operations are paused.
+        SpinnerProgressDialog.dismissDialog();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
index 207c2d1..065075e 100644
--- a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
+++ b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
@@ -29,46 +29,53 @@
 
 /**
  * Spinner model progress dialog that disables all tools for user interaction after it shows up and
- * and re-enables them after it dismisses.
+ * and re-enables them after it dismisses; this class along with all its methods should be accessed
+ * in only UI thread and allows only one instance at a time.
  */
 public class SpinnerProgressDialog extends Dialog {
 
-    private final ViewGroup toolbar;
+    private static ViewGroup toolbar;
+    private static SpinnerProgressDialog dialog;
     private final ArrayList<View> enabledTools = new ArrayList<View>();
 
-    public static SpinnerProgressDialog show(ViewGroup toolbar) {
-        SpinnerProgressDialog dialog = new SpinnerProgressDialog(toolbar);
-        dialog.setCancelable(false);
-        dialog.show();
-        return dialog;
+    public static void initialize(ViewGroup toolbar) {
+        SpinnerProgressDialog.toolbar = toolbar;
     }
 
-    private SpinnerProgressDialog(ViewGroup toolbar) {
-        super(toolbar.getContext(), R.style.SpinnerProgressDialog);
-
-        addContentView(new ProgressBar(toolbar.getContext()), new LayoutParams(
-                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
-
-        // Disable enabled tools when showing spinner progress dialog.
-        for (int i = 0; i < toolbar.getChildCount(); i++) {
-            View view = toolbar.getChildAt(i);
-            if (view.isEnabled()) {
-                enabledTools.add(view);
-                view.setEnabled(false);
+    public static void showDialog() {
+        // There should be only one progress dialog running at a time.
+        if (dialog == null) {
+            dialog = new SpinnerProgressDialog();
+            dialog.setCancelable(false);
+            dialog.show();
+            // Disable enabled tools when showing spinner progress dialog.
+            for (int i = 0; i < toolbar.getChildCount(); i++) {
+                View view = toolbar.getChildAt(i);
+                if (view.isEnabled()) {
+                    dialog.enabledTools.add(view);
+                    view.setEnabled(false);
+                }
             }
         }
-        this.toolbar = toolbar;
     }
 
-    @Override
-    public void dismiss() {
-        super.dismiss();
-        // Enable tools that were disabled by this spinner progress dialog.
-        for (View view : enabledTools) {
-            view.setEnabled(true);
+    public static void dismissDialog() {
+        if (dialog != null) {
+            dialog.dismiss();
+            // Enable tools that were disabled by this spinner progress dialog.
+            for (View view : dialog.enabledTools) {
+                view.setEnabled(true);
+            }
+            dialog = null;
         }
     }
 
+    private SpinnerProgressDialog() {
+        super(toolbar.getContext(), R.style.SpinnerProgressDialog);
+        addContentView(new ProgressBar(toolbar.getContext()), new LayoutParams(
+                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         super.onTouchEvent(event);
diff --git a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
index 4a4f786..a4f9621 100644
--- a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
+++ b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
@@ -17,6 +17,7 @@
 package com.android.gallery3d.picasasource;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MediaSource;
@@ -80,6 +81,10 @@
         }
     }
 
+    public static MediaItem getFaceItem(Context context, MediaItem item, int faceIndex) {
+        throw new UnsupportedOperationException();
+    }
+
     public static boolean isPicasaImage(MediaObject object) {
         return false;
     }