1 package com.android.gallery3d.data;
2 
3 import android.annotation.TargetApi;
4 import android.content.ContentResolver;
5 import android.database.Cursor;
6 import android.net.Uri;
7 import android.provider.MediaStore.Files;
8 import android.provider.MediaStore.Files.FileColumns;
9 import android.provider.MediaStore.Images;
10 import android.provider.MediaStore.Images.ImageColumns;
11 import android.provider.MediaStore.Video;
12 import android.util.Log;
13 
14 import com.android.gallery3d.common.ApiHelper;
15 import com.android.gallery3d.common.Utils;
16 import com.android.gallery3d.util.ThreadPool.JobContext;
17 
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Comparator;
21 import java.util.HashMap;
22 
23 class BucketHelper {
24 
25     private static final String TAG = "BucketHelper";
26     private static final String EXTERNAL_MEDIA = "external";
27 
28     // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
29     // name of where an image or video is in. BUCKET_ID is a hash of the path
30     // name of that directory (see computeBucketValues() in MediaProvider for
31     // details). MEDIA_TYPE is video, image, audio, etc.
32     //
33     // The "albums" are not explicitly recorded in the database, but each image
34     // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
35     // "album" to be the collection of images/videos which have the same value
36     // for the two columns.
37     //
38     // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to
39     // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE).
40     // In the meantime sort them by the timestamp of the latest image/video in
41     // each of the album.
42     //
43     // The order of columns below is important: it must match to the index in
44     // MediaStore.
45     private static final String[] PROJECTION_BUCKET = {
46             ImageColumns.BUCKET_ID,
47             FileColumns.MEDIA_TYPE,
48             ImageColumns.BUCKET_DISPLAY_NAME};
49 
50     // The indices should match the above projections.
51     private static final int INDEX_BUCKET_ID = 0;
52     private static final int INDEX_MEDIA_TYPE = 1;
53     private static final int INDEX_BUCKET_NAME = 2;
54 
55     // We want to order the albums by reverse chronological order. We abuse the
56     // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
57     // The template for "WHERE" parameter is like:
58     //    SELECT ... FROM ... WHERE (%s)
59     // and we make it look like:
60     //    SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
61     // The "(1)" means true. The "1,(2)" means the first two columns specified
62     // after SELECT. Note that because there is a ")" in the template, we use
63     // "(2" to match it.
64     private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2";
65 
66     private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
67 
68     // Before HoneyComb there is no Files table. Thus, we need to query the
69     // bucket info from the Images and Video tables and then merge them
70     // together.
71     //
72     // A bucket can exist in both tables. In this case, we need to find the
73     // latest timestamp from the two tables and sort ourselves. So we add the
74     // MAX(date_taken) to the projection and remove the media_type since we
75     // already know the media type from the table we query from.
76     private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
77             ImageColumns.BUCKET_ID,
78             "MAX(datetaken)",
79             ImageColumns.BUCKET_DISPLAY_NAME};
80 
81     // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
82     // PROJECTION_BUCKET so we can reuse the values defined before.
83     private static final int INDEX_DATE_TAKEN = 1;
84 
85     // When query from the Images or Video tables, we only need to group by BUCKET_ID.
86     private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1";
87 
loadBucketEntries( JobContext jc, ContentResolver resolver, int type)88     public static BucketEntry[] loadBucketEntries(
89             JobContext jc, ContentResolver resolver, int type) {
90         if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
91             return loadBucketEntriesFromFilesTable(jc, resolver, type);
92         } else {
93             return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
94         }
95     }
96 
updateBucketEntriesFromTable(JobContext jc, ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets)97     private static void updateBucketEntriesFromTable(JobContext jc,
98             ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
99         Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
100                 BUCKET_GROUP_BY_IN_ONE_TABLE, null, null);
101         if (cursor == null) {
102             Log.w(TAG, "cannot open media database: " + tableUri);
103             return;
104         }
105         try {
106             while (cursor.moveToNext()) {
107                 int bucketId = cursor.getInt(INDEX_BUCKET_ID);
108                 int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
109                 BucketEntry entry = buckets.get(bucketId);
110                 if (entry == null) {
111                     entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
112                     buckets.put(bucketId, entry);
113                     entry.dateTaken = dateTaken;
114                 } else {
115                     entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
116                 }
117             }
118         } finally {
119             Utils.closeSilently(cursor);
120         }
121     }
122 
loadBucketEntriesFromImagesAndVideoTable( JobContext jc, ContentResolver resolver, int type)123     private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
124             JobContext jc, ContentResolver resolver, int type) {
125         HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
126         if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
127             updateBucketEntriesFromTable(
128                     jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
129         }
130         if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
131             updateBucketEntriesFromTable(
132                     jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
133         }
134         BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
135         Arrays.sort(entries, new Comparator<BucketEntry>() {
136             @Override
137             public int compare(BucketEntry a, BucketEntry b) {
138                 // sorted by dateTaken in descending order
139                 return b.dateTaken - a.dateTaken;
140             }
141         });
142         return entries;
143     }
144 
loadBucketEntriesFromFilesTable( JobContext jc, ContentResolver resolver, int type)145     private static BucketEntry[] loadBucketEntriesFromFilesTable(
146             JobContext jc, ContentResolver resolver, int type) {
147         Uri uri = getFilesContentUri();
148 
149         Cursor cursor = resolver.query(uri,
150                 PROJECTION_BUCKET, BUCKET_GROUP_BY,
151                 null, BUCKET_ORDER_BY);
152         if (cursor == null) {
153             Log.w(TAG, "cannot open local database: " + uri);
154             return new BucketEntry[0];
155         }
156         ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
157         int typeBits = 0;
158         if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
159             typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
160         }
161         if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
162             typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
163         }
164         try {
165             while (cursor.moveToNext()) {
166                 if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
167                     BucketEntry entry = new BucketEntry(
168                             cursor.getInt(INDEX_BUCKET_ID),
169                             cursor.getString(INDEX_BUCKET_NAME));
170                     if (!buffer.contains(entry)) {
171                         buffer.add(entry);
172                     }
173                 }
174                 if (jc.isCancelled()) return null;
175             }
176         } finally {
177             Utils.closeSilently(cursor);
178         }
179         return buffer.toArray(new BucketEntry[buffer.size()]);
180     }
181 
getBucketNameInTable( ContentResolver resolver, Uri tableUri, int bucketId)182     private static String getBucketNameInTable(
183             ContentResolver resolver, Uri tableUri, int bucketId) {
184         String selectionArgs[] = new String[] {String.valueOf(bucketId)};
185         Uri uri = tableUri.buildUpon()
186                 .appendQueryParameter("limit", "1")
187                 .build();
188         Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
189                 "bucket_id = ?", selectionArgs, null);
190         try {
191             if (cursor != null && cursor.moveToNext()) {
192                 return cursor.getString(INDEX_BUCKET_NAME);
193             }
194         } finally {
195             Utils.closeSilently(cursor);
196         }
197         return null;
198     }
199 
200     @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
getFilesContentUri()201     private static Uri getFilesContentUri() {
202         return Files.getContentUri(EXTERNAL_MEDIA);
203     }
204 
getBucketName(ContentResolver resolver, int bucketId)205     public static String getBucketName(ContentResolver resolver, int bucketId) {
206         if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
207             String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
208             return result == null ? "" : result;
209         } else {
210             String result = getBucketNameInTable(
211                     resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
212             if (result != null) return result;
213             result = getBucketNameInTable(
214                     resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
215             return result == null ? "" : result;
216         }
217     }
218 
219     public static class BucketEntry {
220         public String bucketName;
221         public int bucketId;
222         public int dateTaken;
223 
BucketEntry(int id, String name)224         public BucketEntry(int id, String name) {
225             bucketId = id;
226             bucketName = Utils.ensureNotNull(name);
227         }
228 
229         @Override
hashCode()230         public int hashCode() {
231             return bucketId;
232         }
233 
234         @Override
equals(Object object)235         public boolean equals(Object object) {
236             if (!(object instanceof BucketEntry)) return false;
237             BucketEntry entry = (BucketEntry) object;
238             return bucketId == entry.bucketId;
239         }
240     }
241 }
242