1package com.android.gallery3d.data;
2
3import android.annotation.TargetApi;
4import android.content.ContentResolver;
5import android.database.Cursor;
6import android.net.Uri;
7import android.provider.MediaStore.Files;
8import android.provider.MediaStore.Files.FileColumns;
9import android.provider.MediaStore.Images;
10import android.provider.MediaStore.Images.ImageColumns;
11import android.provider.MediaStore.Video;
12import android.util.Log;
13
14import com.android.gallery3d.common.ApiHelper;
15import com.android.gallery3d.common.Utils;
16import com.android.gallery3d.util.ThreadPool.JobContext;
17
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Comparator;
21import java.util.HashMap;
22
23class 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
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
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
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
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
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)
201    private static Uri getFilesContentUri() {
202        return Files.getContentUri(EXTERNAL_MEDIA);
203    }
204
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
224        public BucketEntry(int id, String name) {
225            bucketId = id;
226            bucketName = Utils.ensureNotNull(name);
227        }
228
229        @Override
230        public int hashCode() {
231            return bucketId;
232        }
233
234        @Override
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