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