Don't use Files.getContentUri in previous API levels.

Change-Id: I4d096734d22c93b83b2bf8082450ad0d73ade837
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
index ac14d9b..54b0587 100644
--- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
+++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
@@ -146,6 +146,9 @@
     public static final boolean HAS_ACTION_BAR_SET_HOME_BUTTON_ENABLED =
             Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
 
+    public static final boolean HAS_MEDIA_PROVIDER_FILES_TABLE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
     private static boolean hasField(Class<?> klass, String fieldName) {
         try {
             klass.getDeclaredField(fieldName);
diff --git a/src/com/android/gallery3d/data/BucketHelper.java b/src/com/android/gallery3d/data/BucketHelper.java
new file mode 100644
index 0000000..71c0ad4
--- /dev/null
+++ b/src/com/android/gallery3d/data/BucketHelper.java
@@ -0,0 +1,241 @@
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+class BucketHelper {
+
+    private static final String TAG = "BucketHelper";
+    private static final String EXTERNAL_MEDIA = "external";
+
+    // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
+    // name of where an image or video is in. BUCKET_ID is a hash of the path
+    // name of that directory (see computeBucketValues() in MediaProvider for
+    // details). MEDIA_TYPE is video, image, audio, etc.
+    //
+    // The "albums" are not explicitly recorded in the database, but each image
+    // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
+    // "album" to be the collection of images/videos which have the same value
+    // for the two columns.
+    //
+    // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to
+    // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE).
+    // In the meantime sort them by the timestamp of the latest image/video in
+    // each of the album.
+    //
+    // The order of columns below is important: it must match to the index in
+    // MediaStore.
+    private static final String[] PROJECTION_BUCKET = {
+            ImageColumns.BUCKET_ID,
+            FileColumns.MEDIA_TYPE,
+            ImageColumns.BUCKET_DISPLAY_NAME};
+
+    // The indices should match the above projections.
+    private static final int INDEX_BUCKET_ID = 0;
+    private static final int INDEX_MEDIA_TYPE = 1;
+    private static final int INDEX_BUCKET_NAME = 2;
+
+    // We want to order the albums by reverse chronological order. We abuse the
+    // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
+    // The template for "WHERE" parameter is like:
+    //    SELECT ... FROM ... WHERE (%s)
+    // and we make it look like:
+    //    SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
+    // The "(1)" means true. The "1,(2)" means the first two columns specified
+    // after SELECT. Note that because there is a ")" in the template, we use
+    // "(2" to match it.
+    private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2";
+
+    private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
+
+    // Before HoneyComb there is no Files table. Thus, we need to query the
+    // bucket info from the Images and Video tables and then merge them
+    // together.
+    //
+    // A bucket can exist in both tables. In this case, we need to find the
+    // latest timestamp from the two tables and sort ourselves. So we add the
+    // MAX(date_taken) to the projection and remove the media_type since we
+    // already know the media type from the table we query from.
+    private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
+            ImageColumns.BUCKET_ID,
+            "MAX(date_taken)",
+            ImageColumns.BUCKET_DISPLAY_NAME};
+
+    // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
+    // PROJECTION_BUCKET so we can reuse the values defined before.
+    private static final int INDEX_DATE_TAKEN = 1;
+
+    // When query from the Images or Video tables, we only need to group by BUCKET_ID.
+    private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1";
+
+    public static BucketEntry[] loadBucketEntries(
+            JobContext jc, ContentResolver resolver, int type) {
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            return loadBucketEntriesFromFilesTable(jc, resolver, type);
+        } else {
+            return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
+        }
+    }
+
+    private static void updateBucketEntriesFromTable(JobContext jc,
+            ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
+        Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
+                BUCKET_GROUP_BY_IN_ONE_TABLE, null, null);
+        if (cursor == null) {
+            Log.w(TAG, "cannot open media database: " + tableUri);
+            return;
+        }
+        try {
+            while (cursor.moveToNext()) {
+                int bucketId = cursor.getInt(INDEX_BUCKET_ID);
+                int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
+                BucketEntry entry = buckets.get(bucketId);
+                if (entry == null) {
+                    entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
+                    buckets.put(bucketId, entry);
+                    entry.dateTaken = dateTaken;
+                } else {
+                    entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
+                }
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+    }
+
+    private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
+            JobContext jc, ContentResolver resolver, int type) {
+        HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
+        if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+            updateBucketEntriesFromTable(
+                    jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
+        }
+        if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+            updateBucketEntriesFromTable(
+                    jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
+        }
+        BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
+        Arrays.sort(entries, new Comparator<BucketEntry>() {
+            @Override
+            public int compare(BucketEntry a, BucketEntry b) {
+                // sorted by dateTaken in descending order
+                return b.dateTaken - a.dateTaken;
+            }
+        });
+        return entries;
+    }
+
+    private static BucketEntry[] loadBucketEntriesFromFilesTable(
+            JobContext jc, ContentResolver resolver, int type) {
+        Uri uri = getFilesContentUri();
+
+        Cursor cursor = resolver.query(uri,
+                PROJECTION_BUCKET, BUCKET_GROUP_BY,
+                null, BUCKET_ORDER_BY);
+        if (cursor == null) {
+            Log.w(TAG, "cannot open local database: " + uri);
+            return new BucketEntry[0];
+        }
+        ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
+        int typeBits = 0;
+        if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+            typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
+        }
+        if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+            typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
+        }
+        try {
+            while (cursor.moveToNext()) {
+                if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
+                    BucketEntry entry = new BucketEntry(
+                            cursor.getInt(INDEX_BUCKET_ID),
+                            cursor.getString(INDEX_BUCKET_NAME));
+                    if (!buffer.contains(entry)) {
+                        buffer.add(entry);
+                    }
+                }
+                if (jc.isCancelled()) return null;
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+        return buffer.toArray(new BucketEntry[buffer.size()]);
+    }
+
+    private static String getBucketNameInTable(
+            ContentResolver resolver, Uri tableUri, int bucketId) {
+        String selectionArgs[] = new String[] {String.valueOf(bucketId)};
+        Uri uri = tableUri.buildUpon()
+                .appendQueryParameter("limit", "1")
+                .build();
+        Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
+                "bucket_id = ?", selectionArgs, null);
+        try {
+            if (cursor != null && cursor.moveToNext()) {
+                return cursor.getString(INDEX_BUCKET_NAME);
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+        return null;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static Uri getFilesContentUri() {
+        return Files.getContentUri(EXTERNAL_MEDIA);
+    }
+
+    public static String getBucketName(ContentResolver resolver, int bucketId) {
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
+            return result == null ? "" : result;
+        } else {
+            String result = getBucketNameInTable(
+                    resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
+            if (result != null) return result;
+            result = getBucketNameInTable(
+                    resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
+            return result == null ? "" : result;
+        }
+    }
+
+    public static class BucketEntry {
+        public String bucketName;
+        public int bucketId;
+        public int dateTaken;
+
+        public BucketEntry(int id, String name) {
+            bucketId = id;
+            bucketName = Utils.ensureNotNull(name);
+        }
+
+        @Override
+        public int hashCode() {
+            return bucketId;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (!(object instanceof BucketEntry)) return false;
+            BucketEntry entry = (BucketEntry) object;
+            return bucketId == entry.bucketId;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java
index f45eebf..fed7040 100644
--- a/src/com/android/gallery3d/data/LocalAlbum.java
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -86,8 +86,8 @@
     public LocalAlbum(Path path, GalleryApp application, int bucketId,
             boolean isImage) {
         this(path, application, bucketId, isImage,
-                LocalAlbumSet.getBucketName(application.getContentResolver(),
-                bucketId));
+                BucketHelper.getBucketName(
+                application.getContentResolver(), bucketId));
     }
 
     @Override
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
index 07741ef..579a71e 100644
--- a/src/com/android/gallery3d/data/LocalAlbumSet.java
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -16,20 +16,14 @@
 
 package com.android.gallery3d.data;
 
-import android.content.ContentResolver;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Handler;
-import android.provider.MediaStore.Files;
-import android.provider.MediaStore.Files.FileColumns;
 import android.provider.MediaStore.Images;
-import android.provider.MediaStore.Images.ImageColumns;
 import android.provider.MediaStore.Video;
-import android.util.Log;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BucketHelper.BucketEntry;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.MediaSetUtils;
@@ -48,51 +42,10 @@
     public static final Path PATH_VIDEO = Path.fromString("/local/video");
 
     private static final String TAG = "LocalAlbumSet";
-    private static final String EXTERNAL_MEDIA = "external";
 
-    // The indices should match the following projections.
-    private static final int INDEX_BUCKET_ID = 0;
-    private static final int INDEX_MEDIA_TYPE = 1;
-    private static final int INDEX_BUCKET_NAME = 2;
-
-    private static final Uri mBaseUri = Files.getContentUri(EXTERNAL_MEDIA);
     private static final Uri mWatchUriImage = Images.Media.EXTERNAL_CONTENT_URI;
     private static final Uri mWatchUriVideo = Video.Media.EXTERNAL_CONTENT_URI;
 
-    // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
-    // name of where an image or video is in. BUCKET_ID is a hash of the path
-    // name of that directory (see computeBucketValues() in MediaProvider for
-    // details). MEDIA_TYPE is video, image, audio, etc.
-    //
-    // The "albums" are not explicitly recorded in the database, but each image
-    // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
-    // "album" to be the collection of images/videos which have the same value
-    // for the two columns.
-    //
-    // The goal of the query (used in loadSubMediaSets()) is to find all albums,
-    // that is, all unique values for (BUCKET_ID, MEDIA_TYPE). In the meantime
-    // sort them by the timestamp of the latest image/video in each of the album.
-    //
-    // The order of columns below is important: it must match to the index in
-    // MediaStore.
-    private static final String[] PROJECTION_BUCKET = {
-            ImageColumns.BUCKET_ID,
-            FileColumns.MEDIA_TYPE,
-            ImageColumns.BUCKET_DISPLAY_NAME };
-
-    // We want to order the albums by reverse chronological order. We abuse the
-    // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
-    // The template for "WHERE" parameter is like:
-    //    SELECT ... FROM ... WHERE (%s)
-    // and we make it look like:
-    //    SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
-    // The "(1)" means true. The "1,(2)" means the first two columns specified
-    // after SELECT. Note that because there is a ")" in the template, we use
-    // "(2" to match it.
-    private static final String BUCKET_GROUP_BY =
-            "1) GROUP BY 1,(2";
-    private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
-
     private final GalleryApp mApplication;
     private final int mType;
     private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
@@ -139,44 +92,6 @@
         return mName;
     }
 
-    private BucketEntry[] loadBucketEntries(JobContext jc) {
-        Uri uri = mBaseUri;
-
-        Log.v("DebugLoadingTime", "start quering media provider");
-        Cursor cursor = mApplication.getContentResolver().query(
-                uri, PROJECTION_BUCKET, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
-        if (cursor == null) {
-            Log.w(TAG, "cannot open local database: " + uri);
-            return new BucketEntry[0];
-        }
-        ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
-        int typeBits = 0;
-        if ((mType & MEDIA_TYPE_IMAGE) != 0) {
-            typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
-        }
-        if ((mType & MEDIA_TYPE_VIDEO) != 0) {
-            typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
-        }
-        try {
-            while (cursor.moveToNext()) {
-                if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
-                    BucketEntry entry = new BucketEntry(
-                            cursor.getInt(INDEX_BUCKET_ID),
-                            cursor.getString(INDEX_BUCKET_NAME));
-                    if (!buffer.contains(entry)) {
-                        buffer.add(entry);
-                    }
-                }
-                if (jc.isCancelled()) return null;
-            }
-            Log.v("DebugLoadingTime", "got " + buffer.size() + " buckets");
-        } finally {
-            cursor.close();
-        }
-        return buffer.toArray(new BucketEntry[buffer.size()]);
-    }
-
-
     private static int findBucket(BucketEntry entries[], int bucketId) {
         for (int i = 0, n = entries.length; i < n ; ++i) {
             if (entries[i].bucketId == bucketId) return i;
@@ -191,7 +106,8 @@
         public ArrayList<MediaSet> run(JobContext jc) {
             // Note: it will be faster if we only select media_type and bucket_id.
             //       need to test the performance if that is worth
-            BucketEntry[] entries = loadBucketEntries(jc);
+            BucketEntry[] entries = BucketHelper.loadBucketEntries(
+                    jc, mApplication.getContentResolver(), mType);
 
             if (jc.isCancelled()) return null;
 
@@ -239,28 +155,6 @@
         }
     }
 
-    public static String getBucketName(ContentResolver resolver, int bucketId) {
-        Uri uri = mBaseUri.buildUpon()
-                .appendQueryParameter("limit", "1")
-                .build();
-
-        Cursor cursor = resolver.query(
-                uri, PROJECTION_BUCKET, "bucket_id = ?",
-                new String[]{String.valueOf(bucketId)}, null);
-
-        if (cursor == null) {
-            Log.w(TAG, "query fail: " + uri);
-            return "";
-        }
-        try {
-            return cursor.moveToNext()
-                    ? cursor.getString(INDEX_BUCKET_NAME)
-                    : "";
-        } finally {
-            cursor.close();
-        }
-    }
-
     @Override
     public synchronized boolean isLoading() {
         return mIsLoading;
@@ -308,28 +202,6 @@
         mNotifierVideo.fakeChange();
     }
 
-    private static class BucketEntry {
-        public String bucketName;
-        public int bucketId;
-
-        public BucketEntry(int id, String name) {
-            bucketId = id;
-            bucketName = Utils.ensureNotNull(name);
-        }
-
-        @Override
-        public int hashCode() {
-            return bucketId;
-        }
-
-        @Override
-        public boolean equals(Object object) {
-            if (!(object instanceof BucketEntry)) return false;
-            BucketEntry entry = (BucketEntry) object;
-            return bucketId == entry.bucketId;
-        }
-    }
-
     // Circular shift the array range from a[i] to a[j] (inclusive). That is,
     // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
     private static <T> void circularShiftRight(T[] array, int i, int j) {
diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java
index 1e34e78..da3d511 100644
--- a/src/com/android/gallery3d/data/LocalMergeAlbum.java
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -19,6 +19,8 @@
 import android.net.Uri;
 import android.provider.MediaStore;
 
+import com.android.gallery3d.common.ApiHelper;
+
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -82,8 +84,18 @@
 
     @Override
     public Uri getContentUri() {
-        return MediaStore.Files.getContentUri("external").buildUpon().appendQueryParameter(
-                LocalSource.KEY_BUCKET_ID, String.valueOf(mBucketId)).build();
+        String bucketId = String.valueOf(mBucketId);
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            return MediaStore.Files.getContentUri("external").buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+                    .build();
+        } else {
+            // We don't have a single URL for a merged image before ICS
+            // So we used the image's URL as a substitute.
+            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+                    .build();
+        }
     }
 
     @Override