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     private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
56 
57     // Before HoneyComb there is no Files table. Thus, we need to query the
58     // bucket info from the Images and Video tables and then merge them
59     // together.
60     //
61     // A bucket can exist in both tables. In this case, we need to find the
62     // latest timestamp from the two tables and sort ourselves. So we add the
63     // MAX(date_taken) to the projection and remove the media_type since we
64     // already know the media type from the table we query from.
65     private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
66             ImageColumns.BUCKET_ID,
67             "MAX(datetaken)",
68             ImageColumns.BUCKET_DISPLAY_NAME};
69 
70     // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
71     // PROJECTION_BUCKET so we can reuse the values defined before.
72     private static final int INDEX_DATE_TAKEN = 1;
73 
loadBucketEntries( JobContext jc, ContentResolver resolver, int type)74     public static BucketEntry[] loadBucketEntries(
75             JobContext jc, ContentResolver resolver, int type) {
76         if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
77             return loadBucketEntriesFromFilesTable(jc, resolver, type);
78         } else {
79             return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
80         }
81     }
82 
updateBucketEntriesFromTable(JobContext jc, ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets)83     private static void updateBucketEntriesFromTable(JobContext jc,
84             ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
85         Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
86                 null, null, null);
87         if (cursor == null) {
88             Log.w(TAG, "cannot open media database: " + tableUri);
89             return;
90         }
91         try {
92             while (cursor.moveToNext()) {
93                 int bucketId = cursor.getInt(INDEX_BUCKET_ID);
94                 int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
95                 BucketEntry entry = buckets.get(bucketId);
96                 if (entry == null) {
97                     entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
98                     buckets.put(bucketId, entry);
99                     entry.dateTaken = dateTaken;
100                 } else {
101                     entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
102                 }
103             }
104         } finally {
105             Utils.closeSilently(cursor);
106         }
107     }
108 
loadBucketEntriesFromImagesAndVideoTable( JobContext jc, ContentResolver resolver, int type)109     private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
110             JobContext jc, ContentResolver resolver, int type) {
111         HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
112         if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
113             updateBucketEntriesFromTable(
114                     jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
115         }
116         if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
117             updateBucketEntriesFromTable(
118                     jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
119         }
120         BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
121         Arrays.sort(entries, new Comparator<BucketEntry>() {
122             @Override
123             public int compare(BucketEntry a, BucketEntry b) {
124                 // sorted by dateTaken in descending order
125                 return b.dateTaken - a.dateTaken;
126             }
127         });
128         return entries;
129     }
130 
loadBucketEntriesFromFilesTable( JobContext jc, ContentResolver resolver, int type)131     private static BucketEntry[] loadBucketEntriesFromFilesTable(
132             JobContext jc, ContentResolver resolver, int type) {
133         Uri uri = getFilesContentUri();
134 
135         Cursor cursor = resolver.query(uri, PROJECTION_BUCKET, null, null, null);
136         if (cursor == null) {
137             Log.w(TAG, "cannot open local database: " + uri);
138             return new BucketEntry[0];
139         }
140         ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
141         int typeBits = 0;
142         if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
143             typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
144         }
145         if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
146             typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
147         }
148         try {
149             while (cursor.moveToNext()) {
150                 if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
151                     BucketEntry entry = new BucketEntry(
152                             cursor.getInt(INDEX_BUCKET_ID),
153                             cursor.getString(INDEX_BUCKET_NAME));
154                     if (!buffer.contains(entry)) {
155                         buffer.add(entry);
156                     }
157                 }
158                 if (jc.isCancelled()) return null;
159             }
160         } finally {
161             Utils.closeSilently(cursor);
162         }
163         return buffer.toArray(new BucketEntry[buffer.size()]);
164     }
165 
getBucketNameInTable( ContentResolver resolver, Uri tableUri, int bucketId)166     private static String getBucketNameInTable(
167             ContentResolver resolver, Uri tableUri, int bucketId) {
168         String selectionArgs[] = new String[] {String.valueOf(bucketId)};
169         Uri uri = tableUri.buildUpon()
170                 .appendQueryParameter("limit", "1")
171                 .build();
172         Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
173                 "bucket_id = ?", selectionArgs, null);
174         try {
175             if (cursor != null && cursor.moveToNext()) {
176                 return cursor.getString(INDEX_BUCKET_NAME);
177             }
178         } finally {
179             Utils.closeSilently(cursor);
180         }
181         return null;
182     }
183 
184     @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
getFilesContentUri()185     private static Uri getFilesContentUri() {
186         return Files.getContentUri(EXTERNAL_MEDIA);
187     }
188 
getBucketName(ContentResolver resolver, int bucketId)189     public static String getBucketName(ContentResolver resolver, int bucketId) {
190         if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
191             String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
192             return result == null ? "" : result;
193         } else {
194             String result = getBucketNameInTable(
195                     resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
196             if (result != null) return result;
197             result = getBucketNameInTable(
198                     resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
199             return result == null ? "" : result;
200         }
201     }
202 
203     public static class BucketEntry {
204         public String bucketName;
205         public int bucketId;
206         public int dateTaken;
207 
BucketEntry(int id, String name)208         public BucketEntry(int id, String name) {
209             bucketId = id;
210             bucketName = Utils.ensureNotNull(name);
211         }
212 
213         @Override
hashCode()214         public int hashCode() {
215             return bucketId;
216         }
217 
218         @Override
equals(Object object)219         public boolean equals(Object object) {
220             if (!(object instanceof BucketEntry)) return false;
221             BucketEntry entry = (BucketEntry) object;
222             return bucketId == entry.bucketId;
223         }
224     }
225 }
226