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 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