blob: 0575e9b6559f5c8510a1a683b38f8ad129368c57 [file] [log] [blame]
Owen Lina2fba682011-08-17 22:07:43 +08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.data;
18
Owen Lina2fba682011-08-17 22:07:43 +080019import android.content.ContentResolver;
20import android.database.Cursor;
21import android.net.Uri;
22import android.provider.MediaStore.Files;
23import android.provider.MediaStore.Files.FileColumns;
24import android.provider.MediaStore.Images;
25import android.provider.MediaStore.Images.ImageColumns;
26import android.provider.MediaStore.Video;
Owen Line80b4762012-03-13 14:17:01 +080027import android.util.Log;
Owen Lina2fba682011-08-17 22:07:43 +080028
Owen Lin73a04ff2012-03-14 17:27:24 +080029import com.android.gallery3d.R;
30import com.android.gallery3d.app.GalleryApp;
31import com.android.gallery3d.common.Utils;
Owen Lin813ba6f2012-03-16 13:51:49 +080032import com.android.gallery3d.util.Future;
33import com.android.gallery3d.util.FutureListener;
Owen Lin73a04ff2012-03-14 17:27:24 +080034import com.android.gallery3d.util.MediaSetUtils;
Owen Lin813ba6f2012-03-16 13:51:49 +080035import com.android.gallery3d.util.ThreadPool;
36import com.android.gallery3d.util.ThreadPool.JobContext;
Owen Lin73a04ff2012-03-14 17:27:24 +080037
Owen Lina2fba682011-08-17 22:07:43 +080038import java.util.ArrayList;
Owen Lina2fba682011-08-17 22:07:43 +080039import java.util.Comparator;
Owen Lina2fba682011-08-17 22:07:43 +080040
41// LocalAlbumSet lists all image or video albums in the local storage.
42// The path should be "/local/image", "local/video" or "/local/all"
Owen Lin813ba6f2012-03-16 13:51:49 +080043public class LocalAlbumSet extends MediaSet
44 implements FutureListener<ArrayList<MediaSet>> {
Owen Lina2fba682011-08-17 22:07:43 +080045 public static final Path PATH_ALL = Path.fromString("/local/all");
46 public static final Path PATH_IMAGE = Path.fromString("/local/image");
47 public static final Path PATH_VIDEO = Path.fromString("/local/video");
48
49 private static final String TAG = "LocalAlbumSet";
50 private static final String EXTERNAL_MEDIA = "external";
51
52 // The indices should match the following projections.
53 private static final int INDEX_BUCKET_ID = 0;
54 private static final int INDEX_MEDIA_TYPE = 1;
55 private static final int INDEX_BUCKET_NAME = 2;
56
57 private static final Uri mBaseUri = Files.getContentUri(EXTERNAL_MEDIA);
58 private static final Uri mWatchUriImage = Images.Media.EXTERNAL_CONTENT_URI;
59 private static final Uri mWatchUriVideo = Video.Media.EXTERNAL_CONTENT_URI;
60
Chih-Chung Changfbef3862011-09-29 19:07:00 +080061 // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
62 // name of where an image or video is in. BUCKET_ID is a hash of the path
63 // name of that directory (see computeBucketValues() in MediaProvider for
64 // details). MEDIA_TYPE is video, image, audio, etc.
65 //
66 // The "albums" are not explicitly recorded in the database, but each image
67 // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
68 // "album" to be the collection of images/videos which have the same value
69 // for the two columns.
70 //
71 // The goal of the query (used in loadSubMediaSets()) is to find all albums,
72 // that is, all unique values for (BUCKET_ID, MEDIA_TYPE). In the meantime
73 // sort them by the timestamp of the latest image/video in each of the album.
74 //
75 // The order of columns below is important: it must match to the index in
76 // MediaStore.
Owen Lina2fba682011-08-17 22:07:43 +080077 private static final String[] PROJECTION_BUCKET = {
78 ImageColumns.BUCKET_ID,
79 FileColumns.MEDIA_TYPE,
80 ImageColumns.BUCKET_DISPLAY_NAME };
81
Chih-Chung Changfbef3862011-09-29 19:07:00 +080082 // We want to order the albums by reverse chronological order. We abuse the
83 // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
84 // The template for "WHERE" parameter is like:
85 // SELECT ... FROM ... WHERE (%s)
86 // and we make it look like:
87 // SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
88 // The "(1)" means true. The "1,(2)" means the first two columns specified
89 // after SELECT. Note that because there is a ")" in the template, we use
90 // "(2" to match it.
91 private static final String BUCKET_GROUP_BY =
92 "1) GROUP BY 1,(2";
93 private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
94
Owen Lina2fba682011-08-17 22:07:43 +080095 private final GalleryApp mApplication;
96 private final int mType;
97 private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
98 private final ChangeNotifier mNotifierImage;
99 private final ChangeNotifier mNotifierVideo;
100 private final String mName;
Owen Lin813ba6f2012-03-16 13:51:49 +0800101 private Future<ArrayList<MediaSet>> mLoadTask;
102 private ArrayList<MediaSet> mLoadBuffer;
Owen Lina2fba682011-08-17 22:07:43 +0800103
104 public LocalAlbumSet(Path path, GalleryApp application) {
105 super(path, nextVersionNumber());
106 mApplication = application;
107 mType = getTypeFromPath(path);
108 mNotifierImage = new ChangeNotifier(this, mWatchUriImage, application);
109 mNotifierVideo = new ChangeNotifier(this, mWatchUriVideo, application);
110 mName = application.getResources().getString(
111 R.string.set_label_local_albums);
112 }
113
114 private static int getTypeFromPath(Path path) {
115 String name[] = path.split();
116 if (name.length < 2) {
117 throw new IllegalArgumentException(path.toString());
118 }
119 if ("all".equals(name[1])) return MEDIA_TYPE_ALL;
120 if ("image".equals(name[1])) return MEDIA_TYPE_IMAGE;
121 if ("video".equals(name[1])) return MEDIA_TYPE_VIDEO;
122 throw new IllegalArgumentException(path.toString());
123 }
124
125 @Override
126 public MediaSet getSubMediaSet(int index) {
127 return mAlbums.get(index);
128 }
129
130 @Override
131 public int getSubMediaSetCount() {
132 return mAlbums.size();
133 }
134
135 @Override
136 public String getName() {
137 return mName;
138 }
139
Owen Lin813ba6f2012-03-16 13:51:49 +0800140 private BucketEntry[] loadBucketEntries(JobContext jc) {
Owen Line80b4762012-03-13 14:17:01 +0800141 Uri uri = mBaseUri;
142
143 Log.v("DebugLoadingTime", "start quering media provider");
144 Cursor cursor = mApplication.getContentResolver().query(
145 uri, PROJECTION_BUCKET, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
146 if (cursor == null) {
147 Log.w(TAG, "cannot open local database: " + uri);
148 return new BucketEntry[0];
149 }
Chih-Chung Changfbef3862011-09-29 19:07:00 +0800150 ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
Owen Lina2fba682011-08-17 22:07:43 +0800151 int typeBits = 0;
152 if ((mType & MEDIA_TYPE_IMAGE) != 0) {
153 typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
154 }
155 if ((mType & MEDIA_TYPE_VIDEO) != 0) {
156 typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
157 }
158 try {
159 while (cursor.moveToNext()) {
160 if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
Chih-Chung Changfbef3862011-09-29 19:07:00 +0800161 BucketEntry entry = new BucketEntry(
Owen Lina2fba682011-08-17 22:07:43 +0800162 cursor.getInt(INDEX_BUCKET_ID),
Chih-Chung Changfbef3862011-09-29 19:07:00 +0800163 cursor.getString(INDEX_BUCKET_NAME));
164 if (!buffer.contains(entry)) {
165 buffer.add(entry);
166 }
Owen Lina2fba682011-08-17 22:07:43 +0800167 }
Owen Lin813ba6f2012-03-16 13:51:49 +0800168 if (jc.isCancelled()) return null;
Owen Lina2fba682011-08-17 22:07:43 +0800169 }
Owen Line80b4762012-03-13 14:17:01 +0800170 Log.v("DebugLoadingTime", "got " + buffer.size() + " buckets");
Owen Lina2fba682011-08-17 22:07:43 +0800171 } finally {
172 cursor.close();
173 }
174 return buffer.toArray(new BucketEntry[buffer.size()]);
175 }
176
177
178 private static int findBucket(BucketEntry entries[], int bucketId) {
179 for (int i = 0, n = entries.length; i < n ; ++i) {
180 if (entries[i].bucketId == bucketId) return i;
181 }
182 return -1;
183 }
184
Owen Lin813ba6f2012-03-16 13:51:49 +0800185 private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
Owen Lina2fba682011-08-17 22:07:43 +0800186
Owen Lin813ba6f2012-03-16 13:51:49 +0800187 @Override
188 @SuppressWarnings("unchecked")
189 public ArrayList<MediaSet> run(JobContext jc) {
190 // Note: it will be faster if we only select media_type and bucket_id.
191 // need to test the performance if that is worth
192 BucketEntry[] entries = loadBucketEntries(jc);
Owen Lina2fba682011-08-17 22:07:43 +0800193
Owen Lin813ba6f2012-03-16 13:51:49 +0800194 if (jc.isCancelled()) return null;
Owen Lina2fba682011-08-17 22:07:43 +0800195
Owen Lin813ba6f2012-03-16 13:51:49 +0800196 int offset = 0;
197 // Move camera and download bucket to the front, while keeping the
198 // order of others.
199 int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
200 if (index != -1) {
201 circularShiftRight(entries, offset++, index);
202 }
203 index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
204 if (index != -1) {
205 circularShiftRight(entries, offset++, index);
206 }
207
208 ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
209 DataManager dataManager = mApplication.getDataManager();
210 for (BucketEntry entry : entries) {
211 MediaSet album = getLocalAlbum(dataManager,
212 mType, mPath, entry.bucketId, entry.bucketName);
213 album.reload();
214 albums.add(album);
215 }
216 return albums;
Owen Lina2fba682011-08-17 22:07:43 +0800217 }
Owen Lina2fba682011-08-17 22:07:43 +0800218 }
219
220 private MediaSet getLocalAlbum(
221 DataManager manager, int type, Path parent, int id, String name) {
Owen Lin813ba6f2012-03-16 13:51:49 +0800222 synchronized (DataManager.LOCK) {
223 Path path = parent.getChild(id);
224 MediaObject object = manager.peekMediaObject(path);
225 if (object != null) return (MediaSet) object;
226 switch (type) {
227 case MEDIA_TYPE_IMAGE:
228 return new LocalAlbum(path, mApplication, id, true, name);
229 case MEDIA_TYPE_VIDEO:
230 return new LocalAlbum(path, mApplication, id, false, name);
231 case MEDIA_TYPE_ALL:
232 Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
233 return new LocalMergeAlbum(path, comp, new MediaSet[] {
234 getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
235 getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id);
236 }
237 throw new IllegalArgumentException(String.valueOf(type));
Owen Lina2fba682011-08-17 22:07:43 +0800238 }
Owen Lina2fba682011-08-17 22:07:43 +0800239 }
240
241 public static String getBucketName(ContentResolver resolver, int bucketId) {
242 Uri uri = mBaseUri.buildUpon()
243 .appendQueryParameter("limit", "1")
244 .build();
245
246 Cursor cursor = resolver.query(
247 uri, PROJECTION_BUCKET, "bucket_id = ?",
248 new String[]{String.valueOf(bucketId)}, null);
249
250 if (cursor == null) {
251 Log.w(TAG, "query fail: " + uri);
252 return "";
253 }
254 try {
255 return cursor.moveToNext()
256 ? cursor.getString(INDEX_BUCKET_NAME)
257 : "";
258 } finally {
259 cursor.close();
260 }
261 }
262
263 @Override
Owen Lin813ba6f2012-03-16 13:51:49 +0800264 // synchronized on this function for
265 // 1. Prevent calling reload() concurrently.
266 // 2. Prevent calling onFutureDone() and reload() concurrently
267 public synchronized long reload() {
Owen Lina2fba682011-08-17 22:07:43 +0800268 // "|" is used instead of "||" because we want to clear both flags.
269 if (mNotifierImage.isDirty() | mNotifierVideo.isDirty()) {
Owen Lin813ba6f2012-03-16 13:51:49 +0800270 if (mLoadTask != null) mLoadTask.cancel();
271 mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
272 }
273 if (mLoadBuffer != null) {
274 mAlbums = mLoadBuffer;
275 mLoadBuffer = null;
Owen Lina2fba682011-08-17 22:07:43 +0800276 mDataVersion = nextVersionNumber();
Owen Lina2fba682011-08-17 22:07:43 +0800277 }
278 return mDataVersion;
279 }
280
Owen Lin813ba6f2012-03-16 13:51:49 +0800281 @Override
282 public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
283 if (mLoadTask != future) return; // ignore, wait for the latest task
284 mLoadBuffer = future.get();
285 if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
286 notifyContentChanged();
287 }
288
Owen Lina2fba682011-08-17 22:07:43 +0800289 // For debug only. Fake there is a ContentObserver.onChange() event.
290 void fakeChange() {
291 mNotifierImage.fakeChange();
292 mNotifierVideo.fakeChange();
293 }
294
295 private static class BucketEntry {
296 public String bucketName;
297 public int bucketId;
298
299 public BucketEntry(int id, String name) {
300 bucketId = id;
301 bucketName = Utils.ensureNotNull(name);
302 }
303
304 @Override
305 public int hashCode() {
306 return bucketId;
307 }
308
309 @Override
310 public boolean equals(Object object) {
311 if (!(object instanceof BucketEntry)) return false;
312 BucketEntry entry = (BucketEntry) object;
313 return bucketId == entry.bucketId;
314 }
315 }
Chih-Chung Changfbef3862011-09-29 19:07:00 +0800316
317 // Circular shift the array range from a[i] to a[j] (inclusive). That is,
318 // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
319 private static <T> void circularShiftRight(T[] array, int i, int j) {
320 T temp = array[j];
321 for (int k = j; k > i; k--) {
322 array[k] = array[k - 1];
323 }
324 array[i] = temp;
325 }
Owen Lina2fba682011-08-17 22:07:43 +0800326}