summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/data')
-rw-r--r--src/com/android/gallery3d/data/ActionImage.java103
-rw-r--r--src/com/android/gallery3d/data/BucketHelper.java241
-rw-r--r--src/com/android/gallery3d/data/BytesBufferPool.java91
-rw-r--r--src/com/android/gallery3d/data/CameraShortcutImage.java34
-rw-r--r--src/com/android/gallery3d/data/ChangeNotifier.java57
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbum.java143
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbumSet.java159
-rw-r--r--src/com/android/gallery3d/data/ClusterSource.java86
-rw-r--r--src/com/android/gallery3d/data/Clustering.java29
-rw-r--r--src/com/android/gallery3d/data/ComboAlbum.java103
-rw-r--r--src/com/android/gallery3d/data/ComboAlbumSet.java96
-rw-r--r--src/com/android/gallery3d/data/ComboSource.java55
-rw-r--r--src/com/android/gallery3d/data/ContentListener.java21
-rw-r--r--src/com/android/gallery3d/data/DataManager.java371
-rw-r--r--src/com/android/gallery3d/data/DataSourceType.java45
-rw-r--r--src/com/android/gallery3d/data/DecodeUtils.java312
-rw-r--r--src/com/android/gallery3d/data/DownloadCache.java370
-rw-r--r--src/com/android/gallery3d/data/DownloadEntry.java72
-rw-r--r--src/com/android/gallery3d/data/DownloadUtils.java79
-rw-r--r--src/com/android/gallery3d/data/EmptyAlbumImage.java34
-rw-r--r--src/com/android/gallery3d/data/Exif.java48
-rw-r--r--src/com/android/gallery3d/data/Face.java65
-rw-r--r--src/com/android/gallery3d/data/FaceClustering.java142
-rw-r--r--src/com/android/gallery3d/data/FilterDeleteSet.java256
-rw-r--r--src/com/android/gallery3d/data/FilterEmptyPromptSet.java82
-rw-r--r--src/com/android/gallery3d/data/FilterSource.java94
-rw-r--r--src/com/android/gallery3d/data/FilterTypeSet.java137
-rw-r--r--src/com/android/gallery3d/data/ImageCacheRequest.java102
-rw-r--r--src/com/android/gallery3d/data/ImageCacheService.java123
-rw-r--r--src/com/android/gallery3d/data/LocalAlbum.java325
-rw-r--r--src/com/android/gallery3d/data/LocalAlbumSet.java211
-rw-r--r--src/com/android/gallery3d/data/LocalImage.java355
-rw-r--r--src/com/android/gallery3d/data/LocalMediaItem.java109
-rw-r--r--src/com/android/gallery3d/data/LocalMergeAlbum.java257
-rw-r--r--src/com/android/gallery3d/data/LocalSource.java275
-rw-r--r--src/com/android/gallery3d/data/LocalVideo.java242
-rw-r--r--src/com/android/gallery3d/data/LocationClustering.java316
-rw-r--r--src/com/android/gallery3d/data/Log.java53
-rw-r--r--src/com/android/gallery3d/data/MediaDetails.java170
-rw-r--r--src/com/android/gallery3d/data/MediaItem.java134
-rw-r--r--src/com/android/gallery3d/data/MediaObject.java166
-rw-r--r--src/com/android/gallery3d/data/MediaSet.java348
-rw-r--r--src/com/android/gallery3d/data/MediaSource.java96
-rw-r--r--src/com/android/gallery3d/data/MtpClient.java443
-rw-r--r--src/com/android/gallery3d/data/PanoramaMetadataJob.java40
-rw-r--r--src/com/android/gallery3d/data/Path.java241
-rw-r--r--src/com/android/gallery3d/data/PathMatcher.java102
-rw-r--r--src/com/android/gallery3d/data/SecureAlbum.java206
-rw-r--r--src/com/android/gallery3d/data/SecureSource.java56
-rw-r--r--src/com/android/gallery3d/data/SingleItemAlbum.java68
-rw-r--r--src/com/android/gallery3d/data/SizeClustering.java141
-rw-r--r--src/com/android/gallery3d/data/SnailAlbum.java44
-rw-r--r--src/com/android/gallery3d/data/SnailItem.java95
-rw-r--r--src/com/android/gallery3d/data/SnailSource.java70
-rw-r--r--src/com/android/gallery3d/data/TagClustering.java95
-rw-r--r--src/com/android/gallery3d/data/TimeClustering.java439
-rw-r--r--src/com/android/gallery3d/data/UnlockImage.java34
-rw-r--r--src/com/android/gallery3d/data/UriImage.java298
-rw-r--r--src/com/android/gallery3d/data/UriSource.java95
59 files changed, 9074 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/data/ActionImage.java b/src/com/android/gallery3d/data/ActionImage.java
new file mode 100644
index 000000000..58e30b146
--- /dev/null
+++ b/src/com/android/gallery3d/data/ActionImage.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class ActionImage extends MediaItem {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ActionImage";
+ private GalleryApp mApplication;
+ private int mResourceId;
+
+ public ActionImage(Path path, GalleryApp application, int resourceId) {
+ super(path, nextVersionNumber());
+ mApplication = Utils.checkNotNull(application);
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new BitmapJob(type);
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return null;
+ }
+
+ private class BitmapJob implements Job<Bitmap> {
+ private int mType;
+
+ protected BitmapJob(int type) {
+ mType = type;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ int targetSize = MediaItem.getTargetSize(mType);
+ Bitmap bitmap = BitmapFactory.decodeResource(mApplication.getResources(),
+ mResourceId);
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
+ }
+ return bitmap;
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_ACTION;
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_UNKNOWN;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ return null;
+ }
+
+ @Override
+ public String getMimeType() {
+ return "";
+ }
+
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/data/BucketHelper.java b/src/com/android/gallery3d/data/BucketHelper.java
new file mode 100644
index 000000000..3418dafb7
--- /dev/null
+++ b/src/com/android/gallery3d/data/BucketHelper.java
@@ -0,0 +1,241 @@
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+class BucketHelper {
+
+ private static final String TAG = "BucketHelper";
+ private static final String EXTERNAL_MEDIA = "external";
+
+ // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
+ // name of where an image or video is in. BUCKET_ID is a hash of the path
+ // name of that directory (see computeBucketValues() in MediaProvider for
+ // details). MEDIA_TYPE is video, image, audio, etc.
+ //
+ // The "albums" are not explicitly recorded in the database, but each image
+ // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
+ // "album" to be the collection of images/videos which have the same value
+ // for the two columns.
+ //
+ // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to
+ // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE).
+ // In the meantime sort them by the timestamp of the latest image/video in
+ // each of the album.
+ //
+ // The order of columns below is important: it must match to the index in
+ // MediaStore.
+ private static final String[] PROJECTION_BUCKET = {
+ ImageColumns.BUCKET_ID,
+ FileColumns.MEDIA_TYPE,
+ ImageColumns.BUCKET_DISPLAY_NAME};
+
+ // The indices should match the above projections.
+ private static final int INDEX_BUCKET_ID = 0;
+ private static final int INDEX_MEDIA_TYPE = 1;
+ private static final int INDEX_BUCKET_NAME = 2;
+
+ // We want to order the albums by reverse chronological order. We abuse the
+ // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
+ // The template for "WHERE" parameter is like:
+ // SELECT ... FROM ... WHERE (%s)
+ // and we make it look like:
+ // SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
+ // The "(1)" means true. The "1,(2)" means the first two columns specified
+ // after SELECT. Note that because there is a ")" in the template, we use
+ // "(2" to match it.
+ private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2";
+
+ private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
+
+ // Before HoneyComb there is no Files table. Thus, we need to query the
+ // bucket info from the Images and Video tables and then merge them
+ // together.
+ //
+ // A bucket can exist in both tables. In this case, we need to find the
+ // latest timestamp from the two tables and sort ourselves. So we add the
+ // MAX(date_taken) to the projection and remove the media_type since we
+ // already know the media type from the table we query from.
+ private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
+ ImageColumns.BUCKET_ID,
+ "MAX(datetaken)",
+ ImageColumns.BUCKET_DISPLAY_NAME};
+
+ // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
+ // PROJECTION_BUCKET so we can reuse the values defined before.
+ private static final int INDEX_DATE_TAKEN = 1;
+
+ // When query from the Images or Video tables, we only need to group by BUCKET_ID.
+ private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1";
+
+ public static BucketEntry[] loadBucketEntries(
+ JobContext jc, ContentResolver resolver, int type) {
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ return loadBucketEntriesFromFilesTable(jc, resolver, type);
+ } else {
+ return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
+ }
+ }
+
+ private static void updateBucketEntriesFromTable(JobContext jc,
+ ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
+ Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
+ BUCKET_GROUP_BY_IN_ONE_TABLE, null, null);
+ if (cursor == null) {
+ Log.w(TAG, "cannot open media database: " + tableUri);
+ return;
+ }
+ try {
+ while (cursor.moveToNext()) {
+ int bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
+ BucketEntry entry = buckets.get(bucketId);
+ if (entry == null) {
+ entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
+ buckets.put(bucketId, entry);
+ entry.dateTaken = dateTaken;
+ } else {
+ entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
+ }
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+
+ private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
+ JobContext jc, ContentResolver resolver, int type) {
+ HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
+ if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+ updateBucketEntriesFromTable(
+ jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
+ }
+ if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+ updateBucketEntriesFromTable(
+ jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
+ }
+ BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
+ Arrays.sort(entries, new Comparator<BucketEntry>() {
+ @Override
+ public int compare(BucketEntry a, BucketEntry b) {
+ // sorted by dateTaken in descending order
+ return b.dateTaken - a.dateTaken;
+ }
+ });
+ return entries;
+ }
+
+ private static BucketEntry[] loadBucketEntriesFromFilesTable(
+ JobContext jc, ContentResolver resolver, int type) {
+ Uri uri = getFilesContentUri();
+
+ Cursor cursor = resolver.query(uri,
+ PROJECTION_BUCKET, BUCKET_GROUP_BY,
+ null, BUCKET_ORDER_BY);
+ if (cursor == null) {
+ Log.w(TAG, "cannot open local database: " + uri);
+ return new BucketEntry[0];
+ }
+ ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
+ int typeBits = 0;
+ if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
+ }
+ if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
+ }
+ try {
+ while (cursor.moveToNext()) {
+ if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
+ BucketEntry entry = new BucketEntry(
+ cursor.getInt(INDEX_BUCKET_ID),
+ cursor.getString(INDEX_BUCKET_NAME));
+ if (!buffer.contains(entry)) {
+ buffer.add(entry);
+ }
+ }
+ if (jc.isCancelled()) return null;
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ return buffer.toArray(new BucketEntry[buffer.size()]);
+ }
+
+ private static String getBucketNameInTable(
+ ContentResolver resolver, Uri tableUri, int bucketId) {
+ String selectionArgs[] = new String[] {String.valueOf(bucketId)};
+ Uri uri = tableUri.buildUpon()
+ .appendQueryParameter("limit", "1")
+ .build();
+ Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
+ "bucket_id = ?", selectionArgs, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ return cursor.getString(INDEX_BUCKET_NAME);
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ return null;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static Uri getFilesContentUri() {
+ return Files.getContentUri(EXTERNAL_MEDIA);
+ }
+
+ public static String getBucketName(ContentResolver resolver, int bucketId) {
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
+ return result == null ? "" : result;
+ } else {
+ String result = getBucketNameInTable(
+ resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
+ if (result != null) return result;
+ result = getBucketNameInTable(
+ resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
+ return result == null ? "" : result;
+ }
+ }
+
+ public static class BucketEntry {
+ public String bucketName;
+ public int bucketId;
+ public int dateTaken;
+
+ public BucketEntry(int id, String name) {
+ bucketId = id;
+ bucketName = Utils.ensureNotNull(name);
+ }
+
+ @Override
+ public int hashCode() {
+ return bucketId;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof BucketEntry)) return false;
+ BucketEntry entry = (BucketEntry) object;
+ return bucketId == entry.bucketId;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/BytesBufferPool.java b/src/com/android/gallery3d/data/BytesBufferPool.java
new file mode 100644
index 000000000..d2da323fc
--- /dev/null
+++ b/src/com/android/gallery3d/data/BytesBufferPool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class BytesBufferPool {
+
+ private static final int READ_STEP = 4096;
+
+ public static class BytesBuffer {
+ public byte[] data;
+ public int offset;
+ public int length;
+
+ private BytesBuffer(int capacity) {
+ this.data = new byte[capacity];
+ }
+
+ // an helper function to read content from FileDescriptor
+ public void readFrom(JobContext jc, FileDescriptor fd) throws IOException {
+ FileInputStream fis = new FileInputStream(fd);
+ length = 0;
+ try {
+ int capacity = data.length;
+ while (true) {
+ int step = Math.min(READ_STEP, capacity - length);
+ int rc = fis.read(data, length, step);
+ if (rc < 0 || jc.isCancelled()) return;
+ length += rc;
+
+ if (length == capacity) {
+ byte[] newData = new byte[data.length * 2];
+ System.arraycopy(data, 0, newData, 0, data.length);
+ data = newData;
+ capacity = data.length;
+ }
+ }
+ } finally {
+ fis.close();
+ }
+ }
+ }
+
+ private final int mPoolSize;
+ private final int mBufferSize;
+ private final ArrayList<BytesBuffer> mList;
+
+ public BytesBufferPool(int poolSize, int bufferSize) {
+ mList = new ArrayList<BytesBuffer>(poolSize);
+ mPoolSize = poolSize;
+ mBufferSize = bufferSize;
+ }
+
+ public synchronized BytesBuffer get() {
+ int n = mList.size();
+ return n > 0 ? mList.remove(n - 1) : new BytesBuffer(mBufferSize);
+ }
+
+ public synchronized void recycle(BytesBuffer buffer) {
+ if (buffer.data.length != mBufferSize) return;
+ if (mList.size() < mPoolSize) {
+ buffer.offset = 0;
+ buffer.length = 0;
+ mList.add(buffer);
+ }
+ }
+
+ public synchronized void clear() {
+ mList.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/data/CameraShortcutImage.java b/src/com/android/gallery3d/data/CameraShortcutImage.java
new file mode 100644
index 000000000..865270b4c
--- /dev/null
+++ b/src/com/android/gallery3d/data/CameraShortcutImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class CameraShortcutImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "CameraShortcutImage";
+
+ public CameraShortcutImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_camera);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_CAMERA_SHORTCUT;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java
new file mode 100644
index 000000000..558a8648e
--- /dev/null
+++ b/src/com/android/gallery3d/data/ChangeNotifier.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// This handles change notification for media sets.
+public class ChangeNotifier {
+
+ private MediaSet mMediaSet;
+ private AtomicBoolean mContentDirty = new AtomicBoolean(true);
+
+ public ChangeNotifier(MediaSet set, Uri uri, GalleryApp application) {
+ mMediaSet = set;
+ application.getDataManager().registerChangeNotifier(uri, this);
+ }
+
+ public ChangeNotifier(MediaSet set, Uri[] uris, GalleryApp application) {
+ mMediaSet = set;
+ for (int i = 0; i < uris.length; i++) {
+ application.getDataManager().registerChangeNotifier(uris[i], this);
+ }
+ }
+
+ // Returns the dirty flag and clear it.
+ public boolean isDirty() {
+ return mContentDirty.compareAndSet(true, false);
+ }
+
+ public void fakeChange() {
+ onChange(false);
+ }
+
+ protected void onChange(boolean selfChange) {
+ if (mContentDirty.compareAndSet(false, true)) {
+ mMediaSet.notifyContentChanged();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java
new file mode 100644
index 000000000..8681952bf
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbum.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class ClusterAlbum extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ClusterAlbum";
+ private ArrayList<Path> mPaths = new ArrayList<Path>();
+ private String mName = "";
+ private DataManager mDataManager;
+ private MediaSet mClusterAlbumSet;
+ private MediaItem mCover;
+
+ public ClusterAlbum(Path path, DataManager dataManager,
+ MediaSet clusterAlbumSet) {
+ super(path, nextVersionNumber());
+ mDataManager = dataManager;
+ mClusterAlbumSet = clusterAlbumSet;
+ mClusterAlbumSet.addContentListener(this);
+ }
+
+ public void setCoverMediaItem(MediaItem cover) {
+ mCover = cover;
+ }
+
+ @Override
+ public MediaItem getCoverMediaItem() {
+ return mCover != null ? mCover : super.getCoverMediaItem();
+ }
+
+ void setMediaItems(ArrayList<Path> paths) {
+ mPaths = paths;
+ }
+
+ ArrayList<Path> getMediaItems() {
+ return mPaths;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return mPaths.size();
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ return getMediaItemFromPath(mPaths, start, count, mDataManager);
+ }
+
+ public static ArrayList<MediaItem> getMediaItemFromPath(
+ ArrayList<Path> paths, int start, int count,
+ DataManager dataManager) {
+ if (start >= paths.size()) {
+ return new ArrayList<MediaItem>();
+ }
+ int end = Math.min(start + count, paths.size());
+ ArrayList<Path> subset = new ArrayList<Path>(paths.subList(start, end));
+ final MediaItem[] buf = new MediaItem[end - start];
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ buf[index] = item;
+ }
+ };
+ dataManager.mapMediaItems(subset, consumer, 0);
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
+ for (int i = 0; i < buf.length; i++) {
+ result.add(buf[i]);
+ }
+ return result;
+ }
+
+ @Override
+ protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
+ mDataManager.mapMediaItems(mPaths, consumer, startIndex);
+ return mPaths.size();
+ }
+
+ @Override
+ public int getTotalMediaItemCount() {
+ return mPaths.size();
+ }
+
+ @Override
+ public long reload() {
+ if (mClusterAlbumSet.reload() > mDataVersion) {
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO;
+ }
+
+ @Override
+ public void delete() {
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) {
+ item.delete();
+ }
+ }
+ };
+ mDataManager.mapMediaItems(mPaths, consumer, 0);
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java
new file mode 100644
index 000000000..cb212ba36
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class ClusterAlbumSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ClusterAlbumSet";
+ private GalleryApp mApplication;
+ private MediaSet mBaseSet;
+ private int mKind;
+ private ArrayList<ClusterAlbum> mAlbums = new ArrayList<ClusterAlbum>();
+ private boolean mFirstReloadDone;
+
+ public ClusterAlbumSet(Path path, GalleryApp application,
+ MediaSet baseSet, int kind) {
+ super(path, INVALID_DATA_VERSION);
+ mApplication = application;
+ mBaseSet = baseSet;
+ mKind = kind;
+ baseSet.addContentListener(this);
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return mAlbums.get(index);
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mAlbums.size();
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+
+ @Override
+ public long reload() {
+ if (mBaseSet.reload() > mDataVersion) {
+ if (mFirstReloadDone) {
+ updateClustersContents();
+ } else {
+ updateClusters();
+ mFirstReloadDone = true;
+ }
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ private void updateClusters() {
+ mAlbums.clear();
+ Clustering clustering;
+ Context context = mApplication.getAndroidContext();
+ switch (mKind) {
+ case ClusterSource.CLUSTER_ALBUMSET_TIME:
+ clustering = new TimeClustering(context);
+ break;
+ case ClusterSource.CLUSTER_ALBUMSET_LOCATION:
+ clustering = new LocationClustering(context);
+ break;
+ case ClusterSource.CLUSTER_ALBUMSET_TAG:
+ clustering = new TagClustering(context);
+ break;
+ case ClusterSource.CLUSTER_ALBUMSET_FACE:
+ clustering = new FaceClustering(context);
+ break;
+ default: /* CLUSTER_ALBUMSET_SIZE */
+ clustering = new SizeClustering(context);
+ break;
+ }
+
+ clustering.run(mBaseSet);
+ int n = clustering.getNumberOfClusters();
+ DataManager dataManager = mApplication.getDataManager();
+ for (int i = 0; i < n; i++) {
+ Path childPath;
+ String childName = clustering.getClusterName(i);
+ if (mKind == ClusterSource.CLUSTER_ALBUMSET_TAG) {
+ childPath = mPath.getChild(Uri.encode(childName));
+ } else if (mKind == ClusterSource.CLUSTER_ALBUMSET_SIZE) {
+ long minSize = ((SizeClustering) clustering).getMinSize(i);
+ childPath = mPath.getChild(minSize);
+ } else {
+ childPath = mPath.getChild(i);
+ }
+
+ ClusterAlbum album;
+ synchronized (DataManager.LOCK) {
+ album = (ClusterAlbum) dataManager.peekMediaObject(childPath);
+ if (album == null) {
+ album = new ClusterAlbum(childPath, dataManager, this);
+ }
+ }
+ album.setMediaItems(clustering.getCluster(i));
+ album.setName(childName);
+ album.setCoverMediaItem(clustering.getClusterCover(i));
+ mAlbums.add(album);
+ }
+ }
+
+ private void updateClustersContents() {
+ final HashSet<Path> existing = new HashSet<Path>();
+ mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ existing.add(item.getPath());
+ }
+ });
+
+ int n = mAlbums.size();
+
+ // The loop goes backwards because we may remove empty albums from
+ // mAlbums.
+ for (int i = n - 1; i >= 0; i--) {
+ ArrayList<Path> oldPaths = mAlbums.get(i).getMediaItems();
+ ArrayList<Path> newPaths = new ArrayList<Path>();
+ int m = oldPaths.size();
+ for (int j = 0; j < m; j++) {
+ Path p = oldPaths.get(j);
+ if (existing.contains(p)) {
+ newPaths.add(p);
+ }
+ }
+ mAlbums.get(i).setMediaItems(newPaths);
+ if (newPaths.isEmpty()) {
+ mAlbums.remove(i);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/ClusterSource.java b/src/com/android/gallery3d/data/ClusterSource.java
new file mode 100644
index 000000000..a1f22e57a
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterSource.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+class ClusterSource extends MediaSource {
+ static final int CLUSTER_ALBUMSET_TIME = 0;
+ static final int CLUSTER_ALBUMSET_LOCATION = 1;
+ static final int CLUSTER_ALBUMSET_TAG = 2;
+ static final int CLUSTER_ALBUMSET_SIZE = 3;
+ static final int CLUSTER_ALBUMSET_FACE = 4;
+
+ static final int CLUSTER_ALBUM_TIME = 0x100;
+ static final int CLUSTER_ALBUM_LOCATION = 0x101;
+ static final int CLUSTER_ALBUM_TAG = 0x102;
+ static final int CLUSTER_ALBUM_SIZE = 0x103;
+ static final int CLUSTER_ALBUM_FACE = 0x104;
+
+ GalleryApp mApplication;
+ PathMatcher mMatcher;
+
+ public ClusterSource(GalleryApp application) {
+ super("cluster");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/cluster/*/time", CLUSTER_ALBUMSET_TIME);
+ mMatcher.add("/cluster/*/location", CLUSTER_ALBUMSET_LOCATION);
+ mMatcher.add("/cluster/*/tag", CLUSTER_ALBUMSET_TAG);
+ mMatcher.add("/cluster/*/size", CLUSTER_ALBUMSET_SIZE);
+ mMatcher.add("/cluster/*/face", CLUSTER_ALBUMSET_FACE);
+
+ mMatcher.add("/cluster/*/time/*", CLUSTER_ALBUM_TIME);
+ mMatcher.add("/cluster/*/location/*", CLUSTER_ALBUM_LOCATION);
+ mMatcher.add("/cluster/*/tag/*", CLUSTER_ALBUM_TAG);
+ mMatcher.add("/cluster/*/size/*", CLUSTER_ALBUM_SIZE);
+ mMatcher.add("/cluster/*/face/*", CLUSTER_ALBUM_FACE);
+ }
+
+ // The names we accept are:
+ // /cluster/{set}/time /cluster/{set}/time/k
+ // /cluster/{set}/location /cluster/{set}/location/k
+ // /cluster/{set}/tag /cluster/{set}/tag/encoded_tag
+ // /cluster/{set}/size /cluster/{set}/size/min_size
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ int matchType = mMatcher.match(path);
+ String setsName = mMatcher.getVar(0);
+ DataManager dataManager = mApplication.getDataManager();
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ switch (matchType) {
+ case CLUSTER_ALBUMSET_TIME:
+ case CLUSTER_ALBUMSET_LOCATION:
+ case CLUSTER_ALBUMSET_TAG:
+ case CLUSTER_ALBUMSET_SIZE:
+ case CLUSTER_ALBUMSET_FACE:
+ return new ClusterAlbumSet(path, mApplication, sets[0], matchType);
+ case CLUSTER_ALBUM_TIME:
+ case CLUSTER_ALBUM_LOCATION:
+ case CLUSTER_ALBUM_TAG:
+ case CLUSTER_ALBUM_SIZE:
+ case CLUSTER_ALBUM_FACE: {
+ MediaSet parent = dataManager.getMediaSet(path.getParent());
+ // The actual content in the ClusterAlbum will be filled later
+ // when the reload() method in the parent is run.
+ return new ClusterAlbum(path, dataManager, parent);
+ }
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java
new file mode 100644
index 000000000..4072bf57b
--- /dev/null
+++ b/src/com/android/gallery3d/data/Clustering.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+public abstract class Clustering {
+ public abstract void run(MediaSet baseSet);
+ public abstract int getNumberOfClusters();
+ public abstract ArrayList<Path> getCluster(int index);
+ public abstract String getClusterName(int index);
+ public MediaItem getClusterCover(int index) {
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
new file mode 100644
index 000000000..cadd9f8af
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.util.Future;
+
+import java.util.ArrayList;
+
+// ComboAlbum combines multiple media sets into one. It lists all media items
+// from the input albums.
+// This only handles SubMediaSets, not MediaItems. (That's all we need now)
+public class ComboAlbum extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ComboAlbum";
+ private final MediaSet[] mSets;
+ private String mName;
+
+ public ComboAlbum(Path path, MediaSet[] mediaSets, String name) {
+ super(path, nextVersionNumber());
+ mSets = mediaSets;
+ for (MediaSet set : mSets) {
+ set.addContentListener(this);
+ }
+ mName = name;
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ ArrayList<MediaItem> items = new ArrayList<MediaItem>();
+ for (MediaSet set : mSets) {
+ int size = set.getMediaItemCount();
+ if (count < 1) break;
+ if (start < size) {
+ int fetchCount = (start + count <= size) ? count : size - start;
+ ArrayList<MediaItem> fetchItems = set.getMediaItem(start, fetchCount);
+ items.addAll(fetchItems);
+ count -= fetchItems.size();
+ start = 0;
+ } else {
+ start -= size;
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ int count = 0;
+ for (MediaSet set : mSets) {
+ count += set.getMediaItemCount();
+ }
+ return count;
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ public void useNameOfChild(int i) {
+ if (i < mSets.length) mName = mSets[i].getName();
+ }
+
+ @Override
+ public long reload() {
+ boolean changed = false;
+ for (int i = 0, n = mSets.length; i < n; ++i) {
+ long version = mSets[i].reload();
+ if (version > mDataVersion) changed = true;
+ }
+ if (changed) mDataVersion = nextVersionNumber();
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public Future<Integer> requestSync(SyncListener listener) {
+ return requestSyncOnMultipleSets(mSets, listener);
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
new file mode 100644
index 000000000..3f3674500
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.Future;
+
+// ComboAlbumSet combines multiple media sets into one. It lists all sub
+// media sets from the input album sets.
+// This only handles SubMediaSets, not MediaItems. (That's all we need now)
+public class ComboAlbumSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ComboAlbumSet";
+ private final MediaSet[] mSets;
+ private final String mName;
+
+ public ComboAlbumSet(Path path, GalleryApp application, MediaSet[] mediaSets) {
+ super(path, nextVersionNumber());
+ mSets = mediaSets;
+ for (MediaSet set : mSets) {
+ set.addContentListener(this);
+ }
+ mName = application.getResources().getString(
+ R.string.set_label_all_albums);
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ for (MediaSet set : mSets) {
+ int size = set.getSubMediaSetCount();
+ if (index < size) {
+ return set.getSubMediaSet(index);
+ }
+ index -= size;
+ }
+ return null;
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ int count = 0;
+ for (MediaSet set : mSets) {
+ count += set.getSubMediaSetCount();
+ }
+ return count;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public boolean isLoading() {
+ for (int i = 0, n = mSets.length; i < n; ++i) {
+ if (mSets[i].isLoading()) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public long reload() {
+ boolean changed = false;
+ for (int i = 0, n = mSets.length; i < n; ++i) {
+ long version = mSets[i].reload();
+ if (version > mDataVersion) changed = true;
+ }
+ if (changed) mDataVersion = nextVersionNumber();
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public Future<Integer> requestSync(SyncListener listener) {
+ return requestSyncOnMultipleSets(mSets, listener);
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboSource.java b/src/com/android/gallery3d/data/ComboSource.java
new file mode 100644
index 000000000..867d47e64
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboSource.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+class ComboSource extends MediaSource {
+ private static final int COMBO_ALBUMSET = 0;
+ private static final int COMBO_ALBUM = 1;
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+
+ public ComboSource(GalleryApp application) {
+ super("combo");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/combo/*", COMBO_ALBUMSET);
+ mMatcher.add("/combo/*/*", COMBO_ALBUM);
+ }
+
+ // The only path we accept is "/combo/{set1, set2, ...} and /combo/item/{set1, set2, ...}"
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ String[] segments = path.split();
+ if (segments.length < 2) {
+ throw new RuntimeException("bad path: " + path);
+ }
+
+ DataManager dataManager = mApplication.getDataManager();
+ switch (mMatcher.match(path)) {
+ case COMBO_ALBUMSET:
+ return new ComboAlbumSet(path, mApplication,
+ dataManager.getMediaSetsFromString(segments[1]));
+
+ case COMBO_ALBUM:
+ return new ComboAlbum(path,
+ dataManager.getMediaSetsFromString(segments[2]), segments[1]);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ContentListener.java b/src/com/android/gallery3d/data/ContentListener.java
new file mode 100644
index 000000000..5e2952685
--- /dev/null
+++ b/src/com/android/gallery3d/data/ContentListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+public interface ContentListener {
+ public void onContentDirty();
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
new file mode 100644
index 000000000..38865e9f1
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSource.PathId;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+// DataManager manages all media sets and media items in the system.
+//
+// Each MediaSet and MediaItem has a unique 64 bits id. The most significant
+// 32 bits represents its parent, and the least significant 32 bits represents
+// the self id. For MediaSet the self id is is globally unique, but for
+// MediaItem it's unique only relative to its parent.
+//
+// To make sure the id is the same when the MediaSet is re-created, a child key
+// is provided to obtainSetId() to make sure the same self id will be used as
+// when the parent and key are the same. A sequence of child keys is called a
+// path. And it's used to identify a specific media set even if the process is
+// killed and re-created, so child keys should be stable identifiers.
+
+public class DataManager implements StitchingChangeListener {
+ public static final int INCLUDE_IMAGE = 1;
+ public static final int INCLUDE_VIDEO = 2;
+ public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO;
+ public static final int INCLUDE_LOCAL_ONLY = 4;
+ public static final int INCLUDE_LOCAL_IMAGE_ONLY =
+ INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE;
+ public static final int INCLUDE_LOCAL_VIDEO_ONLY =
+ INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO;
+ public static final int INCLUDE_LOCAL_ALL_ONLY =
+ INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO;
+
+ // Any one who would like to access data should require this lock
+ // to prevent concurrency issue.
+ public static final Object LOCK = new Object();
+
+ public static DataManager from(Context context) {
+ GalleryApp app = (GalleryApp) context.getApplicationContext();
+ return app.getDataManager();
+ }
+
+ private static final String TAG = "DataManager";
+
+ // This is the path for the media set seen by the user at top level.
+ private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";
+
+ private static final String TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}";
+
+ private static final String TOP_VIDEO_SET_PATH =
+ "/combo/{/local/video,/picasa/video}";
+
+ private static final String TOP_LOCAL_SET_PATH = "/local/all";
+
+ private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image";
+
+ private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video";
+
+ public static final Comparator<MediaItem> sDateTakenComparator =
+ new DateTakenComparator();
+
+ private static class DateTakenComparator implements Comparator<MediaItem> {
+ @Override
+ public int compare(MediaItem item1, MediaItem item2) {
+ return -Utils.compare(item1.getDateInMs(), item2.getDateInMs());
+ }
+ }
+
+ private final Handler mDefaultMainHandler;
+
+ private GalleryApp mApplication;
+ private int mActiveCount = 0;
+
+ private HashMap<Uri, NotifyBroker> mNotifierMap =
+ new HashMap<Uri, NotifyBroker>();
+
+
+ private HashMap<String, MediaSource> mSourceMap =
+ new LinkedHashMap<String, MediaSource>();
+
+ public DataManager(GalleryApp application) {
+ mApplication = application;
+ mDefaultMainHandler = new Handler(application.getMainLooper());
+ }
+
+ public synchronized void initializeSourceMap() {
+ if (!mSourceMap.isEmpty()) return;
+
+ // the order matters, the UriSource must come last
+ addSource(new LocalSource(mApplication));
+ addSource(new PicasaSource(mApplication));
+ addSource(new ComboSource(mApplication));
+ addSource(new ClusterSource(mApplication));
+ addSource(new FilterSource(mApplication));
+ addSource(new SecureSource(mApplication));
+ addSource(new UriSource(mApplication));
+ addSource(new SnailSource(mApplication));
+
+ if (mActiveCount > 0) {
+ for (MediaSource source : mSourceMap.values()) {
+ source.resume();
+ }
+ }
+ }
+
+ public String getTopSetPath(int typeBits) {
+
+ switch (typeBits) {
+ case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH;
+ case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH;
+ case INCLUDE_ALL: return TOP_SET_PATH;
+ case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH;
+ case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH;
+ case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH;
+ default: throw new IllegalArgumentException();
+ }
+ }
+
+ // open for debug
+ void addSource(MediaSource source) {
+ if (source == null) return;
+ mSourceMap.put(source.getPrefix(), source);
+ }
+
+ // A common usage of this method is:
+ // synchronized (DataManager.LOCK) {
+ // MediaObject object = peekMediaObject(path);
+ // if (object == null) {
+ // object = createMediaObject(...);
+ // }
+ // }
+ public MediaObject peekMediaObject(Path path) {
+ return path.getObject();
+ }
+
+ public MediaObject getMediaObject(Path path) {
+ synchronized (LOCK) {
+ MediaObject obj = path.getObject();
+ if (obj != null) return obj;
+
+ MediaSource source = mSourceMap.get(path.getPrefix());
+ if (source == null) {
+ Log.w(TAG, "cannot find media source for path: " + path);
+ return null;
+ }
+
+ try {
+ MediaObject object = source.createMediaObject(path);
+ if (object == null) {
+ Log.w(TAG, "cannot create media object: " + path);
+ }
+ return object;
+ } catch (Throwable t) {
+ Log.w(TAG, "exception in creating media object: " + path, t);
+ return null;
+ }
+ }
+ }
+
+ public MediaObject getMediaObject(String s) {
+ return getMediaObject(Path.fromString(s));
+ }
+
+ public MediaSet getMediaSet(Path path) {
+ return (MediaSet) getMediaObject(path);
+ }
+
+ public MediaSet getMediaSet(String s) {
+ return (MediaSet) getMediaObject(s);
+ }
+
+ public MediaSet[] getMediaSetsFromString(String segment) {
+ String[] seq = Path.splitSequence(segment);
+ int n = seq.length;
+ MediaSet[] sets = new MediaSet[n];
+ for (int i = 0; i < n; i++) {
+ sets[i] = getMediaSet(seq[i]);
+ }
+ return sets;
+ }
+
+ // Maps a list of Paths to MediaItems, and invoke consumer.consume()
+ // for each MediaItem (may not be in the same order as the input list).
+ // An index number is also passed to consumer.consume() to identify
+ // the original position in the input list of the corresponding Path (plus
+ // startIndex).
+ public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer,
+ int startIndex) {
+ HashMap<String, ArrayList<PathId>> map =
+ new HashMap<String, ArrayList<PathId>>();
+
+ // Group the path by the prefix.
+ int n = list.size();
+ for (int i = 0; i < n; i++) {
+ Path path = list.get(i);
+ String prefix = path.getPrefix();
+ ArrayList<PathId> group = map.get(prefix);
+ if (group == null) {
+ group = new ArrayList<PathId>();
+ map.put(prefix, group);
+ }
+ group.add(new PathId(path, i + startIndex));
+ }
+
+ // For each group, ask the corresponding media source to map it.
+ for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) {
+ String prefix = entry.getKey();
+ MediaSource source = mSourceMap.get(prefix);
+ source.mapMediaItems(entry.getValue(), consumer);
+ }
+ }
+
+ // The following methods forward the request to the proper object.
+ public int getSupportedOperations(Path path) {
+ return getMediaObject(path).getSupportedOperations();
+ }
+
+ public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) {
+ getMediaObject(path).getPanoramaSupport(callback);
+ }
+
+ public void delete(Path path) {
+ getMediaObject(path).delete();
+ }
+
+ public void rotate(Path path, int degrees) {
+ getMediaObject(path).rotate(degrees);
+ }
+
+ public Uri getContentUri(Path path) {
+ return getMediaObject(path).getContentUri();
+ }
+
+ public int getMediaType(Path path) {
+ return getMediaObject(path).getMediaType();
+ }
+
+ public Path findPathByUri(Uri uri, String type) {
+ if (uri == null) return null;
+ for (MediaSource source : mSourceMap.values()) {
+ Path path = source.findPathByUri(uri, type);
+ if (path != null) return path;
+ }
+ return null;
+ }
+
+ public Path getDefaultSetOf(Path item) {
+ MediaSource source = mSourceMap.get(item.getPrefix());
+ return source == null ? null : source.getDefaultSetOf(item);
+ }
+
+ // Returns number of bytes used by cached pictures currently downloaded.
+ public long getTotalUsedCacheSize() {
+ long sum = 0;
+ for (MediaSource source : mSourceMap.values()) {
+ sum += source.getTotalUsedCacheSize();
+ }
+ return sum;
+ }
+
+ // Returns number of bytes used by cached pictures if all pending
+ // downloads and removals are completed.
+ public long getTotalTargetCacheSize() {
+ long sum = 0;
+ for (MediaSource source : mSourceMap.values()) {
+ sum += source.getTotalTargetCacheSize();
+ }
+ return sum;
+ }
+
+ public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) {
+ NotifyBroker broker = null;
+ synchronized (mNotifierMap) {
+ broker = mNotifierMap.get(uri);
+ if (broker == null) {
+ broker = new NotifyBroker(mDefaultMainHandler);
+ mApplication.getContentResolver()
+ .registerContentObserver(uri, true, broker);
+ mNotifierMap.put(uri, broker);
+ }
+ }
+ broker.registerNotifier(notifier);
+ }
+
+ public void resume() {
+ if (++mActiveCount == 1) {
+ for (MediaSource source : mSourceMap.values()) {
+ source.resume();
+ }
+ }
+ }
+
+ public void pause() {
+ if (--mActiveCount == 0) {
+ for (MediaSource source : mSourceMap.values()) {
+ source.pause();
+ }
+ }
+ }
+
+ private static class NotifyBroker extends ContentObserver {
+ private WeakHashMap<ChangeNotifier, Object> mNotifiers =
+ new WeakHashMap<ChangeNotifier, Object>();
+
+ public NotifyBroker(Handler handler) {
+ super(handler);
+ }
+
+ public synchronized void registerNotifier(ChangeNotifier notifier) {
+ mNotifiers.put(notifier, null);
+ }
+
+ @Override
+ public synchronized void onChange(boolean selfChange) {
+ for(ChangeNotifier notifier : mNotifiers.keySet()) {
+ notifier.onChange(selfChange);
+ }
+ }
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ Path path = findPathByUri(uri, null);
+ if (path != null) {
+ MediaObject mediaObject = getMediaObject(path);
+ if (mediaObject != null) {
+ mediaObject.clearCachedPanoramaSupport();
+ }
+ }
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, int progress) {
+ // Do nothing.
+ }
+}
diff --git a/src/com/android/gallery3d/data/DataSourceType.java b/src/com/android/gallery3d/data/DataSourceType.java
new file mode 100644
index 000000000..ab534d0c3
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataSourceType.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.util.MediaSetUtils;
+
+public final class DataSourceType {
+ public static final int TYPE_NOT_CATEGORIZED = 0;
+ public static final int TYPE_LOCAL = 1;
+ public static final int TYPE_PICASA = 2;
+ public static final int TYPE_CAMERA = 3;
+
+ private static final Path PICASA_ROOT = Path.fromString("/picasa");
+ private static final Path LOCAL_ROOT = Path.fromString("/local");
+
+ public static int identifySourceType(MediaSet set) {
+ if (set == null) {
+ return TYPE_NOT_CATEGORIZED;
+ }
+
+ Path path = set.getPath();
+ if (MediaSetUtils.isCameraSource(path)) return TYPE_CAMERA;
+
+ Path prefix = path.getPrefixPath();
+
+ if (prefix == PICASA_ROOT) return TYPE_PICASA;
+ if (prefix == LOCAL_ROOT) return TYPE_LOCAL;
+
+ return TYPE_NOT_CATEGORIZED;
+ }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
new file mode 100644
index 000000000..fa709157d
--- /dev/null
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Build;
+import android.util.FloatMath;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.ui.Log;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class DecodeUtils {
+ private static final String TAG = "DecodeUtils";
+
+ private static class DecodeCanceller implements CancelListener {
+ Options mOptions;
+
+ public DecodeCanceller(Options options) {
+ mOptions = options;
+ }
+
+ @Override
+ public void onCancel() {
+ mOptions.requestCancelDecode();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ public static void setOptionsMutable(Options options) {
+ if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true;
+ }
+
+ public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ setOptionsMutable(options);
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeFileDescriptor(fd, null, options));
+ }
+
+ public static void decodeBounds(JobContext jc, FileDescriptor fd,
+ Options options) {
+ Utils.assertTrue(options != null);
+ options.inJustDecodeBounds = true;
+ jc.setCancelListener(new DecodeCanceller(options));
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ options.inJustDecodeBounds = false;
+ }
+
+ public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
+ return decode(jc, bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
+ int length, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ setOptionsMutable(options);
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(bytes, offset, length, options));
+ }
+
+ public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
+ int length, Options options) {
+ Utils.assertTrue(options != null);
+ options.inJustDecodeBounds = true;
+ jc.setCancelListener(new DecodeCanceller(options));
+ BitmapFactory.decodeByteArray(bytes, offset, length, options);
+ options.inJustDecodeBounds = false;
+ }
+
+ public static Bitmap decodeThumbnail(
+ JobContext jc, String filePath, Options options, int targetSize, int type) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(filePath);
+ FileDescriptor fd = fis.getFD();
+ return decodeThumbnail(jc, fd, options, targetSize, type);
+ } catch (Exception ex) {
+ Log.w(TAG, ex);
+ return null;
+ } finally {
+ Utils.closeSilently(fis);
+ }
+ }
+
+ public static Bitmap decodeThumbnail(
+ JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ if (jc.isCancelled()) return null;
+
+ int w = options.outWidth;
+ int h = options.outHeight;
+
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ // We center-crop the original image as it's micro thumbnail. In this case,
+ // we want to make sure the shorter side >= "targetSize".
+ float scale = (float) targetSize / Math.min(w, h);
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+
+ // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
+ // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
+ final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
+ if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
+ options.inSampleSize = BitmapUtils.computeSampleSize(
+ FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h)));
+ }
+ } else {
+ // For screen nail, we only want to keep the longer side >= targetSize.
+ float scale = (float) targetSize / Math.max(w, h);
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+ }
+
+ options.inJustDecodeBounds = false;
+ setOptionsMutable(options);
+
+ Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
+ if (result == null) return null;
+
+ // We need to resize down if the decoder does not support inSampleSize
+ // (For example, GIF images)
+ float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
+ ? Math.min(result.getWidth(), result.getHeight())
+ : Math.max(result.getWidth(), result.getHeight()));
+
+ if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
+ return ensureGLCompatibleBitmap(result);
+ }
+
+ /**
+ * Decodes the bitmap from the given byte array if the image size is larger than the given
+ * requirement.
+ *
+ * Note: The returned image may be resized down. However, both width and height must be
+ * larger than the <code>targetSize</code>.
+ */
+ public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
+ Options options, int targetSize) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ if (jc.isCancelled()) return null;
+ if (options.outWidth < targetSize || options.outHeight < targetSize) {
+ return null;
+ }
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ options.outWidth, options.outHeight, targetSize);
+ options.inJustDecodeBounds = false;
+ setOptionsMutable(options);
+
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(data, 0, data.length, options));
+ }
+
+ // TODO: This function should not be called directly from
+ // DecodeUtils.requestDecode(...), since we don't have the knowledge
+ // if the bitmap will be uploaded to GL.
+ public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
+ if (bitmap == null || bitmap.getConfig() != null) return bitmap;
+ Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
+ bitmap.recycle();
+ return newBitmap;
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, byte[] bytes, int offset, int length,
+ boolean shareable) {
+ if (offset < 0 || length <= 0 || offset + length > bytes.length) {
+ throw new IllegalArgumentException(String.format(
+ "offset = %s, length = %s, bytes = %s",
+ offset, length, bytes.length));
+ }
+
+ try {
+ return BitmapRegionDecoder.newInstance(
+ bytes, offset, length, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, String filePath, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(filePath, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, FileDescriptor fd, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(fd, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, InputStream is, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(is, shareable);
+ } catch (Throwable t) {
+ // We often cancel the creating of bitmap region decoder,
+ // so just log one line.
+ Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t);
+ return null;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset,
+ int length, BitmapFactory.Options options) {
+ if (options == null) options = new BitmapFactory.Options();
+ if (options.inSampleSize < 1) options.inSampleSize = 1;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inBitmap = (options.inSampleSize == 1)
+ ? findCachedBitmap(jc, data, offset, length, options) : null;
+ try {
+ Bitmap bitmap = decode(jc, data, offset, length, options);
+ if (options.inBitmap != null && options.inBitmap != bitmap) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ return bitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) throw e;
+
+ Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ return decode(jc, data, offset, length, options);
+ }
+ }
+
+ // This is the same as the method above except the source data comes
+ // from a file descriptor instead of a byte array.
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static Bitmap decodeUsingPool(JobContext jc,
+ FileDescriptor fileDescriptor, Options options) {
+ if (options == null) options = new BitmapFactory.Options();
+ if (options.inSampleSize < 1) options.inSampleSize = 1;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inBitmap = (options.inSampleSize == 1)
+ ? findCachedBitmap(jc, fileDescriptor, options) : null;
+ try {
+ Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
+ if (options.inBitmap != null && options.inBitmap != bitmap) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ return bitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) throw e;
+
+ Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ return decode(jc, fileDescriptor, options);
+ }
+ }
+
+ private static Bitmap findCachedBitmap(JobContext jc, byte[] data,
+ int offset, int length, Options options) {
+ decodeBounds(jc, data, offset, length, options);
+ return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+ }
+
+ private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor,
+ Options options) {
+ decodeBounds(jc, fileDescriptor, options);
+ return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
new file mode 100644
index 000000000..be7820b01
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.LruCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DownloadEntry.Columns;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class DownloadCache {
+ private static final String TAG = "DownloadCache";
+ private static final int MAX_DELETE_COUNT = 16;
+ private static final int LRU_CAPACITY = 4;
+
+ private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName();
+
+ private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA};
+ private static final String WHERE_HASH_AND_URL = String.format(
+ "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL);
+ private static final int QUERY_INDEX_ID = 0;
+ private static final int QUERY_INDEX_DATA = 1;
+
+ private static final String FREESPACE_PROJECTION[] = {
+ Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE};
+ private static final String FREESPACE_ORDER_BY =
+ String.format("%s ASC", Columns.LAST_ACCESS);
+ private static final int FREESPACE_IDNEX_ID = 0;
+ private static final int FREESPACE_IDNEX_DATA = 1;
+ private static final int FREESPACE_INDEX_CONTENT_URL = 2;
+ private static final int FREESPACE_INDEX_CONTENT_SIZE = 3;
+
+ private static final String ID_WHERE = Columns.ID + " = ?";
+
+ private static final String SUM_PROJECTION[] =
+ {String.format("sum(%s)", Columns.CONTENT_SIZE)};
+ private static final int SUM_INDEX_SUM = 0;
+
+ private final LruCache<String, Entry> mEntryMap =
+ new LruCache<String, Entry>(LRU_CAPACITY);
+ private final HashMap<String, DownloadTask> mTaskMap =
+ new HashMap<String, DownloadTask>();
+ private final File mRoot;
+ private final GalleryApp mApplication;
+ private final SQLiteDatabase mDatabase;
+ private final long mCapacity;
+
+ private long mTotalBytes = 0;
+ private boolean mInitialized = false;
+
+ public DownloadCache(GalleryApp application, File root, long capacity) {
+ mRoot = Utils.checkNotNull(root);
+ mApplication = Utils.checkNotNull(application);
+ mCapacity = capacity;
+ mDatabase = new DatabaseHelper(application.getAndroidContext())
+ .getWritableDatabase();
+ }
+
+ private Entry findEntryInDatabase(String stringUrl) {
+ long hash = Utils.crc64Long(stringUrl);
+ String whereArgs[] = {String.valueOf(hash), stringUrl};
+ Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION,
+ WHERE_HASH_AND_URL, whereArgs, null, null, null);
+ try {
+ if (cursor.moveToNext()) {
+ File file = new File(cursor.getString(QUERY_INDEX_DATA));
+ long id = cursor.getInt(QUERY_INDEX_ID);
+ Entry entry = null;
+ synchronized (mEntryMap) {
+ entry = mEntryMap.get(stringUrl);
+ if (entry == null) {
+ entry = new Entry(id, file);
+ mEntryMap.put(stringUrl, entry);
+ }
+ }
+ return entry;
+ }
+ } finally {
+ cursor.close();
+ }
+ return null;
+ }
+
+ public Entry download(JobContext jc, URL url) {
+ if (!mInitialized) initialize();
+
+ String stringUrl = url.toString();
+
+ // First find in the entry-pool
+ synchronized (mEntryMap) {
+ Entry entry = mEntryMap.get(stringUrl);
+ if (entry != null) {
+ updateLastAccess(entry.mId);
+ return entry;
+ }
+ }
+
+ // Then, find it in database
+ TaskProxy proxy = new TaskProxy();
+ synchronized (mTaskMap) {
+ Entry entry = findEntryInDatabase(stringUrl);
+ if (entry != null) {
+ updateLastAccess(entry.mId);
+ return entry;
+ }
+
+ // Finally, we need to download the file ....
+ // First check if we are downloading it now ...
+ DownloadTask task = mTaskMap.get(stringUrl);
+ if (task == null) { // if not, start the download task now
+ task = new DownloadTask(stringUrl);
+ mTaskMap.put(stringUrl, task);
+ task.mFuture = mApplication.getThreadPool().submit(task, task);
+ }
+ task.addProxy(proxy);
+ }
+
+ return proxy.get(jc);
+ }
+
+ private void updateLastAccess(long id) {
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ mDatabase.update(TABLE_NAME, values,
+ ID_WHERE, new String[] {String.valueOf(id)});
+ }
+
+ private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
+ if (mTotalBytes <= mCapacity) return;
+ Cursor cursor = mDatabase.query(TABLE_NAME,
+ FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY);
+ try {
+ while (maxDeleteFileCount > 0
+ && mTotalBytes > mCapacity && cursor.moveToNext()) {
+ long id = cursor.getLong(FREESPACE_IDNEX_ID);
+ String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL);
+ long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE);
+ String path = cursor.getString(FREESPACE_IDNEX_DATA);
+ boolean containsKey;
+ synchronized (mEntryMap) {
+ containsKey = mEntryMap.containsKey(url);
+ }
+ if (!containsKey) {
+ --maxDeleteFileCount;
+ mTotalBytes -= size;
+ new File(path).delete();
+ mDatabase.delete(TABLE_NAME,
+ ID_WHERE, new String[]{String.valueOf(id)});
+ } else {
+ // skip delete, since it is being used
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private synchronized long insertEntry(String url, File file) {
+ long size = file.length();
+ mTotalBytes += size;
+
+ ContentValues values = new ContentValues();
+ String hashCode = String.valueOf(Utils.crc64Long(url));
+ values.put(Columns.DATA, file.getAbsolutePath());
+ values.put(Columns.HASH_CODE, hashCode);
+ values.put(Columns.CONTENT_URL, url);
+ values.put(Columns.CONTENT_SIZE, size);
+ values.put(Columns.LAST_UPDATED, System.currentTimeMillis());
+ return mDatabase.insert(TABLE_NAME, "", values);
+ }
+
+ private synchronized void initialize() {
+ if (mInitialized) return;
+ mInitialized = true;
+ if (!mRoot.isDirectory()) mRoot.mkdirs();
+ if (!mRoot.isDirectory()) {
+ throw new RuntimeException("cannot create " + mRoot.getAbsolutePath());
+ }
+
+ Cursor cursor = mDatabase.query(
+ TABLE_NAME, SUM_PROJECTION, null, null, null, null, null);
+ mTotalBytes = 0;
+ try {
+ if (cursor.moveToNext()) {
+ mTotalBytes = cursor.getLong(SUM_INDEX_SUM);
+ }
+ } finally {
+ cursor.close();
+ }
+ if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+
+ private final class DatabaseHelper extends SQLiteOpenHelper {
+ public static final String DATABASE_NAME = "download.db";
+ public static final int DATABASE_VERSION = 2;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ DownloadEntry.SCHEMA.createTables(db);
+ // Delete old files
+ for (File file : mRoot.listFiles()) {
+ if (!file.delete()) {
+ Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
+ }
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ //reset everything
+ DownloadEntry.SCHEMA.dropTables(db);
+ onCreate(db);
+ }
+ }
+
+ public class Entry {
+ public File cacheFile;
+ protected long mId;
+
+ Entry(long id, File cacheFile) {
+ mId = id;
+ this.cacheFile = Utils.checkNotNull(cacheFile);
+ }
+ }
+
+ private class DownloadTask implements Job<File>, FutureListener<File> {
+ private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>();
+ private Future<File> mFuture;
+ private final String mUrl;
+
+ public DownloadTask(String url) {
+ mUrl = Utils.checkNotNull(url);
+ }
+
+ public void removeProxy(TaskProxy proxy) {
+ synchronized (mTaskMap) {
+ Utils.assertTrue(mProxySet.remove(proxy));
+ if (mProxySet.isEmpty()) {
+ mFuture.cancel();
+ mTaskMap.remove(mUrl);
+ }
+ }
+ }
+
+ // should be used in synchronized block of mDatabase
+ public void addProxy(TaskProxy proxy) {
+ proxy.mTask = this;
+ mProxySet.add(proxy);
+ }
+
+ @Override
+ public void onFutureDone(Future<File> future) {
+ File file = future.get();
+ long id = 0;
+ if (file != null) { // insert to database
+ id = insertEntry(mUrl, file);
+ }
+
+ if (future.isCancelled()) {
+ Utils.assertTrue(mProxySet.isEmpty());
+ return;
+ }
+
+ synchronized (mTaskMap) {
+ Entry entry = null;
+ synchronized (mEntryMap) {
+ if (file != null) {
+ entry = new Entry(id, file);
+ Utils.assertTrue(mEntryMap.put(mUrl, entry) == null);
+ }
+ }
+ for (TaskProxy proxy : mProxySet) {
+ proxy.setResult(entry);
+ }
+ mTaskMap.remove(mUrl);
+ freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+ }
+
+ @Override
+ public File run(JobContext jc) {
+ // TODO: utilize etag
+ jc.setMode(ThreadPool.MODE_NETWORK);
+ File tempFile = null;
+ try {
+ URL url = new URL(mUrl);
+ tempFile = File.createTempFile("cache", ".tmp", mRoot);
+ // download from url to tempFile
+ jc.setMode(ThreadPool.MODE_NETWORK);
+ boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile);
+ jc.setMode(ThreadPool.MODE_NONE);
+ if (downloaded) return tempFile;
+ } catch (Exception e) {
+ Log.e(TAG, String.format("fail to download %s", mUrl), e);
+ } finally {
+ jc.setMode(ThreadPool.MODE_NONE);
+ }
+ if (tempFile != null) tempFile.delete();
+ return null;
+ }
+ }
+
+ public static class TaskProxy {
+ private DownloadTask mTask;
+ private boolean mIsCancelled = false;
+ private Entry mEntry;
+
+ synchronized void setResult(Entry entry) {
+ if (mIsCancelled) return;
+ mEntry = entry;
+ notifyAll();
+ }
+
+ public synchronized Entry get(JobContext jc) {
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ mTask.removeProxy(TaskProxy.this);
+ synchronized (TaskProxy.this) {
+ mIsCancelled = true;
+ TaskProxy.this.notifyAll();
+ }
+ }
+ });
+ while (!mIsCancelled && mEntry == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "ignore interrupt", e);
+ }
+ }
+ jc.setCancelListener(null);
+ return mEntry;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/DownloadEntry.java b/src/com/android/gallery3d/data/DownloadEntry.java
new file mode 100644
index 000000000..578523f73
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadEntry.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.common.Entry;
+import com.android.gallery3d.common.EntrySchema;
+
+
+@Entry.Table("download")
+public class DownloadEntry extends Entry {
+ public static final EntrySchema SCHEMA = new EntrySchema(DownloadEntry.class);
+
+ public static interface Columns extends Entry.Columns {
+ public static final String HASH_CODE = "hash_code";
+ public static final String CONTENT_URL = "content_url";
+ public static final String CONTENT_SIZE = "_size";
+ public static final String ETAG = "etag";
+ public static final String LAST_ACCESS = "last_access";
+ public static final String LAST_UPDATED = "last_updated";
+ public static final String DATA = "_data";
+ }
+
+ @Column(value = "hash_code", indexed = true)
+ public long hashCode;
+
+ @Column("content_url")
+ public String contentUrl;
+
+ @Column("_size")
+ public long contentSize;
+
+ @Column("etag")
+ public String eTag;
+
+ @Column(value = "last_access", indexed = true)
+ public long lastAccessTime;
+
+ @Column(value = "last_updated")
+ public long lastUpdatedTime;
+
+ @Column("_data")
+ public String path;
+
+ @Override
+ public String toString() {
+ // Note: THIS IS REQUIRED. We used all the fields here. Otherwise,
+ // ProGuard will remove these UNUSED fields. However, these
+ // fields are needed to generate database.
+ return new StringBuilder()
+ .append("hash_code: ").append(hashCode).append(", ")
+ .append("content_url").append(contentUrl).append(", ")
+ .append("_size").append(contentSize).append(", ")
+ .append("etag").append(eTag).append(", ")
+ .append("last_access").append(lastAccessTime).append(", ")
+ .append("last_updated").append(lastUpdatedTime).append(",")
+ .append("_data").append(path)
+ .toString();
+ }
+}
diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java
new file mode 100644
index 000000000..137898e91
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.URL;
+
+public class DownloadUtils {
+ private static final String TAG = "DownloadService";
+
+ public static boolean requestDownload(JobContext jc, URL url, File file) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file);
+ return download(jc, url, fos);
+ } catch (Throwable t) {
+ return false;
+ } finally {
+ Utils.closeSilently(fos);
+ }
+ }
+
+ public static void dump(JobContext jc, InputStream is, OutputStream os)
+ throws IOException {
+ byte buffer[] = new byte[4096];
+ int rc = is.read(buffer, 0, buffer.length);
+ final Thread thread = Thread.currentThread();
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ thread.interrupt();
+ }
+ });
+ while (rc > 0) {
+ if (jc.isCancelled()) throw new InterruptedIOException();
+ os.write(buffer, 0, rc);
+ rc = is.read(buffer, 0, buffer.length);
+ }
+ jc.setCancelListener(null);
+ Thread.interrupted(); // consume the interrupt signal
+ }
+
+ public static boolean download(JobContext jc, URL url, OutputStream output) {
+ InputStream input = null;
+ try {
+ input = url.openStream();
+ dump(jc, input, output);
+ return true;
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to download", t);
+ return false;
+ } finally {
+ Utils.closeSilently(input);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/data/EmptyAlbumImage.java b/src/com/android/gallery3d/data/EmptyAlbumImage.java
new file mode 100644
index 000000000..6f8c37c6b
--- /dev/null
+++ b/src/com/android/gallery3d/data/EmptyAlbumImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class EmptyAlbumImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EmptyAlbumImage";
+
+ public EmptyAlbumImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_empty);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_BACK;
+ }
+}
diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java
new file mode 100644
index 000000000..950e7de18
--- /dev/null
+++ b/src/com/android/gallery3d/data/Exif.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Exif {
+ private static final String TAG = "CameraExif";
+
+ // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+ public static int getOrientation(InputStream is) {
+ if (is == null) {
+ return 0;
+ }
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(is);
+ Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (val == null) {
+ return 0;
+ } else {
+ return ExifInterface.getRotationForOrientationValue(val.shortValue());
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read EXIF orientation", e);
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java
new file mode 100644
index 000000000..d2dc22bfc
--- /dev/null
+++ b/src/com/android/gallery3d/data/Face.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.Utils;
+
+import java.util.StringTokenizer;
+
+public class Face implements Comparable<Face> {
+ private String mName;
+ private String mPersonId;
+ private Rect mPosition;
+
+ public Face(String name, String personId, String rect) {
+ mName = name;
+ mPersonId = personId;
+ Utils.assertTrue(mName != null && mPersonId != null && rect != null);
+ StringTokenizer tokenizer = new StringTokenizer(rect);
+ mPosition = new Rect();
+ while (tokenizer.hasMoreElements()) {
+ mPosition.left = Integer.parseInt(tokenizer.nextToken());
+ mPosition.top = Integer.parseInt(tokenizer.nextToken());
+ mPosition.right = Integer.parseInt(tokenizer.nextToken());
+ mPosition.bottom = Integer.parseInt(tokenizer.nextToken());
+ }
+ }
+
+ public Rect getPosition() {
+ return mPosition;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Face) {
+ Face face = (Face) obj;
+ return mPersonId.equals(face.mPersonId);
+ }
+ return false;
+ }
+
+ @Override
+ public int compareTo(Face another) {
+ return mName.compareTo(another.mName);
+ }
+}
diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java
new file mode 100644
index 000000000..819915edb
--- /dev/null
+++ b/src/com/android/gallery3d/data/FaceClustering.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import java.util.ArrayList;
+import java.util.TreeMap;
+
+public class FaceClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FaceClustering";
+
+ private FaceCluster[] mClusters;
+ private String mUntaggedString;
+ private Context mContext;
+
+ private class FaceCluster {
+ ArrayList<Path> mPaths = new ArrayList<Path>();
+ String mName;
+ MediaItem mCoverItem;
+ Rect mCoverRegion;
+ int mCoverFaceIndex;
+
+ public FaceCluster(String name) {
+ mName = name;
+ }
+
+ public void add(MediaItem item, int faceIndex) {
+ Path path = item.getPath();
+ mPaths.add(path);
+ Face[] faces = item.getFaces();
+ if (faces != null) {
+ Face face = faces[faceIndex];
+ if (mCoverItem == null) {
+ mCoverItem = item;
+ mCoverRegion = face.getPosition();
+ mCoverFaceIndex = faceIndex;
+ } else {
+ Rect region = face.getPosition();
+ if (mCoverRegion.width() < region.width() &&
+ mCoverRegion.height() < region.height()) {
+ mCoverItem = item;
+ mCoverRegion = face.getPosition();
+ mCoverFaceIndex = faceIndex;
+ }
+ }
+ }
+ }
+
+ public int size() {
+ return mPaths.size();
+ }
+
+ public MediaItem getCover() {
+ if (mCoverItem != null) {
+ if (PicasaSource.isPicasaImage(mCoverItem)) {
+ return PicasaSource.getFaceItem(mContext, mCoverItem, mCoverFaceIndex);
+ } else {
+ return mCoverItem;
+ }
+ }
+ return null;
+ }
+ }
+
+ public FaceClustering(Context context) {
+ mUntaggedString = context.getResources().getString(R.string.untagged);
+ mContext = context;
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final TreeMap<Face, FaceCluster> map =
+ new TreeMap<Face, FaceCluster>();
+ final FaceCluster untagged = new FaceCluster(mUntaggedString);
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ Face[] faces = item.getFaces();
+ if (faces == null || faces.length == 0) {
+ untagged.add(item, -1);
+ return;
+ }
+ for (int j = 0; j < faces.length; j++) {
+ Face face = faces[j];
+ FaceCluster cluster = map.get(face);
+ if (cluster == null) {
+ cluster = new FaceCluster(face.getName());
+ map.put(face, cluster);
+ }
+ cluster.add(item, j);
+ }
+ }
+ });
+
+ int m = map.size();
+ mClusters = map.values().toArray(new FaceCluster[m + ((untagged.size() > 0) ? 1 : 0)]);
+ if (untagged.size() > 0) {
+ mClusters[m] = untagged;
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.length;
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters[index].mPaths;
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mClusters[index].mName;
+ }
+
+ @Override
+ public MediaItem getClusterCover(int index) {
+ return mClusters[index].getCover();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterDeleteSet.java b/src/com/android/gallery3d/data/FilterDeleteSet.java
new file mode 100644
index 000000000..c76412ff8
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterDeleteSet.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+// FilterDeleteSet filters a base MediaSet to remove some deletion items (we
+// expect the number to be small). The user can use the following methods to
+// add/remove deletion items:
+//
+// void addDeletion(Path path, int index);
+// void removeDelection(Path path);
+// void clearDeletion();
+// int getNumberOfDeletions();
+//
+public class FilterDeleteSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterDeleteSet";
+
+ private static final int REQUEST_ADD = 1;
+ private static final int REQUEST_REMOVE = 2;
+ private static final int REQUEST_CLEAR = 3;
+
+ private static class Request {
+ int type; // one of the REQUEST_* constants
+ Path path;
+ int indexHint;
+ public Request(int type, Path path, int indexHint) {
+ this.type = type;
+ this.path = path;
+ this.indexHint = indexHint;
+ }
+ }
+
+ private static class Deletion {
+ Path path;
+ int index;
+ public Deletion(Path path, int index) {
+ this.path = path;
+ this.index = index;
+ }
+ }
+
+ // The underlying MediaSet
+ private final MediaSet mBaseSet;
+
+ // Pending Requests
+ private ArrayList<Request> mRequests = new ArrayList<Request>();
+
+ // Deletions currently in effect, ordered by index
+ private ArrayList<Deletion> mCurrent = new ArrayList<Deletion>();
+
+ public FilterDeleteSet(Path path, MediaSet baseSet) {
+ super(path, INVALID_DATA_VERSION);
+ mBaseSet = baseSet;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBaseSet.isCameraRoll();
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return mBaseSet.getMediaItemCount() - mCurrent.size();
+ }
+
+ // Gets the MediaItems whose (post-deletion) index are in the range [start,
+ // start + count). Because we remove some of the MediaItems, the index need
+ // to be adjusted.
+ //
+ // For example, if there are 12 items in total. The deleted items are 3, 5,
+ // 10, and the the requested range is [3, 7]:
+ //
+ // The original index: 0 1 2 3 4 5 6 7 8 9 A B C
+ // The deleted items: X X X
+ // The new index: 0 1 2 3 4 5 6 7 8 9
+ // Requested: * * * * *
+ //
+ // We need to figure out the [3, 7] actually maps to the original index 4,
+ // 6, 7, 8, 9.
+ //
+ // We can break the MediaItems into segments, each segment other than the
+ // last one ends in a deleted item. The difference between the new index and
+ // the original index increases with each segment:
+ //
+ // 0 1 2 X (new index = old index)
+ // 4 X (new index = old index - 1)
+ // 6 7 8 9 X (new index = old index - 2)
+ // B C (new index = old index - 3)
+ //
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ if (count <= 0) return new ArrayList<MediaItem>();
+
+ int end = start + count - 1;
+ int n = mCurrent.size();
+ // Find the segment that "start" falls into. Count the number of items
+ // not yet deleted until it reaches "start".
+ int i = 0;
+ for (i = 0; i < n; i++) {
+ Deletion d = mCurrent.get(i);
+ if (d.index - i > start) break;
+ }
+ // Find the segment that "end" falls into.
+ int j = i;
+ for (; j < n; j++) {
+ Deletion d = mCurrent.get(j);
+ if (d.index - j > end) break;
+ }
+
+ // Now get enough to cover deleted items in [start, end]
+ ArrayList<MediaItem> base = mBaseSet.getMediaItem(start + i, count + (j - i));
+
+ // Remove the deleted items.
+ for (int m = j - 1; m >= i; m--) {
+ Deletion d = mCurrent.get(m);
+ int k = d.index - (start + i);
+ base.remove(k);
+ }
+ return base;
+ }
+
+ // We apply the pending requests in the mRequests to construct mCurrent in reload().
+ @Override
+ public long reload() {
+ boolean newData = mBaseSet.reload() > mDataVersion;
+ synchronized (mRequests) {
+ if (!newData && mRequests.isEmpty()) {
+ return mDataVersion;
+ }
+ for (int i = 0; i < mRequests.size(); i++) {
+ Request r = mRequests.get(i);
+ switch (r.type) {
+ case REQUEST_ADD: {
+ // Add the path into mCurrent if there is no duplicate.
+ int n = mCurrent.size();
+ int j;
+ for (j = 0; j < n; j++) {
+ if (mCurrent.get(j).path == r.path) break;
+ }
+ if (j == n) {
+ mCurrent.add(new Deletion(r.path, r.indexHint));
+ }
+ break;
+ }
+ case REQUEST_REMOVE: {
+ // Remove the path from mCurrent.
+ int n = mCurrent.size();
+ for (int j = 0; j < n; j++) {
+ if (mCurrent.get(j).path == r.path) {
+ mCurrent.remove(j);
+ break;
+ }
+ }
+ break;
+ }
+ case REQUEST_CLEAR: {
+ mCurrent.clear();
+ break;
+ }
+ }
+ }
+ mRequests.clear();
+ }
+
+ if (!mCurrent.isEmpty()) {
+ // See if the elements in mCurrent can be found in the MediaSet. We
+ // don't want to search the whole mBaseSet, so we just search a
+ // small window that contains the index hints (plus some margin).
+ int minIndex = mCurrent.get(0).index;
+ int maxIndex = minIndex;
+ for (int i = 1; i < mCurrent.size(); i++) {
+ Deletion d = mCurrent.get(i);
+ minIndex = Math.min(d.index, minIndex);
+ maxIndex = Math.max(d.index, maxIndex);
+ }
+
+ int n = mBaseSet.getMediaItemCount();
+ int from = Math.max(minIndex - 5, 0);
+ int to = Math.min(maxIndex + 5, n);
+ ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from);
+ ArrayList<Deletion> result = new ArrayList<Deletion>();
+ for (int i = 0; i < items.size(); i++) {
+ MediaItem item = items.get(i);
+ if (item == null) continue;
+ Path p = item.getPath();
+ // Find the matching path in mCurrent, if found move it to result
+ for (int j = 0; j < mCurrent.size(); j++) {
+ Deletion d = mCurrent.get(j);
+ if (d.path == p) {
+ d.index = from + i;
+ result.add(d);
+ mCurrent.remove(j);
+ break;
+ }
+ }
+ }
+ mCurrent = result;
+ }
+
+ mDataVersion = nextVersionNumber();
+ return mDataVersion;
+ }
+
+ private void sendRequest(int type, Path path, int indexHint) {
+ Request r = new Request(type, path, indexHint);
+ synchronized (mRequests) {
+ mRequests.add(r);
+ }
+ notifyContentChanged();
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ public void addDeletion(Path path, int indexHint) {
+ sendRequest(REQUEST_ADD, path, indexHint);
+ }
+
+ public void removeDeletion(Path path) {
+ sendRequest(REQUEST_REMOVE, path, 0 /* unused */);
+ }
+
+ public void clearDeletion() {
+ sendRequest(REQUEST_CLEAR, null /* unused */ , 0 /* unused */);
+ }
+
+ // Returns number of deletions _in effect_ (the number will only gets
+ // updated after a reload()).
+ public int getNumberOfDeletions() {
+ return mCurrent.size();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterEmptyPromptSet.java b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
new file mode 100644
index 000000000..b576e06d4
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class FilterEmptyPromptSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterEmptyPromptSet";
+
+ private ArrayList<MediaItem> mEmptyItem;
+ private MediaSet mBaseSet;
+
+ public FilterEmptyPromptSet(Path path, MediaSet baseSet, MediaItem emptyItem) {
+ super(path, INVALID_DATA_VERSION);
+ mEmptyItem = new ArrayList<MediaItem>(1);
+ mEmptyItem.add(emptyItem);
+ mBaseSet = baseSet;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ int itemCount = mBaseSet.getMediaItemCount();
+ if (itemCount > 0) {
+ return itemCount;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ int itemCount = mBaseSet.getMediaItemCount();
+ if (itemCount > 0) {
+ return mBaseSet.getMediaItem(start, count);
+ } else if (start == 0 && count == 1) {
+ return mEmptyItem;
+ } else {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBaseSet.isCameraRoll();
+ }
+
+ @Override
+ public long reload() {
+ return mBaseSet.reload();
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java
new file mode 100644
index 000000000..d689fe336
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterSource.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class FilterSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterSource";
+ private static final int FILTER_BY_MEDIATYPE = 0;
+ private static final int FILTER_BY_DELETE = 1;
+ private static final int FILTER_BY_EMPTY = 2;
+ private static final int FILTER_BY_EMPTY_ITEM = 3;
+ private static final int FILTER_BY_CAMERA_SHORTCUT = 4;
+ private static final int FILTER_BY_CAMERA_SHORTCUT_ITEM = 5;
+
+ public static final String FILTER_EMPTY_ITEM = "/filter/empty_prompt";
+ public static final String FILTER_CAMERA_SHORTCUT = "/filter/camera_shortcut";
+ private static final String FILTER_CAMERA_SHORTCUT_ITEM = "/filter/camera_shortcut_item";
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+ private MediaItem mEmptyItem;
+ private MediaItem mCameraShortcutItem;
+
+ public FilterSource(GalleryApp application) {
+ super("filter");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE);
+ mMatcher.add("/filter/delete/*", FILTER_BY_DELETE);
+ mMatcher.add("/filter/empty/*", FILTER_BY_EMPTY);
+ mMatcher.add(FILTER_EMPTY_ITEM, FILTER_BY_EMPTY_ITEM);
+ mMatcher.add(FILTER_CAMERA_SHORTCUT, FILTER_BY_CAMERA_SHORTCUT);
+ mMatcher.add(FILTER_CAMERA_SHORTCUT_ITEM, FILTER_BY_CAMERA_SHORTCUT_ITEM);
+
+ mEmptyItem = new EmptyAlbumImage(Path.fromString(FILTER_EMPTY_ITEM),
+ mApplication);
+ mCameraShortcutItem = new CameraShortcutImage(
+ Path.fromString(FILTER_CAMERA_SHORTCUT_ITEM), mApplication);
+ }
+
+ // The name we accept are:
+ // /filter/mediatype/k/{set} where k is the media type we want.
+ // /filter/delete/{set}
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ int matchType = mMatcher.match(path);
+ DataManager dataManager = mApplication.getDataManager();
+ switch (matchType) {
+ case FILTER_BY_MEDIATYPE: {
+ int mediaType = mMatcher.getIntVar(0);
+ String setsName = mMatcher.getVar(1);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterTypeSet(path, dataManager, sets[0], mediaType);
+ }
+ case FILTER_BY_DELETE: {
+ String setsName = mMatcher.getVar(0);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterDeleteSet(path, sets[0]);
+ }
+ case FILTER_BY_EMPTY: {
+ String setsName = mMatcher.getVar(0);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterEmptyPromptSet(path, sets[0], mEmptyItem);
+ }
+ case FILTER_BY_EMPTY_ITEM: {
+ return mEmptyItem;
+ }
+ case FILTER_BY_CAMERA_SHORTCUT: {
+ return new SingleItemAlbum(path, mCameraShortcutItem);
+ }
+ case FILTER_BY_CAMERA_SHORTCUT_ITEM: {
+ return mCameraShortcutItem;
+ }
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterTypeSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java
new file mode 100644
index 000000000..477ef73ad
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterTypeSet.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+// FilterTypeSet filters a base MediaSet according to a matching media type.
+public class FilterTypeSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterTypeSet";
+
+ private final DataManager mDataManager;
+ private final MediaSet mBaseSet;
+ private final int mMediaType;
+ private final ArrayList<Path> mPaths = new ArrayList<Path>();
+ private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+
+ public FilterTypeSet(Path path, DataManager dataManager, MediaSet baseSet,
+ int mediaType) {
+ super(path, INVALID_DATA_VERSION);
+ mDataManager = dataManager;
+ mBaseSet = baseSet;
+ mMediaType = mediaType;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return mAlbums.get(index);
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mAlbums.size();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return mPaths.size();
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ return ClusterAlbum.getMediaItemFromPath(
+ mPaths, start, count, mDataManager);
+ }
+
+ @Override
+ public long reload() {
+ if (mBaseSet.reload() > mDataVersion) {
+ updateData();
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ private void updateData() {
+ // Albums
+ mAlbums.clear();
+ String basePath = "/filter/mediatype/" + mMediaType;
+
+ for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) {
+ MediaSet set = mBaseSet.getSubMediaSet(i);
+ String filteredPath = basePath + "/{" + set.getPath().toString() + "}";
+ MediaSet filteredSet = mDataManager.getMediaSet(filteredPath);
+ filteredSet.reload();
+ if (filteredSet.getMediaItemCount() > 0
+ || filteredSet.getSubMediaSetCount() > 0) {
+ mAlbums.add(filteredSet);
+ }
+ }
+
+ // Items
+ mPaths.clear();
+ final int total = mBaseSet.getMediaItemCount();
+ final Path[] buf = new Path[total];
+
+ mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if (item.getMediaType() == mMediaType) {
+ if (index < 0 || index >= total) return;
+ Path path = item.getPath();
+ buf[index] = path;
+ }
+ }
+ });
+
+ for (int i = 0; i < total; i++) {
+ if (buf[i] != null) {
+ mPaths.add(buf[i]);
+ }
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_SHARE | SUPPORT_DELETE;
+ }
+
+ @Override
+ public void delete() {
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) {
+ item.delete();
+ }
+ }
+ };
+ mDataManager.mapMediaItems(mPaths, consumer, 0);
+ }
+}
diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
new file mode 100644
index 000000000..6cbc5c5ea
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+abstract class ImageCacheRequest implements Job<Bitmap> {
+ private static final String TAG = "ImageCacheRequest";
+
+ protected GalleryApp mApplication;
+ private Path mPath;
+ private int mType;
+ private int mTargetSize;
+ private long mTimeModified;
+
+ public ImageCacheRequest(GalleryApp application,
+ Path path, long timeModified, int type, int targetSize) {
+ mApplication = application;
+ mPath = path;
+ mType = type;
+ mTargetSize = targetSize;
+ mTimeModified = timeModified;
+ }
+
+ private String debugTag() {
+ return mPath + "," + mTimeModified + "," +
+ ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" :
+ (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?");
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ ImageCacheService cacheService = mApplication.getImageCacheService();
+
+ BytesBuffer buffer = MediaItem.getBytesBufferPool().get();
+ try {
+ boolean found = cacheService.getImageData(mPath, mTimeModified, mType, buffer);
+ if (jc.isCancelled()) return null;
+ if (found) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap;
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = DecodeUtils.decodeUsingPool(jc,
+ buffer.data, buffer.offset, buffer.length, options);
+ } else {
+ bitmap = DecodeUtils.decodeUsingPool(jc,
+ buffer.data, buffer.offset, buffer.length, options);
+ }
+ if (bitmap == null && !jc.isCancelled()) {
+ Log.w(TAG, "decode cached failed " + debugTag());
+ }
+ return bitmap;
+ }
+ } finally {
+ MediaItem.getBytesBufferPool().recycle(buffer);
+ }
+ Bitmap bitmap = onDecodeOriginal(jc, mType);
+ if (jc.isCancelled()) return null;
+
+ if (bitmap == null) {
+ Log.w(TAG, "decode orig failed " + debugTag());
+ return null;
+ }
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeAndCropCenter(bitmap, mTargetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true);
+ }
+ if (jc.isCancelled()) return null;
+
+ byte[] array = BitmapUtils.compressToBytes(bitmap);
+ if (jc.isCancelled()) return null;
+
+ cacheService.putImageData(mPath, mTimeModified, mType, array);
+ return bitmap;
+ }
+
+ public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize);
+}
diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java
new file mode 100644
index 000000000..1c7cb8c5e
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.common.BlobCache.LookupRequest;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class ImageCacheService {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ImageCacheService";
+
+ private static final String IMAGE_CACHE_FILE = "imgcache";
+ private static final int IMAGE_CACHE_MAX_ENTRIES = 5000;
+ private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024;
+ private static final int IMAGE_CACHE_VERSION = 7;
+
+ private BlobCache mCache;
+
+ public ImageCacheService(Context context) {
+ mCache = CacheManager.getCache(context, IMAGE_CACHE_FILE,
+ IMAGE_CACHE_MAX_ENTRIES, IMAGE_CACHE_MAX_BYTES,
+ IMAGE_CACHE_VERSION);
+ }
+
+ /**
+ * Gets the cached image data for the given <code>path</code>,
+ * <code>timeModified</code> and <code>type</code>.
+ *
+ * The image data will be stored in <code>buffer.data</code>, started from
+ * <code>buffer.offset</code> for <code>buffer.length</code> bytes. If the
+ * buffer.data is not big enough, a new byte array will be allocated and returned.
+ *
+ * @return true if the image data is found; false if not found.
+ */
+ public boolean getImageData(Path path, long timeModified, int type, BytesBuffer buffer) {
+ byte[] key = makeKey(path, timeModified, type);
+ long cacheKey = Utils.crc64Long(key);
+ try {
+ LookupRequest request = new LookupRequest();
+ request.key = cacheKey;
+ request.buffer = buffer.data;
+ synchronized (mCache) {
+ if (!mCache.lookup(request)) return false;
+ }
+ if (isSameKey(key, request.buffer)) {
+ buffer.data = request.buffer;
+ buffer.offset = key.length;
+ buffer.length = request.length - buffer.offset;
+ return true;
+ }
+ } catch (IOException ex) {
+ // ignore.
+ }
+ return false;
+ }
+
+ public void putImageData(Path path, long timeModified, int type, byte[] value) {
+ byte[] key = makeKey(path, timeModified, type);
+ long cacheKey = Utils.crc64Long(key);
+ ByteBuffer buffer = ByteBuffer.allocate(key.length + value.length);
+ buffer.put(key);
+ buffer.put(value);
+ synchronized (mCache) {
+ try {
+ mCache.insert(cacheKey, buffer.array());
+ } catch (IOException ex) {
+ // ignore.
+ }
+ }
+ }
+
+ public void clearImageData(Path path, long timeModified, int type) {
+ byte[] key = makeKey(path, timeModified, type);
+ long cacheKey = Utils.crc64Long(key);
+ synchronized (mCache) {
+ try {
+ mCache.clearEntry(cacheKey);
+ } catch (IOException ex) {
+ // ignore.
+ }
+ }
+ }
+
+ private static byte[] makeKey(Path path, long timeModified, int type) {
+ return GalleryUtils.getBytes(path.toString() + "+" + timeModified + "+" + type);
+ }
+
+ private static boolean isSameKey(byte[] key, byte[] buffer) {
+ int n = key.length;
+ if (buffer.length < n) {
+ return false;
+ }
+ for (int i = 0; i < n; ++i) {
+ if (key[i] != buffer[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java
new file mode 100644
index 000000000..7b7015af6
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+
+// LocalAlbumSet lists all media items in one bucket on local storage.
+// The media items need to be all images or all videos, but not both.
+public class LocalAlbum extends MediaSet {
+ private static final String TAG = "LocalAlbum";
+ private static final String[] COUNT_PROJECTION = { "count(*)" };
+
+ private static final int INVALID_COUNT = -1;
+ private final String mWhereClause;
+ private final String mOrderClause;
+ private final Uri mBaseUri;
+ private final String[] mProjection;
+
+ private final GalleryApp mApplication;
+ private final ContentResolver mResolver;
+ private final int mBucketId;
+ private final String mName;
+ private final boolean mIsImage;
+ private final ChangeNotifier mNotifier;
+ private final Path mItemPath;
+ private int mCachedCount = INVALID_COUNT;
+
+ public LocalAlbum(Path path, GalleryApp application, int bucketId,
+ boolean isImage, String name) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mResolver = application.getContentResolver();
+ mBucketId = bucketId;
+ mName = name;
+ mIsImage = isImage;
+
+ if (isImage) {
+ mWhereClause = ImageColumns.BUCKET_ID + " = ?";
+ mOrderClause = ImageColumns.DATE_TAKEN + " DESC, "
+ + ImageColumns._ID + " DESC";
+ mBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ mProjection = LocalImage.PROJECTION;
+ mItemPath = LocalImage.ITEM_PATH;
+ } else {
+ mWhereClause = VideoColumns.BUCKET_ID + " = ?";
+ mOrderClause = VideoColumns.DATE_TAKEN + " DESC, "
+ + VideoColumns._ID + " DESC";
+ mBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
+ mProjection = LocalVideo.PROJECTION;
+ mItemPath = LocalVideo.ITEM_PATH;
+ }
+
+ mNotifier = new ChangeNotifier(this, mBaseUri, application);
+ }
+
+ public LocalAlbum(Path path, GalleryApp application, int bucketId,
+ boolean isImage) {
+ this(path, application, bucketId, isImage,
+ BucketHelper.getBucketName(
+ application.getContentResolver(), bucketId));
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBucketId == MediaSetUtils.CAMERA_BUCKET_ID;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ if (mIsImage) {
+ return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+ String.valueOf(mBucketId)).build();
+ } else {
+ return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+ String.valueOf(mBucketId)).build();
+ }
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ DataManager dataManager = mApplication.getDataManager();
+ Uri uri = mBaseUri.buildUpon()
+ .appendQueryParameter("limit", start + "," + count).build();
+ ArrayList<MediaItem> list = new ArrayList<MediaItem>();
+ GalleryUtils.assertNotInRenderThread();
+ Cursor cursor = mResolver.query(
+ uri, mProjection, mWhereClause,
+ new String[]{String.valueOf(mBucketId)},
+ mOrderClause);
+ if (cursor == null) {
+ Log.w(TAG, "query fail: " + uri);
+ return list;
+ }
+
+ try {
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(0); // _id must be in the first column
+ Path childPath = mItemPath.getChild(id);
+ MediaItem item = loadOrUpdateItem(childPath, cursor,
+ dataManager, mApplication, mIsImage);
+ list.add(item);
+ }
+ } finally {
+ cursor.close();
+ }
+ return list;
+ }
+
+ private static MediaItem loadOrUpdateItem(Path path, Cursor cursor,
+ DataManager dataManager, GalleryApp app, boolean isImage) {
+ synchronized (DataManager.LOCK) {
+ LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path);
+ if (item == null) {
+ if (isImage) {
+ item = new LocalImage(path, app, cursor);
+ } else {
+ item = new LocalVideo(path, app, cursor);
+ }
+ } else {
+ item.updateContent(cursor);
+ }
+ return item;
+ }
+ }
+
+ // The pids array are sorted by the (path) id.
+ public static MediaItem[] getMediaItemById(
+ GalleryApp application, boolean isImage, ArrayList<Integer> ids) {
+ // get the lower and upper bound of (path) id
+ MediaItem[] result = new MediaItem[ids.size()];
+ if (ids.isEmpty()) return result;
+ int idLow = ids.get(0);
+ int idHigh = ids.get(ids.size() - 1);
+
+ // prepare the query parameters
+ Uri baseUri;
+ String[] projection;
+ Path itemPath;
+ if (isImage) {
+ baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ projection = LocalImage.PROJECTION;
+ itemPath = LocalImage.ITEM_PATH;
+ } else {
+ baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+ projection = LocalVideo.PROJECTION;
+ itemPath = LocalVideo.ITEM_PATH;
+ }
+
+ ContentResolver resolver = application.getContentResolver();
+ DataManager dataManager = application.getDataManager();
+ Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?",
+ new String[]{String.valueOf(idLow), String.valueOf(idHigh)},
+ "_id");
+ if (cursor == null) {
+ Log.w(TAG, "query fail" + baseUri);
+ return result;
+ }
+ try {
+ int n = ids.size();
+ int i = 0;
+
+ while (i < n && cursor.moveToNext()) {
+ int id = cursor.getInt(0); // _id must be in the first column
+
+ // Match id with the one on the ids list.
+ if (ids.get(i) > id) {
+ continue;
+ }
+
+ while (ids.get(i) < id) {
+ if (++i >= n) {
+ return result;
+ }
+ }
+
+ Path childPath = itemPath.getChild(id);
+ MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager,
+ application, isImage);
+ result[i] = item;
+ ++i;
+ }
+ return result;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static Cursor getItemCursor(ContentResolver resolver, Uri uri,
+ String[] projection, int id) {
+ return resolver.query(uri, projection, "_id=?",
+ new String[]{String.valueOf(id)}, null);
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ if (mCachedCount == INVALID_COUNT) {
+ Cursor cursor = mResolver.query(
+ mBaseUri, COUNT_PROJECTION, mWhereClause,
+ new String[]{String.valueOf(mBucketId)}, null);
+ if (cursor == null) {
+ Log.w(TAG, "query fail");
+ return 0;
+ }
+ try {
+ Utils.assertTrue(cursor.moveToNext());
+ mCachedCount = cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+ return mCachedCount;
+ }
+
+ @Override
+ public String getName() {
+ return getLocalizedName(mApplication.getResources(), mBucketId, mName);
+ }
+
+ @Override
+ public long reload() {
+ if (mNotifier.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ mCachedCount = INVALID_COUNT;
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO;
+ }
+
+ @Override
+ public void delete() {
+ GalleryUtils.assertNotInRenderThread();
+ mResolver.delete(mBaseUri, mWhereClause,
+ new String[]{String.valueOf(mBucketId)});
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ public static String getLocalizedName(Resources res, int bucketId,
+ String name) {
+ if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+ return res.getString(R.string.folder_camera);
+ } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+ return res.getString(R.string.folder_download);
+ } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+ return res.getString(R.string.folder_imported);
+ } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+ return res.getString(R.string.folder_screenshot);
+ } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+ return res.getString(R.string.folder_edited_online_photos);
+ } else {
+ return name;
+ }
+ }
+
+ // Relative path is the absolute path minus external storage path
+ public static String getRelativePath(int bucketId) {
+ String relativePath = "/";
+ if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+ relativePath += BucketNames.CAMERA;
+ } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+ relativePath += BucketNames.DOWNLOAD;
+ } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+ relativePath += BucketNames.IMPORTED;
+ } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+ relativePath += BucketNames.SCREENSHOTS;
+ } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+ relativePath += BucketNames.EDITED_ONLINE_PHOTOS;
+ } else {
+ // If the first few cases didn't hit the matching path, do a
+ // thorough search in the local directories.
+ File extStorage = Environment.getExternalStorageDirectory();
+ String path = GalleryUtils.searchDirForPath(extStorage, bucketId);
+ if (path == null) {
+ Log.w(TAG, "Relative path for bucket id: " + bucketId + " is not found.");
+ relativePath = null;
+ } else {
+ relativePath = path.substring(extStorage.getAbsolutePath().length());
+ }
+ }
+ return relativePath;
+ }
+
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
new file mode 100644
index 000000000..b2b4b8c5d
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.BucketHelper.BucketEntry;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+// LocalAlbumSet lists all image or video albums in the local storage.
+// The path should be "/local/image", "local/video" or "/local/all"
+public class LocalAlbumSet extends MediaSet
+ implements FutureListener<ArrayList<MediaSet>> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "LocalAlbumSet";
+
+ public static final Path PATH_ALL = Path.fromString("/local/all");
+ public static final Path PATH_IMAGE = Path.fromString("/local/image");
+ public static final Path PATH_VIDEO = Path.fromString("/local/video");
+
+ private static final Uri[] mWatchUris =
+ {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+
+ private final GalleryApp mApplication;
+ private final int mType;
+ private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+ private final ChangeNotifier mNotifier;
+ private final String mName;
+ private final Handler mHandler;
+ private boolean mIsLoading;
+
+ private Future<ArrayList<MediaSet>> mLoadTask;
+ private ArrayList<MediaSet> mLoadBuffer;
+
+ public LocalAlbumSet(Path path, GalleryApp application) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mHandler = new Handler(application.getMainLooper());
+ mType = getTypeFromPath(path);
+ mNotifier = new ChangeNotifier(this, mWatchUris, application);
+ mName = application.getResources().getString(
+ R.string.set_label_local_albums);
+ }
+
+ private static int getTypeFromPath(Path path) {
+ String name[] = path.split();
+ if (name.length < 2) {
+ throw new IllegalArgumentException(path.toString());
+ }
+ return getTypeFromString(name[1]);
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return mAlbums.get(index);
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mAlbums.size();
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ private static int findBucket(BucketEntry entries[], int bucketId) {
+ for (int i = 0, n = entries.length; i < n; ++i) {
+ if (entries[i].bucketId == bucketId) return i;
+ }
+ return -1;
+ }
+
+ private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public ArrayList<MediaSet> run(JobContext jc) {
+ // Note: it will be faster if we only select media_type and bucket_id.
+ // need to test the performance if that is worth
+ BucketEntry[] entries = BucketHelper.loadBucketEntries(
+ jc, mApplication.getContentResolver(), mType);
+
+ if (jc.isCancelled()) return null;
+
+ int offset = 0;
+ // Move camera and download bucket to the front, while keeping the
+ // order of others.
+ int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
+ if (index != -1) {
+ circularShiftRight(entries, offset++, index);
+ }
+ index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
+ if (index != -1) {
+ circularShiftRight(entries, offset++, index);
+ }
+
+ ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
+ DataManager dataManager = mApplication.getDataManager();
+ for (BucketEntry entry : entries) {
+ MediaSet album = getLocalAlbum(dataManager,
+ mType, mPath, entry.bucketId, entry.bucketName);
+ albums.add(album);
+ }
+ return albums;
+ }
+ }
+
+ private MediaSet getLocalAlbum(
+ DataManager manager, int type, Path parent, int id, String name) {
+ synchronized (DataManager.LOCK) {
+ Path path = parent.getChild(id);
+ MediaObject object = manager.peekMediaObject(path);
+ if (object != null) return (MediaSet) object;
+ switch (type) {
+ case MEDIA_TYPE_IMAGE:
+ return new LocalAlbum(path, mApplication, id, true, name);
+ case MEDIA_TYPE_VIDEO:
+ return new LocalAlbum(path, mApplication, id, false, name);
+ case MEDIA_TYPE_ALL:
+ Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
+ return new LocalMergeAlbum(path, comp, new MediaSet[] {
+ getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
+ getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id);
+ }
+ throw new IllegalArgumentException(String.valueOf(type));
+ }
+ }
+
+ @Override
+ public synchronized boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Override
+ // synchronized on this function for
+ // 1. Prevent calling reload() concurrently.
+ // 2. Prevent calling onFutureDone() and reload() concurrently
+ public synchronized long reload() {
+ if (mNotifier.isDirty()) {
+ if (mLoadTask != null) mLoadTask.cancel();
+ mIsLoading = true;
+ mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
+ }
+ if (mLoadBuffer != null) {
+ mAlbums = mLoadBuffer;
+ mLoadBuffer = null;
+ for (MediaSet album : mAlbums) {
+ album.reload();
+ }
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
+ if (mLoadTask != future) return; // ignore, wait for the latest task
+ mLoadBuffer = future.get();
+ mIsLoading = false;
+ if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyContentChanged();
+ }
+ });
+ }
+
+ // For debug only. Fake there is a ContentObserver.onChange() event.
+ void fakeChange() {
+ mNotifier.fakeChange();
+ }
+
+ // Circular shift the array range from a[i] to a[j] (inclusive). That is,
+ // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
+ private static <T> void circularShiftRight(T[] array, int i, int j) {
+ T temp = array[j];
+ for (int k = j; k > i; k--) {
+ array[k] = array[k - 1];
+ }
+ array[i] = temp;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
new file mode 100644
index 000000000..cc70dd457
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.app.StitchingProgressManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// LocalImage represents an image in the local storage.
+public class LocalImage extends LocalMediaItem {
+ private static final String TAG = "LocalImage";
+
+ static final Path ITEM_PATH = Path.fromString("/local/image/item");
+
+ // Must preserve order between these indices and the order of the terms in
+ // the following PROJECTION array.
+ private static final int INDEX_ID = 0;
+ private static final int INDEX_CAPTION = 1;
+ private static final int INDEX_MIME_TYPE = 2;
+ private static final int INDEX_LATITUDE = 3;
+ private static final int INDEX_LONGITUDE = 4;
+ private static final int INDEX_DATE_TAKEN = 5;
+ private static final int INDEX_DATE_ADDED = 6;
+ private static final int INDEX_DATE_MODIFIED = 7;
+ private static final int INDEX_DATA = 8;
+ private static final int INDEX_ORIENTATION = 9;
+ private static final int INDEX_BUCKET_ID = 10;
+ private static final int INDEX_SIZE = 11;
+ private static final int INDEX_WIDTH = 12;
+ private static final int INDEX_HEIGHT = 13;
+
+ static final String[] PROJECTION = {
+ ImageColumns._ID, // 0
+ ImageColumns.TITLE, // 1
+ ImageColumns.MIME_TYPE, // 2
+ ImageColumns.LATITUDE, // 3
+ ImageColumns.LONGITUDE, // 4
+ ImageColumns.DATE_TAKEN, // 5
+ ImageColumns.DATE_ADDED, // 6
+ ImageColumns.DATE_MODIFIED, // 7
+ ImageColumns.DATA, // 8
+ ImageColumns.ORIENTATION, // 9
+ ImageColumns.BUCKET_ID, // 10
+ ImageColumns.SIZE, // 11
+ "0", // 12
+ "0" // 13
+ };
+
+ static {
+ updateWidthAndHeightProjection();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private static void updateWidthAndHeightProjection() {
+ if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+ PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
+ PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT;
+ }
+ }
+
+ private final GalleryApp mApplication;
+
+ public int rotation;
+
+ private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+ public LocalImage(Path path, GalleryApp application, Cursor cursor) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ loadFromCursor(cursor);
+ }
+
+ public LocalImage(Path path, GalleryApp application, int id) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ ContentResolver resolver = mApplication.getContentResolver();
+ Uri uri = Images.Media.EXTERNAL_CONTENT_URI;
+ Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
+ if (cursor == null) {
+ throw new RuntimeException("cannot get cursor for: " + path);
+ }
+ try {
+ if (cursor.moveToNext()) {
+ loadFromCursor(cursor);
+ } else {
+ throw new RuntimeException("cannot find data for: " + path);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void loadFromCursor(Cursor cursor) {
+ id = cursor.getInt(INDEX_ID);
+ caption = cursor.getString(INDEX_CAPTION);
+ mimeType = cursor.getString(INDEX_MIME_TYPE);
+ latitude = cursor.getDouble(INDEX_LATITUDE);
+ longitude = cursor.getDouble(INDEX_LONGITUDE);
+ dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+ dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+ dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+ filePath = cursor.getString(INDEX_DATA);
+ rotation = cursor.getInt(INDEX_ORIENTATION);
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE);
+ width = cursor.getInt(INDEX_WIDTH);
+ height = cursor.getInt(INDEX_HEIGHT);
+ }
+
+ @Override
+ protected boolean updateFromCursor(Cursor cursor) {
+ UpdateHelper uh = new UpdateHelper();
+ id = uh.update(id, cursor.getInt(INDEX_ID));
+ caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
+ mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
+ latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
+ longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
+ dateTakenInMs = uh.update(
+ dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
+ dateAddedInSec = uh.update(
+ dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
+ dateModifiedInSec = uh.update(
+ dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
+ filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
+ rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
+ bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
+ fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
+ width = uh.update(width, cursor.getInt(INDEX_WIDTH));
+ height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalImageRequest(mApplication, mPath, dateModifiedInSec,
+ type, filePath);
+ }
+
+ public static class LocalImageRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalImageRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath) {
+ super(application, path, timeModified, type,
+ MediaItem.getTargetSize(type));
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public Bitmap onDecodeOriginal(JobContext jc, final int type) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ int targetSize = MediaItem.getTargetSize(type);
+
+ // try to decode from JPEG EXIF
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ ExifInterface exif = new ExifInterface();
+ byte[] thumbData = null;
+ try {
+ exif.readExif(mLocalFilePath);
+ thumbData = exif.getThumbnail();
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath);
+ } catch (IOException e) {
+ Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath);
+ }
+ if (thumbData != null) {
+ Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
+ jc, thumbData, options, targetSize);
+ if (bitmap != null) return bitmap;
+ }
+ }
+
+ return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
+ }
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new LocalLargeImageRequest(filePath);
+ }
+
+ public static class LocalLargeImageRequest
+ implements Job<BitmapRegionDecoder> {
+ String mLocalFilePath;
+
+ public LocalLargeImageRequest(String localFilePath) {
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null && progressManager.getProgress(getContentUri()) != null) {
+ return 0; // doesn't support anything while stitching!
+ }
+ int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP
+ | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO;
+ if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) {
+ operation |= SUPPORT_FULL_IMAGE;
+ }
+
+ if (BitmapUtils.isRotationSupported(mimeType)) {
+ operation |= SUPPORT_ROTATE;
+ }
+
+ if (GalleryUtils.isValidLocation(latitude, longitude)) {
+ operation |= SUPPORT_SHOW_ON_MAP;
+ }
+ return operation;
+ }
+
+ @Override
+ public void getPanoramaSupport(PanoramaSupportCallback callback) {
+ mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+ }
+
+ @Override
+ public void clearCachedPanoramaSupport() {
+ mPanoramaMetadata.clearCachedValues();
+ }
+
+ @Override
+ public void delete() {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ ContentResolver contentResolver = mApplication.getContentResolver();
+ SaveImage.deleteAuxFiles(contentResolver, getContentUri());
+ contentResolver.delete(baseUri, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @Override
+ public void rotate(int degrees) {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ ContentValues values = new ContentValues();
+ int rotation = (this.rotation + degrees) % 360;
+ if (rotation < 0) rotation += 360;
+
+ if (mimeType.equalsIgnoreCase("image/jpeg")) {
+ ExifInterface exifInterface = new ExifInterface();
+ ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+ ExifInterface.getOrientationValueForRotation(rotation));
+ if(tag != null) {
+ exifInterface.setTag(tag);
+ try {
+ exifInterface.forceRewriteExif(filePath);
+ fileSize = new File(filePath).length();
+ values.put(Images.Media.SIZE, fileSize);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot find file to set exif: " + filePath);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot set exif data: " + filePath);
+ }
+ } else {
+ Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION);
+ }
+ }
+
+ values.put(Images.Media.ORIENTATION, rotation);
+ mApplication.getContentResolver().update(baseUri, values, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @Override
+ public Uri getContentUri() {
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_IMAGE;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation));
+ if (MIME_TYPE_JPEG.equals(mimeType)) {
+ // ExifInterface returns incorrect values for photos in other format.
+ // For example, the width and height of an webp images is always '0'.
+ MediaDetails.extractExifInfo(details, filePath);
+ }
+ return details;
+ }
+
+ @Override
+ public int getRotation() {
+ return rotation;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java
new file mode 100644
index 000000000..7e003cd3a
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.database.Cursor;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+//
+// LocalMediaItem is an abstract class captures those common fields
+// in LocalImage and LocalVideo.
+//
+public abstract class LocalMediaItem extends MediaItem {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "LocalMediaItem";
+
+ // database fields
+ public int id;
+ public String caption;
+ public String mimeType;
+ public long fileSize;
+ public double latitude = INVALID_LATLNG;
+ public double longitude = INVALID_LATLNG;
+ public long dateTakenInMs;
+ public long dateAddedInSec;
+ public long dateModifiedInSec;
+ public String filePath;
+ public int bucketId;
+ public int width;
+ public int height;
+
+ public LocalMediaItem(Path path, long version) {
+ super(path, version);
+ }
+
+ @Override
+ public long getDateInMs() {
+ return dateTakenInMs;
+ }
+
+ @Override
+ public String getName() {
+ return caption;
+ }
+
+ @Override
+ public void getLatLong(double[] latLong) {
+ latLong[0] = latitude;
+ latLong[1] = longitude;
+ }
+
+ abstract protected boolean updateFromCursor(Cursor cursor);
+
+ public int getBucketId() {
+ return bucketId;
+ }
+
+ protected void updateContent(Cursor cursor) {
+ if (updateFromCursor(cursor)) {
+ mDataVersion = nextVersionNumber();
+ }
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ details.addDetail(MediaDetails.INDEX_PATH, filePath);
+ details.addDetail(MediaDetails.INDEX_TITLE, caption);
+ DateFormat formater = DateFormat.getDateTimeInstance();
+ details.addDetail(MediaDetails.INDEX_DATETIME,
+ formater.format(new Date(dateModifiedInSec * 1000)));
+ details.addDetail(MediaDetails.INDEX_WIDTH, width);
+ details.addDetail(MediaDetails.INDEX_HEIGHT, height);
+
+ if (GalleryUtils.isValidLocation(latitude, longitude)) {
+ details.addDetail(MediaDetails.INDEX_LOCATION, new double[] {latitude, longitude});
+ }
+ if (fileSize > 0) details.addDetail(MediaDetails.INDEX_SIZE, fileSize);
+ return details;
+ }
+
+ @Override
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public long getSize() {
+ return fileSize;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java
new file mode 100644
index 000000000..f0b5e5726
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to
+// determine the order of items. The items are assumed to be sorted in the input
+// media sets (with the same order that the Comparator uses).
+//
+// This only handles MediaItems, not SubMediaSets.
+public class LocalMergeAlbum extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "LocalMergeAlbum";
+ private static final int PAGE_SIZE = 64;
+
+ private final Comparator<MediaItem> mComparator;
+ private final MediaSet[] mSources;
+
+ private FetchCache[] mFetcher;
+ private int mSupportedOperation;
+ private int mBucketId;
+
+ // mIndex maps global position to the position of each underlying media sets.
+ private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>();
+
+ public LocalMergeAlbum(
+ Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) {
+ super(path, INVALID_DATA_VERSION);
+ mComparator = comparator;
+ mSources = sources;
+ mBucketId = bucketId;
+ for (MediaSet set : mSources) {
+ set.addContentListener(this);
+ }
+ reload();
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ if (mSources.length == 0) return false;
+ for(MediaSet set : mSources) {
+ if (!set.isCameraRoll()) return false;
+ }
+ return true;
+ }
+
+ private void updateData() {
+ ArrayList<MediaSet> matches = new ArrayList<MediaSet>();
+ int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL;
+ mFetcher = new FetchCache[mSources.length];
+ for (int i = 0, n = mSources.length; i < n; ++i) {
+ mFetcher[i] = new FetchCache(mSources[i]);
+ supported &= mSources[i].getSupportedOperations();
+ }
+ mSupportedOperation = supported;
+ mIndex.clear();
+ mIndex.put(0, new int[mSources.length]);
+ }
+
+ private void invalidateCache() {
+ for (int i = 0, n = mSources.length; i < n; i++) {
+ mFetcher[i].invalidate();
+ }
+ mIndex.clear();
+ mIndex.put(0, new int[mSources.length]);
+ }
+
+ @Override
+ public Uri getContentUri() {
+ String bucketId = String.valueOf(mBucketId);
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ return MediaStore.Files.getContentUri("external").buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+ .build();
+ } else {
+ // We don't have a single URL for a merged image before ICS
+ // So we used the image's URL as a substitute.
+ return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+ .build();
+ }
+ }
+
+ @Override
+ public String getName() {
+ return mSources.length == 0 ? "" : mSources[0].getName();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return getTotalMediaItemCount();
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+
+ // First find the nearest mark position <= start.
+ SortedMap<Integer, int[]> head = mIndex.headMap(start + 1);
+ int markPos = head.lastKey();
+ int[] subPos = head.get(markPos).clone();
+ MediaItem[] slot = new MediaItem[mSources.length];
+
+ int size = mSources.length;
+
+ // fill all slots
+ for (int i = 0; i < size; i++) {
+ slot[i] = mFetcher[i].getItem(subPos[i]);
+ }
+
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+ for (int i = markPos; i < start + count; i++) {
+ int k = -1; // k points to the best slot up to now.
+ for (int j = 0; j < size; j++) {
+ if (slot[j] != null) {
+ if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) {
+ k = j;
+ }
+ }
+ }
+
+ // If we don't have anything, all streams are exhausted.
+ if (k == -1) break;
+
+ // Pick the best slot and refill it.
+ subPos[k]++;
+ if (i >= start) {
+ result.add(slot[k]);
+ }
+ slot[k] = mFetcher[k].getItem(subPos[k]);
+
+ // Periodically leave a mark in the index, so we can come back later.
+ if ((i + 1) % PAGE_SIZE == 0) {
+ mIndex.put(i + 1, subPos.clone());
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public int getTotalMediaItemCount() {
+ int count = 0;
+ for (MediaSet set : mSources) {
+ count += set.getTotalMediaItemCount();
+ }
+ return count;
+ }
+
+ @Override
+ public long reload() {
+ boolean changed = false;
+ for (int i = 0, n = mSources.length; i < n; ++i) {
+ if (mSources[i].reload() > mDataVersion) changed = true;
+ }
+ if (changed) {
+ mDataVersion = nextVersionNumber();
+ updateData();
+ invalidateCache();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return mSupportedOperation;
+ }
+
+ @Override
+ public void delete() {
+ for (MediaSet set : mSources) {
+ set.delete();
+ }
+ }
+
+ @Override
+ public void rotate(int degrees) {
+ for (MediaSet set : mSources) {
+ set.rotate(degrees);
+ }
+ }
+
+ private static class FetchCache {
+ private MediaSet mBaseSet;
+ private SoftReference<ArrayList<MediaItem>> mCacheRef;
+ private int mStartPos;
+
+ public FetchCache(MediaSet baseSet) {
+ mBaseSet = baseSet;
+ }
+
+ public void invalidate() {
+ mCacheRef = null;
+ }
+
+ public MediaItem getItem(int index) {
+ boolean needLoading = false;
+ ArrayList<MediaItem> cache = null;
+ if (mCacheRef == null
+ || index < mStartPos || index >= mStartPos + PAGE_SIZE) {
+ needLoading = true;
+ } else {
+ cache = mCacheRef.get();
+ if (cache == null) {
+ needLoading = true;
+ }
+ }
+
+ if (needLoading) {
+ cache = mBaseSet.getMediaItem(index, PAGE_SIZE);
+ mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache);
+ mStartPos = index;
+ }
+
+ if (index < mStartPos || index >= mStartPos + cache.size()) {
+ return null;
+ }
+
+ return cache.get(index - mStartPos);
+ }
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java
new file mode 100644
index 000000000..a2e3d1443
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalSource.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentProviderClient;
+import android.content.ContentUris;
+import android.content.UriMatcher;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+class LocalSource extends MediaSource {
+
+ public static final String KEY_BUCKET_ID = "bucketId";
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+ private static final int NO_MATCH = -1;
+ private final UriMatcher mUriMatcher = new UriMatcher(NO_MATCH);
+ public static final Comparator<PathId> sIdComparator = new IdComparator();
+
+ private static final int LOCAL_IMAGE_ALBUMSET = 0;
+ private static final int LOCAL_VIDEO_ALBUMSET = 1;
+ private static final int LOCAL_IMAGE_ALBUM = 2;
+ private static final int LOCAL_VIDEO_ALBUM = 3;
+ private static final int LOCAL_IMAGE_ITEM = 4;
+ private static final int LOCAL_VIDEO_ITEM = 5;
+ private static final int LOCAL_ALL_ALBUMSET = 6;
+ private static final int LOCAL_ALL_ALBUM = 7;
+
+ private static final String TAG = "LocalSource";
+
+ private ContentProviderClient mClient;
+
+ public LocalSource(GalleryApp context) {
+ super("local");
+ mApplication = context;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET);
+ mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET);
+ mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET);
+
+ mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM);
+ mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM);
+ mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM);
+ mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM);
+ mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM);
+
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/images/media/#", LOCAL_IMAGE_ITEM);
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/video/media/#", LOCAL_VIDEO_ITEM);
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/images/media", LOCAL_IMAGE_ALBUM);
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/video/media", LOCAL_VIDEO_ALBUM);
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/file", LOCAL_ALL_ALBUM);
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ GalleryApp app = mApplication;
+ switch (mMatcher.match(path)) {
+ case LOCAL_ALL_ALBUMSET:
+ case LOCAL_IMAGE_ALBUMSET:
+ case LOCAL_VIDEO_ALBUMSET:
+ return new LocalAlbumSet(path, mApplication);
+ case LOCAL_IMAGE_ALBUM:
+ return new LocalAlbum(path, app, mMatcher.getIntVar(0), true);
+ case LOCAL_VIDEO_ALBUM:
+ return new LocalAlbum(path, app, mMatcher.getIntVar(0), false);
+ case LOCAL_ALL_ALBUM: {
+ int bucketId = mMatcher.getIntVar(0);
+ DataManager dataManager = app.getDataManager();
+ MediaSet imageSet = (MediaSet) dataManager.getMediaObject(
+ LocalAlbumSet.PATH_IMAGE.getChild(bucketId));
+ MediaSet videoSet = (MediaSet) dataManager.getMediaObject(
+ LocalAlbumSet.PATH_VIDEO.getChild(bucketId));
+ Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
+ return new LocalMergeAlbum(
+ path, comp, new MediaSet[] {imageSet, videoSet}, bucketId);
+ }
+ case LOCAL_IMAGE_ITEM:
+ return new LocalImage(path, mApplication, mMatcher.getIntVar(0));
+ case LOCAL_VIDEO_ITEM:
+ return new LocalVideo(path, mApplication, mMatcher.getIntVar(0));
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+
+ private static int getMediaType(String type, int defaultType) {
+ if (type == null) return defaultType;
+ try {
+ int value = Integer.parseInt(type);
+ if ((value & (MEDIA_TYPE_IMAGE
+ | MEDIA_TYPE_VIDEO)) != 0) return value;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "invalid type: " + type, e);
+ }
+ return defaultType;
+ }
+
+ // The media type bit passed by the intent
+ private static final int MEDIA_TYPE_ALL = 0;
+ private static final int MEDIA_TYPE_IMAGE = 1;
+ private static final int MEDIA_TYPE_VIDEO = 4;
+
+ private Path getAlbumPath(Uri uri, int defaultType) {
+ int mediaType = getMediaType(
+ uri.getQueryParameter(Gallery.KEY_MEDIA_TYPES),
+ defaultType);
+ String bucketId = uri.getQueryParameter(KEY_BUCKET_ID);
+ int id = 0;
+ try {
+ id = Integer.parseInt(bucketId);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "invalid bucket id: " + bucketId, e);
+ return null;
+ }
+ switch (mediaType) {
+ case MEDIA_TYPE_IMAGE:
+ return Path.fromString("/local/image").getChild(id);
+ case MEDIA_TYPE_VIDEO:
+ return Path.fromString("/local/video").getChild(id);
+ default:
+ return Path.fromString("/local/all").getChild(id);
+ }
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri, String type) {
+ try {
+ switch (mUriMatcher.match(uri)) {
+ case LOCAL_IMAGE_ITEM: {
+ long id = ContentUris.parseId(uri);
+ return id >= 0 ? LocalImage.ITEM_PATH.getChild(id) : null;
+ }
+ case LOCAL_VIDEO_ITEM: {
+ long id = ContentUris.parseId(uri);
+ return id >= 0 ? LocalVideo.ITEM_PATH.getChild(id) : null;
+ }
+ case LOCAL_IMAGE_ALBUM: {
+ return getAlbumPath(uri, MEDIA_TYPE_IMAGE);
+ }
+ case LOCAL_VIDEO_ALBUM: {
+ return getAlbumPath(uri, MEDIA_TYPE_VIDEO);
+ }
+ case LOCAL_ALL_ALBUM: {
+ return getAlbumPath(uri, MEDIA_TYPE_ALL);
+ }
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "uri: " + uri.toString(), e);
+ }
+ return null;
+ }
+
+ @Override
+ public Path getDefaultSetOf(Path item) {
+ MediaObject object = mApplication.getDataManager().getMediaObject(item);
+ if (object instanceof LocalMediaItem) {
+ return Path.fromString("/local/all").getChild(
+ String.valueOf(((LocalMediaItem) object).getBucketId()));
+ }
+ return null;
+ }
+
+ @Override
+ public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+ ArrayList<PathId> imageList = new ArrayList<PathId>();
+ ArrayList<PathId> videoList = new ArrayList<PathId>();
+ int n = list.size();
+ for (int i = 0; i < n; i++) {
+ PathId pid = list.get(i);
+ // We assume the form is: "/local/{image,video}/item/#"
+ // We don't use mMatcher for efficiency's reason.
+ Path parent = pid.path.getParent();
+ if (parent == LocalImage.ITEM_PATH) {
+ imageList.add(pid);
+ } else if (parent == LocalVideo.ITEM_PATH) {
+ videoList.add(pid);
+ }
+ }
+ // TODO: use "files" table so we can merge the two cases.
+ processMapMediaItems(imageList, consumer, true);
+ processMapMediaItems(videoList, consumer, false);
+ }
+
+ private void processMapMediaItems(ArrayList<PathId> list,
+ ItemConsumer consumer, boolean isImage) {
+ // Sort path by path id
+ Collections.sort(list, sIdComparator);
+ int n = list.size();
+ for (int i = 0; i < n; ) {
+ PathId pid = list.get(i);
+
+ // Find a range of items.
+ ArrayList<Integer> ids = new ArrayList<Integer>();
+ int startId = Integer.parseInt(pid.path.getSuffix());
+ ids.add(startId);
+
+ int j;
+ for (j = i + 1; j < n; j++) {
+ PathId pid2 = list.get(j);
+ int curId = Integer.parseInt(pid2.path.getSuffix());
+ if (curId - startId >= MediaSet.MEDIAITEM_BATCH_FETCH_COUNT) {
+ break;
+ }
+ ids.add(curId);
+ }
+
+ MediaItem[] items = LocalAlbum.getMediaItemById(
+ mApplication, isImage, ids);
+ for(int k = i ; k < j; k++) {
+ PathId pid2 = list.get(k);
+ consumer.consume(pid2.id, items[k - i]);
+ }
+
+ i = j;
+ }
+ }
+
+ // This is a comparator which compares the suffix number in two Paths.
+ private static class IdComparator implements Comparator<PathId> {
+ @Override
+ public int compare(PathId p1, PathId p2) {
+ String s1 = p1.path.getSuffix();
+ String s2 = p2.path.getSuffix();
+ int len1 = s1.length();
+ int len2 = s2.length();
+ if (len1 < len2) {
+ return -1;
+ } else if (len1 > len2) {
+ return 1;
+ } else {
+ return s1.compareTo(s2);
+ }
+ }
+ }
+
+ @Override
+ public void resume() {
+ mClient = mApplication.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY);
+ }
+
+ @Override
+ public void pause() {
+ mClient.release();
+ mClient = null;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java
new file mode 100644
index 000000000..4b8774ca4
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+// LocalVideo represents a video in the local storage.
+public class LocalVideo extends LocalMediaItem {
+ private static final String TAG = "LocalVideo";
+ static final Path ITEM_PATH = Path.fromString("/local/video/item");
+
+ // Must preserve order between these indices and the order of the terms in
+ // the following PROJECTION array.
+ private static final int INDEX_ID = 0;
+ private static final int INDEX_CAPTION = 1;
+ private static final int INDEX_MIME_TYPE = 2;
+ private static final int INDEX_LATITUDE = 3;
+ private static final int INDEX_LONGITUDE = 4;
+ private static final int INDEX_DATE_TAKEN = 5;
+ private static final int INDEX_DATE_ADDED = 6;
+ private static final int INDEX_DATE_MODIFIED = 7;
+ private static final int INDEX_DATA = 8;
+ private static final int INDEX_DURATION = 9;
+ private static final int INDEX_BUCKET_ID = 10;
+ private static final int INDEX_SIZE = 11;
+ private static final int INDEX_RESOLUTION = 12;
+
+ static final String[] PROJECTION = new String[] {
+ VideoColumns._ID,
+ VideoColumns.TITLE,
+ VideoColumns.MIME_TYPE,
+ VideoColumns.LATITUDE,
+ VideoColumns.LONGITUDE,
+ VideoColumns.DATE_TAKEN,
+ VideoColumns.DATE_ADDED,
+ VideoColumns.DATE_MODIFIED,
+ VideoColumns.DATA,
+ VideoColumns.DURATION,
+ VideoColumns.BUCKET_ID,
+ VideoColumns.SIZE,
+ VideoColumns.RESOLUTION,
+ };
+
+ private final GalleryApp mApplication;
+
+ public int durationInSec;
+
+ public LocalVideo(Path path, GalleryApp application, Cursor cursor) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ loadFromCursor(cursor);
+ }
+
+ public LocalVideo(Path path, GalleryApp context, int id) {
+ super(path, nextVersionNumber());
+ mApplication = context;
+ ContentResolver resolver = mApplication.getContentResolver();
+ Uri uri = Video.Media.EXTERNAL_CONTENT_URI;
+ Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
+ if (cursor == null) {
+ throw new RuntimeException("cannot get cursor for: " + path);
+ }
+ try {
+ if (cursor.moveToNext()) {
+ loadFromCursor(cursor);
+ } else {
+ throw new RuntimeException("cannot find data for: " + path);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void loadFromCursor(Cursor cursor) {
+ id = cursor.getInt(INDEX_ID);
+ caption = cursor.getString(INDEX_CAPTION);
+ mimeType = cursor.getString(INDEX_MIME_TYPE);
+ latitude = cursor.getDouble(INDEX_LATITUDE);
+ longitude = cursor.getDouble(INDEX_LONGITUDE);
+ dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+ dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+ dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+ filePath = cursor.getString(INDEX_DATA);
+ durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE);
+ parseResolution(cursor.getString(INDEX_RESOLUTION));
+ }
+
+ private void parseResolution(String resolution) {
+ if (resolution == null) return;
+ int m = resolution.indexOf('x');
+ if (m == -1) return;
+ try {
+ int w = Integer.parseInt(resolution.substring(0, m));
+ int h = Integer.parseInt(resolution.substring(m + 1));
+ width = w;
+ height = h;
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ }
+ }
+
+ @Override
+ protected boolean updateFromCursor(Cursor cursor) {
+ UpdateHelper uh = new UpdateHelper();
+ id = uh.update(id, cursor.getInt(INDEX_ID));
+ caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
+ mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
+ latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
+ longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
+ dateTakenInMs = uh.update(
+ dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
+ dateAddedInSec = uh.update(
+ dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
+ dateModifiedInSec = uh.update(
+ dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
+ filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
+ durationInSec = uh.update(
+ durationInSec, cursor.getInt(INDEX_DURATION) / 1000);
+ bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
+ fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalVideoRequest(mApplication, getPath(), dateModifiedInSec,
+ type, filePath);
+ }
+
+ public static class LocalVideoRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalVideoRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath) {
+ super(application, path, timeModified, type,
+ MediaItem.getTargetSize(type));
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public Bitmap onDecodeOriginal(JobContext jc, int type) {
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(mLocalFilePath);
+ if (bitmap == null || jc.isCancelled()) return null;
+ return bitmap;
+ }
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ throw new UnsupportedOperationException("Cannot regquest a large image"
+ + " to a local video!");
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_PLAY | SUPPORT_INFO | SUPPORT_TRIM | SUPPORT_MUTE;
+ }
+
+ @Override
+ public void delete() {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+ mApplication.getContentResolver().delete(baseUri, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @Override
+ public void rotate(int degrees) {
+ // TODO
+ }
+
+ @Override
+ public Uri getContentUri() {
+ Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+ return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+ }
+
+ @Override
+ public Uri getPlayUri() {
+ return getContentUri();
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ int s = durationInSec;
+ if (s > 0) {
+ details.addDetail(MediaDetails.INDEX_DURATION, GalleryUtils.formatDuration(
+ mApplication.getAndroidContext(), durationInSec));
+ }
+ return details;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java
new file mode 100644
index 000000000..540322a33
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocationClustering.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.FloatMath;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+
+import java.util.ArrayList;
+
+class LocationClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "LocationClustering";
+
+ private static final int MIN_GROUPS = 1;
+ private static final int MAX_GROUPS = 20;
+ private static final int MAX_ITERATIONS = 30;
+
+ // If the total distance change is less than this ratio, stop iterating.
+ private static final float STOP_CHANGE_RATIO = 0.01f;
+ private Context mContext;
+ private ArrayList<ArrayList<SmallItem>> mClusters;
+ private ArrayList<String> mNames;
+ private String mNoLocationString;
+ private Handler mHandler;
+
+ private static class Point {
+ public Point(double lat, double lng) {
+ latRad = Math.toRadians(lat);
+ lngRad = Math.toRadians(lng);
+ }
+ public Point() {}
+ public double latRad, lngRad;
+ }
+
+ private static class SmallItem {
+ Path path;
+ double lat, lng;
+ }
+
+ public LocationClustering(Context context) {
+ mContext = context;
+ mNoLocationString = mContext.getResources().getString(R.string.no_location);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final int total = baseSet.getTotalMediaItemCount();
+ final SmallItem[] buf = new SmallItem[total];
+ // Separate items to two sets: with or without lat-long.
+ final double[] latLong = new double[2];
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if (index < 0 || index >= total) return;
+ SmallItem s = new SmallItem();
+ s.path = item.getPath();
+ item.getLatLong(latLong);
+ s.lat = latLong[0];
+ s.lng = latLong[1];
+ buf[index] = s;
+ }
+ });
+
+ final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>();
+ final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>();
+ final ArrayList<Point> points = new ArrayList<Point>();
+ for (int i = 0; i < total; i++) {
+ SmallItem s = buf[i];
+ if (s == null) continue;
+ if (GalleryUtils.isValidLocation(s.lat, s.lng)) {
+ withLatLong.add(s);
+ points.add(new Point(s.lat, s.lng));
+ } else {
+ withoutLatLong.add(s);
+ }
+ }
+
+ ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>();
+
+ int m = withLatLong.size();
+ if (m > 0) {
+ // cluster the items with lat-long
+ Point[] pointsArray = new Point[m];
+ pointsArray = points.toArray(pointsArray);
+ int[] bestK = new int[1];
+ int[] index = kMeans(pointsArray, bestK);
+
+ for (int i = 0; i < bestK[0]; i++) {
+ clusters.add(new ArrayList<SmallItem>());
+ }
+
+ for (int i = 0; i < m; i++) {
+ clusters.get(index[i]).add(withLatLong.get(i));
+ }
+ }
+
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext);
+ mNames = new ArrayList<String>();
+ boolean hasUnresolvedAddress = false;
+ mClusters = new ArrayList<ArrayList<SmallItem>>();
+ for (ArrayList<SmallItem> cluster : clusters) {
+ String name = generateName(cluster, geocoder);
+ if (name != null) {
+ mNames.add(name);
+ mClusters.add(cluster);
+ } else {
+ // move cluster-i to no location cluster
+ withoutLatLong.addAll(cluster);
+ hasUnresolvedAddress = true;
+ }
+ }
+
+ if (withoutLatLong.size() > 0) {
+ mNames.add(mNoLocationString);
+ mClusters.add(withoutLatLong);
+ }
+
+ if (hasUnresolvedAddress) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext, R.string.no_connectivity,
+ Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+
+ private static String generateName(ArrayList<SmallItem> items,
+ ReverseGeocoder geocoder) {
+ ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong();
+
+ int n = items.size();
+ for (int i = 0; i < n; i++) {
+ SmallItem item = items.get(i);
+ double itemLatitude = item.lat;
+ double itemLongitude = item.lng;
+
+ if (set.mMinLatLatitude > itemLatitude) {
+ set.mMinLatLatitude = itemLatitude;
+ set.mMinLatLongitude = itemLongitude;
+ }
+ if (set.mMaxLatLatitude < itemLatitude) {
+ set.mMaxLatLatitude = itemLatitude;
+ set.mMaxLatLongitude = itemLongitude;
+ }
+ if (set.mMinLonLongitude > itemLongitude) {
+ set.mMinLonLatitude = itemLatitude;
+ set.mMinLonLongitude = itemLongitude;
+ }
+ if (set.mMaxLonLongitude < itemLongitude) {
+ set.mMaxLonLatitude = itemLatitude;
+ set.mMaxLonLongitude = itemLongitude;
+ }
+ }
+
+ return geocoder.computeAddress(set);
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.size();
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ ArrayList<SmallItem> items = mClusters.get(index);
+ ArrayList<Path> result = new ArrayList<Path>(items.size());
+ for (int i = 0, n = items.size(); i < n; i++) {
+ result.add(items.get(i).path);
+ }
+ return result;
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames.get(index);
+ }
+
+ // Input: n points
+ // Output: the best k is stored in bestK[0], and the return value is the
+ // an array which specifies the group that each point belongs (0 to k - 1).
+ private static int[] kMeans(Point points[], int[] bestK) {
+ int n = points.length;
+
+ // min and max number of groups wanted
+ int minK = Math.min(n, MIN_GROUPS);
+ int maxK = Math.min(n, MAX_GROUPS);
+
+ Point[] center = new Point[maxK]; // center of each group.
+ Point[] groupSum = new Point[maxK]; // sum of points in each group.
+ int[] groupCount = new int[maxK]; // number of points in each group.
+ int[] grouping = new int[n]; // The group assignment for each point.
+
+ for (int i = 0; i < maxK; i++) {
+ center[i] = new Point();
+ groupSum[i] = new Point();
+ }
+
+ // The score we want to minimize is:
+ // (sum of distance from each point to its group center) * sqrt(k).
+ float bestScore = Float.MAX_VALUE;
+ // The best group assignment up to now.
+ int[] bestGrouping = new int[n];
+ // The best K up to now.
+ bestK[0] = 1;
+
+ float lastDistance = 0;
+ float totalDistance = 0;
+
+ for (int k = minK; k <= maxK; k++) {
+ // step 1: (arbitrarily) pick k points as the initial centers.
+ int delta = n / k;
+ for (int i = 0; i < k; i++) {
+ Point p = points[i * delta];
+ center[i].latRad = p.latRad;
+ center[i].lngRad = p.lngRad;
+ }
+
+ for (int iter = 0; iter < MAX_ITERATIONS; iter++) {
+ // step 2: assign each point to the nearest center.
+ for (int i = 0; i < k; i++) {
+ groupSum[i].latRad = 0;
+ groupSum[i].lngRad = 0;
+ groupCount[i] = 0;
+ }
+ totalDistance = 0;
+
+ for (int i = 0; i < n; i++) {
+ Point p = points[i];
+ float bestDistance = Float.MAX_VALUE;
+ int bestIndex = 0;
+ for (int j = 0; j < k; j++) {
+ float distance = (float) GalleryUtils.fastDistanceMeters(
+ p.latRad, p.lngRad, center[j].latRad, center[j].lngRad);
+ // We may have small non-zero distance introduced by
+ // floating point calculation, so zero out small
+ // distances less than 1 meter.
+ if (distance < 1) {
+ distance = 0;
+ }
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestIndex = j;
+ }
+ }
+ grouping[i] = bestIndex;
+ groupCount[bestIndex]++;
+ groupSum[bestIndex].latRad += p.latRad;
+ groupSum[bestIndex].lngRad += p.lngRad;
+ totalDistance += bestDistance;
+ }
+
+ // step 3: calculate new centers
+ for (int i = 0; i < k; i++) {
+ if (groupCount[i] > 0) {
+ center[i].latRad = groupSum[i].latRad / groupCount[i];
+ center[i].lngRad = groupSum[i].lngRad / groupCount[i];
+ }
+ }
+
+ if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance)
+ / totalDistance) < STOP_CHANGE_RATIO) {
+ break;
+ }
+ lastDistance = totalDistance;
+ }
+
+ // step 4: remove empty groups and reassign group number
+ int reassign[] = new int[k];
+ int realK = 0;
+ for (int i = 0; i < k; i++) {
+ if (groupCount[i] > 0) {
+ reassign[i] = realK++;
+ }
+ }
+
+ // step 5: calculate the final score
+ float score = totalDistance * FloatMath.sqrt(realK);
+
+ if (score < bestScore) {
+ bestScore = score;
+ bestK[0] = realK;
+ for (int i = 0; i < n; i++) {
+ bestGrouping[i] = reassign[grouping[i]];
+ }
+ if (score == 0) {
+ break;
+ }
+ }
+ }
+ return bestGrouping;
+ }
+}
diff --git a/src/com/android/gallery3d/data/Log.java b/src/com/android/gallery3d/data/Log.java
new file mode 100644
index 000000000..3384eb66c
--- /dev/null
+++ b/src/com/android/gallery3d/data/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+public class Log {
+ public static int v(String tag, String msg) {
+ return android.util.Log.v(tag, msg);
+ }
+ public static int v(String tag, String msg, Throwable tr) {
+ return android.util.Log.v(tag, msg, tr);
+ }
+ public static int d(String tag, String msg) {
+ return android.util.Log.d(tag, msg);
+ }
+ public static int d(String tag, String msg, Throwable tr) {
+ return android.util.Log.d(tag, msg, tr);
+ }
+ public static int i(String tag, String msg) {
+ return android.util.Log.i(tag, msg);
+ }
+ public static int i(String tag, String msg, Throwable tr) {
+ return android.util.Log.i(tag, msg, tr);
+ }
+ public static int w(String tag, String msg) {
+ return android.util.Log.w(tag, msg);
+ }
+ public static int w(String tag, String msg, Throwable tr) {
+ return android.util.Log.w(tag, msg, tr);
+ }
+ public static int w(String tag, Throwable tr) {
+ return android.util.Log.w(tag, tr);
+ }
+ public static int e(String tag, String msg) {
+ return android.util.Log.e(tag, msg);
+ }
+ public static int e(String tag, String msg, Throwable tr) {
+ return android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java
new file mode 100644
index 000000000..cac524b88
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+public class MediaDetails implements Iterable<Entry<Integer, Object>> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaDetails";
+
+ private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>();
+ private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>();
+
+ public static final int INDEX_TITLE = 1;
+ public static final int INDEX_DESCRIPTION = 2;
+ public static final int INDEX_DATETIME = 3;
+ public static final int INDEX_LOCATION = 4;
+ public static final int INDEX_WIDTH = 5;
+ public static final int INDEX_HEIGHT = 6;
+ public static final int INDEX_ORIENTATION = 7;
+ public static final int INDEX_DURATION = 8;
+ public static final int INDEX_MIMETYPE = 9;
+ public static final int INDEX_SIZE = 10;
+
+ // for EXIF
+ public static final int INDEX_MAKE = 100;
+ public static final int INDEX_MODEL = 101;
+ public static final int INDEX_FLASH = 102;
+ public static final int INDEX_FOCAL_LENGTH = 103;
+ public static final int INDEX_WHITE_BALANCE = 104;
+ public static final int INDEX_APERTURE = 105;
+ public static final int INDEX_SHUTTER_SPEED = 106;
+ public static final int INDEX_EXPOSURE_TIME = 107;
+ public static final int INDEX_ISO = 108;
+
+ // Put this last because it may be long.
+ public static final int INDEX_PATH = 200;
+
+ public static class FlashState {
+ private static int FLASH_FIRED_MASK = 1;
+ private static int FLASH_RETURN_MASK = 2 | 4;
+ private static int FLASH_MODE_MASK = 8 | 16;
+ private static int FLASH_FUNCTION_MASK = 32;
+ private static int FLASH_RED_EYE_MASK = 64;
+ private int mState;
+
+ public FlashState(int state) {
+ mState = state;
+ }
+
+ public boolean isFlashFired() {
+ return (mState & FLASH_FIRED_MASK) != 0;
+ }
+ }
+
+ public void addDetail(int index, Object value) {
+ mDetails.put(index, value);
+ }
+
+ public Object getDetail(int index) {
+ return mDetails.get(index);
+ }
+
+ public int size() {
+ return mDetails.size();
+ }
+
+ @Override
+ public Iterator<Entry<Integer, Object>> iterator() {
+ return mDetails.entrySet().iterator();
+ }
+
+ public void setUnit(int index, int unit) {
+ mUnits.put(index, unit);
+ }
+
+ public boolean hasUnit(int index) {
+ return mUnits.containsKey(index);
+ }
+
+ public int getUnit(int index) {
+ return mUnits.get(index);
+ }
+
+ private static void setExifData(MediaDetails details, ExifTag tag,
+ int key) {
+ if (tag != null) {
+ String value = null;
+ int type = tag.getDataType();
+ if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) {
+ value = String.valueOf(tag.getValueAsRational(0).toDouble());
+ } else if (type == ExifTag.TYPE_ASCII) {
+ value = tag.getValueAsString();
+ } else {
+ value = String.valueOf(tag.forceGetValueAsLong(0));
+ }
+ if (key == MediaDetails.INDEX_FLASH) {
+ MediaDetails.FlashState state = new MediaDetails.FlashState(
+ Integer.valueOf(value.toString()));
+ details.addDetail(key, state);
+ } else {
+ details.addDetail(key, value);
+ }
+ }
+ }
+
+ public static void extractExifInfo(MediaDetails details, String filePath) {
+
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(filePath);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Could not find file to read exif: " + filePath, e);
+ } catch (IOException e) {
+ Log.w(TAG, "Could not read exif from file: " + filePath, e);
+ }
+
+ setExifData(details, exif.getTag(ExifInterface.TAG_FLASH),
+ MediaDetails.INDEX_FLASH);
+ setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH),
+ MediaDetails.INDEX_WIDTH);
+ setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH),
+ MediaDetails.INDEX_HEIGHT);
+ setExifData(details, exif.getTag(ExifInterface.TAG_MAKE),
+ MediaDetails.INDEX_MAKE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_MODEL),
+ MediaDetails.INDEX_MODEL);
+ setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE),
+ MediaDetails.INDEX_APERTURE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS),
+ MediaDetails.INDEX_ISO);
+ setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE),
+ MediaDetails.INDEX_WHITE_BALANCE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME),
+ MediaDetails.INDEX_EXPOSURE_TIME);
+ ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH);
+ if (focalTag != null) {
+ details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
+ focalTag.getValueAsRational(0).toDouble());
+ details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
new file mode 100644
index 000000000..59ea86551
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+
+// MediaItem represents an image or a video item.
+public abstract class MediaItem extends MediaObject {
+ // NOTE: These type numbers are stored in the image cache, so it should not
+ // not be changed without resetting the cache.
+ public static final int TYPE_THUMBNAIL = 1;
+ public static final int TYPE_MICROTHUMBNAIL = 2;
+
+ public static final int CACHED_IMAGE_QUALITY = 95;
+
+ public static final int IMAGE_READY = 0;
+ public static final int IMAGE_WAIT = 1;
+ public static final int IMAGE_ERROR = -1;
+
+ public static final String MIME_TYPE_JPEG = "image/jpeg";
+
+ private static final int BYTESBUFFE_POOL_SIZE = 4;
+ private static final int BYTESBUFFER_SIZE = 200 * 1024;
+
+ private static int sMicrothumbnailTargetSize = 200;
+ private static final BytesBufferPool sMicroThumbBufferPool =
+ new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE);
+
+ private static int sThumbnailTargetSize = 640;
+
+ // TODO: fix default value for latlng and change this.
+ public static final double INVALID_LATLNG = 0f;
+
+ public abstract Job<Bitmap> requestImage(int type);
+ public abstract Job<BitmapRegionDecoder> requestLargeImage();
+
+ public MediaItem(Path path, long version) {
+ super(path, version);
+ }
+
+ public long getDateInMs() {
+ return 0;
+ }
+
+ public String getName() {
+ return null;
+ }
+
+ public void getLatLong(double[] latLong) {
+ latLong[0] = INVALID_LATLNG;
+ latLong[1] = INVALID_LATLNG;
+ }
+
+ public String[] getTags() {
+ return null;
+ }
+
+ public Face[] getFaces() {
+ return null;
+ }
+
+ // The rotation of the full-resolution image. By default, it returns the value of
+ // getRotation().
+ public int getFullImageRotation() {
+ return getRotation();
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+
+ public long getSize() {
+ return 0;
+ }
+
+ public abstract String getMimeType();
+
+ public String getFilePath() {
+ return "";
+ }
+
+ // Returns width and height of the media item.
+ // Returns 0, 0 if the information is not available.
+ public abstract int getWidth();
+ public abstract int getHeight();
+
+ // This is an alternative for requestImage() in PhotoPage. If this
+ // is implemented, you don't need to implement requestImage().
+ public ScreenNail getScreenNail() {
+ return null;
+ }
+
+ public static int getTargetSize(int type) {
+ switch (type) {
+ case TYPE_THUMBNAIL:
+ return sThumbnailTargetSize;
+ case TYPE_MICROTHUMBNAIL:
+ return sMicrothumbnailTargetSize;
+ default:
+ throw new RuntimeException(
+ "should only request thumb/microthumb from cache");
+ }
+ }
+
+ public static BytesBufferPool getBytesBufferPool() {
+ return sMicroThumbBufferPool;
+ }
+
+ public static void setThumbnailSizes(int size, int microSize) {
+ sThumbnailTargetSize = size;
+ if (sMicrothumbnailTargetSize != microSize) {
+ sMicrothumbnailTargetSize = microSize;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
new file mode 100644
index 000000000..270d4cf0b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+
+public abstract class MediaObject {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaObject";
+ public static final long INVALID_DATA_VERSION = -1;
+
+ // These are the bits returned from getSupportedOperations():
+ public static final int SUPPORT_DELETE = 1 << 0;
+ public static final int SUPPORT_ROTATE = 1 << 1;
+ public static final int SUPPORT_SHARE = 1 << 2;
+ public static final int SUPPORT_CROP = 1 << 3;
+ public static final int SUPPORT_SHOW_ON_MAP = 1 << 4;
+ public static final int SUPPORT_SETAS = 1 << 5;
+ public static final int SUPPORT_FULL_IMAGE = 1 << 6;
+ public static final int SUPPORT_PLAY = 1 << 7;
+ public static final int SUPPORT_CACHE = 1 << 8;
+ public static final int SUPPORT_EDIT = 1 << 9;
+ public static final int SUPPORT_INFO = 1 << 10;
+ public static final int SUPPORT_TRIM = 1 << 11;
+ public static final int SUPPORT_UNLOCK = 1 << 12;
+ public static final int SUPPORT_BACK = 1 << 13;
+ public static final int SUPPORT_ACTION = 1 << 14;
+ public static final int SUPPORT_CAMERA_SHORTCUT = 1 << 15;
+ public static final int SUPPORT_MUTE = 1 << 16;
+ public static final int SUPPORT_ALL = 0xffffffff;
+
+ // These are the bits returned from getMediaType():
+ public static final int MEDIA_TYPE_UNKNOWN = 1;
+ public static final int MEDIA_TYPE_IMAGE = 2;
+ public static final int MEDIA_TYPE_VIDEO = 4;
+ public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO;
+
+ public static final String MEDIA_TYPE_IMAGE_STRING = "image";
+ public static final String MEDIA_TYPE_VIDEO_STRING = "video";
+ public static final String MEDIA_TYPE_ALL_STRING = "all";
+
+ // These are flags for cache() and return values for getCacheFlag():
+ public static final int CACHE_FLAG_NO = 0;
+ public static final int CACHE_FLAG_SCREENNAIL = 1;
+ public static final int CACHE_FLAG_FULL = 2;
+
+ // These are return values for getCacheStatus():
+ public static final int CACHE_STATUS_NOT_CACHED = 0;
+ public static final int CACHE_STATUS_CACHING = 1;
+ public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2;
+ public static final int CACHE_STATUS_CACHED_FULL = 3;
+
+ private static long sVersionSerial = 0;
+
+ protected long mDataVersion;
+
+ protected final Path mPath;
+
+ public interface PanoramaSupportCallback {
+ void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360);
+ }
+
+ public MediaObject(Path path, long version) {
+ path.setObject(this);
+ mPath = path;
+ mDataVersion = version;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public int getSupportedOperations() {
+ return 0;
+ }
+
+ public void getPanoramaSupport(PanoramaSupportCallback callback) {
+ callback.panoramaInfoAvailable(this, false, false);
+ }
+
+ public void clearCachedPanoramaSupport() {
+ }
+
+ public void delete() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void rotate(int degrees) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getContentUri() {
+ String className = getClass().getName();
+ Log.e(TAG, "Class " + className + "should implement getContentUri.");
+ Log.e(TAG, "The object was created from path: " + getPath());
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getPlayUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getMediaType() {
+ return MEDIA_TYPE_UNKNOWN;
+ }
+
+ public MediaDetails getDetails() {
+ MediaDetails details = new MediaDetails();
+ return details;
+ }
+
+ public long getDataVersion() {
+ return mDataVersion;
+ }
+
+ public int getCacheFlag() {
+ return CACHE_FLAG_NO;
+ }
+
+ public int getCacheStatus() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long getCacheSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void cache(int flag) {
+ throw new UnsupportedOperationException();
+ }
+
+ public static synchronized long nextVersionNumber() {
+ return ++MediaObject.sVersionSerial;
+ }
+
+ public static int getTypeFromString(String s) {
+ if (MEDIA_TYPE_ALL_STRING.equals(s)) return MediaObject.MEDIA_TYPE_ALL;
+ if (MEDIA_TYPE_IMAGE_STRING.equals(s)) return MediaObject.MEDIA_TYPE_IMAGE;
+ if (MEDIA_TYPE_VIDEO_STRING.equals(s)) return MediaObject.MEDIA_TYPE_VIDEO;
+ throw new IllegalArgumentException(s);
+ }
+
+ public static String getTypeString(int type) {
+ switch (type) {
+ case MEDIA_TYPE_IMAGE: return MEDIA_TYPE_IMAGE_STRING;
+ case MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO_STRING;
+ case MEDIA_TYPE_ALL: return MEDIA_TYPE_ALL_STRING;
+ }
+ throw new IllegalArgumentException();
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
new file mode 100644
index 000000000..683aa6b32
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.Future;
+
+import java.util.ArrayList;
+import java.util.WeakHashMap;
+
+// MediaSet is a directory-like data structure.
+// It contains MediaItems and sub-MediaSets.
+//
+// The primary interface are:
+// getMediaItemCount(), getMediaItem() and
+// getSubMediaSetCount(), getSubMediaSet().
+//
+// getTotalMediaItemCount() returns the number of all MediaItems, including
+// those in sub-MediaSets.
+public abstract class MediaSet extends MediaObject {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaSet";
+
+ public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
+ public static final int INDEX_NOT_FOUND = -1;
+
+ public static final int SYNC_RESULT_SUCCESS = 0;
+ public static final int SYNC_RESULT_CANCELLED = 1;
+ public static final int SYNC_RESULT_ERROR = 2;
+
+ /** Listener to be used with requestSync(SyncListener). */
+ public static interface SyncListener {
+ /**
+ * Called when the sync task completed. Completion may be due to normal termination,
+ * an exception, or cancellation.
+ *
+ * @param mediaSet the MediaSet that's done with sync
+ * @param resultCode one of the SYNC_RESULT_* constants
+ */
+ void onSyncDone(MediaSet mediaSet, int resultCode);
+ }
+
+ public MediaSet(Path path, long version) {
+ super(path, version);
+ }
+
+ public int getMediaItemCount() {
+ return 0;
+ }
+
+ // Returns the media items in the range [start, start + count).
+ //
+ // The number of media items returned may be less than the specified count
+ // if there are not enough media items available. The number of
+ // media items available may not be consistent with the return value of
+ // getMediaItemCount() because the contents of database may have already
+ // changed.
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ return new ArrayList<MediaItem>();
+ }
+
+ public MediaItem getCoverMediaItem() {
+ ArrayList<MediaItem> items = getMediaItem(0, 1);
+ if (items.size() > 0) return items.get(0);
+ for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+ MediaItem cover = getSubMediaSet(i).getCoverMediaItem();
+ if (cover != null) return cover;
+ }
+ return null;
+ }
+
+ public int getSubMediaSetCount() {
+ return 0;
+ }
+
+ public MediaSet getSubMediaSet(int index) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ public boolean isLeafAlbum() {
+ return false;
+ }
+
+ public boolean isCameraRoll() {
+ return false;
+ }
+
+ /**
+ * Method {@link #reload()} may process the loading task in background, this method tells
+ * its client whether the loading is still in process or not.
+ */
+ public boolean isLoading() {
+ return false;
+ }
+
+ public int getTotalMediaItemCount() {
+ int total = getMediaItemCount();
+ for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+ total += getSubMediaSet(i).getTotalMediaItemCount();
+ }
+ return total;
+ }
+
+ // TODO: we should have better implementation of sub classes
+ public int getIndexOfItem(Path path, int hint) {
+ // hint < 0 is handled below
+ // first, try to find it around the hint
+ int start = Math.max(0,
+ hint - MEDIAITEM_BATCH_FETCH_COUNT / 2);
+ ArrayList<MediaItem> list = getMediaItem(
+ start, MEDIAITEM_BATCH_FETCH_COUNT);
+ int index = getIndexOf(path, list);
+ if (index != INDEX_NOT_FOUND) return start + index;
+
+ // try to find it globally
+ start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0;
+ list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+ while (true) {
+ index = getIndexOf(path, list);
+ if (index != INDEX_NOT_FOUND) return start + index;
+ if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND;
+ start += MEDIAITEM_BATCH_FETCH_COUNT;
+ list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+ }
+ }
+
+ protected int getIndexOf(Path path, ArrayList<MediaItem> list) {
+ for (int i = 0, n = list.size(); i < n; ++i) {
+ // item could be null only in ClusterAlbum
+ MediaObject item = list.get(i);
+ if (item != null && item.mPath == path) return i;
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+ public abstract String getName();
+
+ private WeakHashMap<ContentListener, Object> mListeners =
+ new WeakHashMap<ContentListener, Object>();
+
+ // NOTE: The MediaSet only keeps a weak reference to the listener. The
+ // listener is automatically removed when there is no other reference to
+ // the listener.
+ public void addContentListener(ContentListener listener) {
+ mListeners.put(listener, null);
+ }
+
+ public void removeContentListener(ContentListener listener) {
+ mListeners.remove(listener);
+ }
+
+ // This should be called by subclasses when the content is changed.
+ public void notifyContentChanged() {
+ for (ContentListener listener : mListeners.keySet()) {
+ listener.onContentDirty();
+ }
+ }
+
+ // Reload the content. Return the current data version. reload() should be called
+ // in the same thread as getMediaItem(int, int) and getSubMediaSet(int).
+ public abstract long reload();
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ details.addDetail(MediaDetails.INDEX_TITLE, getName());
+ return details;
+ }
+
+ // Enumerate all media items in this media set (including the ones in sub
+ // media sets), in an efficient order. ItemConsumer.consumer() will be
+ // called for each media item with its index.
+ public void enumerateMediaItems(ItemConsumer consumer) {
+ enumerateMediaItems(consumer, 0);
+ }
+
+ public void enumerateTotalMediaItems(ItemConsumer consumer) {
+ enumerateTotalMediaItems(consumer, 0);
+ }
+
+ public static interface ItemConsumer {
+ void consume(int index, MediaItem item);
+ }
+
+ // The default implementation uses getMediaItem() for enumerateMediaItems().
+ // Subclasses may override this and use more efficient implementations.
+ // Returns the number of items enumerated.
+ protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
+ int total = getMediaItemCount();
+ int start = 0;
+ while (start < total) {
+ int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start);
+ ArrayList<MediaItem> items = getMediaItem(start, count);
+ for (int i = 0, n = items.size(); i < n; i++) {
+ MediaItem item = items.get(i);
+ consumer.consume(startIndex + start + i, item);
+ }
+ start += count;
+ }
+ return total;
+ }
+
+ // Recursively enumerate all media items under this set.
+ // Returns the number of items enumerated.
+ protected int enumerateTotalMediaItems(
+ ItemConsumer consumer, int startIndex) {
+ int start = 0;
+ start += enumerateMediaItems(consumer, startIndex);
+ int m = getSubMediaSetCount();
+ for (int i = 0; i < m; i++) {
+ start += getSubMediaSet(i).enumerateTotalMediaItems(
+ consumer, startIndex + start);
+ }
+ return start;
+ }
+
+ /**
+ * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
+ * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
+ * defined in this class and can be obtained by Future.get().
+ *
+ * Subclasses should perform sync on a different thread.
+ *
+ * The default implementation here returns a Future stub that does nothing and returns
+ * SYNC_RESULT_SUCCESS by get().
+ */
+ public Future<Integer> requestSync(SyncListener listener) {
+ listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
+ return FUTURE_STUB;
+ }
+
+ private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
+ @Override
+ public void cancel() {}
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Integer get() {
+ return SYNC_RESULT_SUCCESS;
+ }
+
+ @Override
+ public void waitDone() {}
+ };
+
+ protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
+ return new MultiSetSyncFuture(sets, listener);
+ }
+
+ private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
+ @SuppressWarnings("hiding")
+ private static final String TAG = "Gallery.MultiSetSync";
+
+ private final SyncListener mListener;
+ private final Future<Integer> mFutures[];
+
+ private boolean mIsCancelled = false;
+ private int mResult = -1;
+ private int mPendingCount;
+
+ @SuppressWarnings("unchecked")
+ MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) {
+ mListener = listener;
+ mPendingCount = sets.length;
+ mFutures = new Future[sets.length];
+
+ synchronized (this) {
+ for (int i = 0, n = sets.length; i < n; ++i) {
+ mFutures[i] = sets[i].requestSync(this);
+ Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName()));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void cancel() {
+ if (mIsCancelled) return;
+ mIsCancelled = true;
+ for (Future<Integer> future : mFutures) future.cancel();
+ if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mPendingCount == 0;
+ }
+
+ @Override
+ public synchronized Integer get() {
+ waitDone();
+ return mResult;
+ }
+
+ @Override
+ public synchronized void waitDone() {
+ try {
+ while (!isDone()) wait();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "waitDone() interrupted");
+ }
+ }
+
+ // SyncListener callback
+ @Override
+ public void onSyncDone(MediaSet mediaSet, int resultCode) {
+ SyncListener listener = null;
+ synchronized (this) {
+ if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR;
+ --mPendingCount;
+ if (mPendingCount == 0) {
+ listener = mListener;
+ notifyAll();
+ }
+ Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
+ + " #pending=" + mPendingCount);
+ }
+ if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
new file mode 100644
index 000000000..95901283b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSource.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+import java.util.ArrayList;
+
+public abstract class MediaSource {
+ private static final String TAG = "MediaSource";
+ private String mPrefix;
+
+ protected MediaSource(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public String getPrefix() {
+ return mPrefix;
+ }
+
+ public Path findPathByUri(Uri uri, String type) {
+ return null;
+ }
+
+ public abstract MediaObject createMediaObject(Path path);
+
+ public void pause() {
+ }
+
+ public void resume() {
+ }
+
+ public Path getDefaultSetOf(Path item) {
+ return null;
+ }
+
+ public long getTotalUsedCacheSize() {
+ return 0;
+ }
+
+ public long getTotalTargetCacheSize() {
+ return 0;
+ }
+
+ public static class PathId {
+ public PathId(Path path, int id) {
+ this.path = path;
+ this.id = id;
+ }
+ public Path path;
+ public int id;
+ }
+
+ // Maps a list of Paths (all belong to this MediaSource) to MediaItems,
+ // and invoke consumer.consume() for each MediaItem with the given id.
+ //
+ // This default implementation uses getMediaObject for each Path. Subclasses
+ // may override this and provide more efficient implementation (like
+ // batching the database query).
+ public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+ int n = list.size();
+ for (int i = 0; i < n; i++) {
+ PathId pid = list.get(i);
+ MediaObject obj;
+ synchronized (DataManager.LOCK) {
+ obj = pid.path.getObject();
+ if (obj == null) {
+ try {
+ obj = createMediaObject(pid.path);
+ } catch (Throwable th) {
+ Log.w(TAG, "cannot create media object: " + pid.path, th);
+ }
+ }
+ }
+ if (obj != null) {
+ consumer.consume(pid.id, (MediaItem) obj);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java
new file mode 100644
index 000000000..737b5b60d
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpClient.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.mtp.MtpStorageInfo;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class helps an application manage a list of connected MTP or PTP devices.
+ * It listens for MTP devices being attached and removed from the USB host bus
+ * and notifies the application when the MTP device list changes.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB_MR1)
+public class MtpClient {
+
+ private static final String TAG = "MtpClient";
+
+ private static final String ACTION_USB_PERMISSION =
+ "android.mtp.MtpClient.action.USB_PERMISSION";
+
+ private final Context mContext;
+ private final UsbManager mUsbManager;
+ private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+ // mDevices contains all MtpDevices that have been seen by our client,
+ // so we can inform when the device has been detached.
+ // mDevices is also used for synchronization in this class.
+ private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
+ // List of MTP devices we should not try to open for which we are currently
+ // asking for permission to open.
+ private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>();
+ // List of MTP devices we should not try to open.
+ // We add devices to this list if the user canceled a permission request or we were
+ // unable to open the device.
+ private final ArrayList<String> mIgnoredDevices = new ArrayList<String>();
+
+ private final PendingIntent mPermissionIntent;
+
+ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ String deviceName = usbDevice.getDeviceName();
+
+ synchronized (mDevices) {
+ MtpDevice mtpDevice = mDevices.get(deviceName);
+
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+ if (mtpDevice != null) {
+ mDevices.remove(deviceName);
+ mRequestPermissionDevices.remove(deviceName);
+ mIgnoredDevices.remove(deviceName);
+ for (Listener listener : mListeners) {
+ listener.deviceRemoved(mtpDevice);
+ }
+ }
+ } else if (ACTION_USB_PERMISSION.equals(action)) {
+ mRequestPermissionDevices.remove(deviceName);
+ boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+ false);
+ Log.d(TAG, "ACTION_USB_PERMISSION: " + permission);
+ if (permission) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else {
+ // so we don't ask for permission again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * An interface for being notified when MTP or PTP devices are attached
+ * or removed. In the current implementation, only PTP devices are supported.
+ */
+ public interface Listener {
+ /**
+ * Called when a new device has been added
+ *
+ * @param device the new device that was added
+ */
+ public void deviceAdded(MtpDevice device);
+
+ /**
+ * Called when a new device has been removed
+ *
+ * @param device the device that was removed
+ */
+ public void deviceRemoved(MtpDevice device);
+ }
+
+ /**
+ * Tests to see if a {@link android.hardware.usb.UsbDevice}
+ * supports the PTP protocol (typically used by digital cameras)
+ *
+ * @param device the device to test
+ * @return true if the device is a PTP device.
+ */
+ static public boolean isCamera(UsbDevice device) {
+ int count = device.getInterfaceCount();
+ for (int i = 0; i < count; i++) {
+ UsbInterface intf = device.getInterface(i);
+ if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
+ intf.getInterfaceSubclass() == 1 &&
+ intf.getInterfaceProtocol() == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * MtpClient constructor
+ *
+ * @param context the {@link android.content.Context} to use for the MtpClient
+ */
+ public MtpClient(Context context) {
+ mContext = context;
+ mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
+ mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ filter.addAction(ACTION_USB_PERMISSION);
+ context.registerReceiver(mUsbReceiver, filter);
+ }
+
+ /**
+ * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP
+ * device and return an {@link android.mtp.MtpDevice} for it.
+ *
+ * @param usbDevice the device to open
+ * @return an MtpDevice for the device.
+ */
+ private MtpDevice openDeviceLocked(UsbDevice usbDevice) {
+ String deviceName = usbDevice.getDeviceName();
+
+ // don't try to open devices that we have decided to ignore
+ // or are currently asking permission for
+ if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName)
+ && !mRequestPermissionDevices.contains(deviceName)) {
+ if (!mUsbManager.hasPermission(usbDevice)) {
+ mUsbManager.requestPermission(usbDevice, mPermissionIntent);
+ mRequestPermissionDevices.add(deviceName);
+ } else {
+ UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice);
+ if (connection != null) {
+ MtpDevice mtpDevice = new MtpDevice(usbDevice);
+ if (mtpDevice.open(connection)) {
+ mDevices.put(usbDevice.getDeviceName(), mtpDevice);
+ return mtpDevice;
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Closes all resources related to the MtpClient object
+ */
+ public void close() {
+ mContext.unregisterReceiver(mUsbReceiver);
+ }
+
+ /**
+ * Registers a {@link com.android.gallery3d.data.MtpClient.Listener} interface to receive
+ * notifications when MTP or PTP devices are added or removed.
+ *
+ * @param listener the listener to register
+ */
+ public void addListener(Listener listener) {
+ synchronized (mDevices) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a {@link com.android.gallery3d.data.MtpClient.Listener} interface.
+ *
+ * @param listener the listener to unregister
+ */
+ public void removeListener(Listener listener) {
+ synchronized (mDevices) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+ * with the given name.
+ *
+ * @param deviceName the name of the USB device
+ * @return the MtpDevice, or null if it does not exist
+ */
+ public MtpDevice getDevice(String deviceName) {
+ synchronized (mDevices) {
+ return mDevices.get(deviceName);
+ }
+ }
+
+ /**
+ * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+ * with the given ID.
+ *
+ * @param id the ID of the USB device
+ * @return the MtpDevice, or null if it does not exist
+ */
+ public MtpDevice getDevice(int id) {
+ synchronized (mDevices) {
+ return mDevices.get(UsbDevice.getDeviceName(id));
+ }
+ }
+
+ /**
+ * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}.
+ *
+ * @return the list of MtpDevices
+ */
+ public List<MtpDevice> getDeviceList() {
+ synchronized (mDevices) {
+ // Query the USB manager since devices might have attached
+ // before we added our listener.
+ for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
+ if (mDevices.get(usbDevice.getDeviceName()) == null) {
+ openDeviceLocked(usbDevice);
+ }
+ }
+
+ return new ArrayList<MtpDevice>(mDevices.values());
+ }
+ }
+
+ /**
+ * Retrieves a list of all {@link android.mtp.MtpStorageInfo}
+ * for the MTP or PTP device with the given USB device name
+ *
+ * @param deviceName the name of the USB device
+ * @return the list of MtpStorageInfo
+ */
+ public List<MtpStorageInfo> getStorageList(String deviceName) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ int[] storageIds = device.getStorageIds();
+ if (storageIds == null) {
+ return null;
+ }
+
+ int length = storageIds.length;
+ ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length);
+ for (int i = 0; i < length; i++) {
+ MtpStorageInfo info = device.getStorageInfo(storageIds[i]);
+ if (info == null) {
+ Log.w(TAG, "getStorageInfo failed");
+ } else {
+ storageList.add(info);
+ }
+ }
+ return storageList;
+ }
+
+ /**
+ * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on
+ * the MTP or PTP device with the given USB device name with the given
+ * object handle
+ *
+ * @param deviceName the name of the USB device
+ * @param objectHandle handle of the object to query
+ * @return the MtpObjectInfo
+ */
+ public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getObjectInfo(objectHandle);
+ }
+
+ /**
+ * Deletes an object on the MTP or PTP device with the given USB device name.
+ *
+ * @param deviceName the name of the USB device
+ * @param objectHandle handle of the object to delete
+ * @return true if the deletion succeeds
+ */
+ public boolean deleteObject(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return false;
+ }
+ return device.deleteObject(objectHandle);
+ }
+
+ /**
+ * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects
+ * on the MTP or PTP device with the given USB device name and given storage ID
+ * and/or object handle.
+ * If the object handle is zero, then all objects in the root of the storage unit
+ * will be returned. Otherwise, all immediate children of the object will be returned.
+ * If the storage ID is also zero, then all objects on all storage units will be returned.
+ *
+ * @param deviceName the name of the USB device
+ * @param storageId the ID of the storage unit to query, or zero for all
+ * @param objectHandle the handle of the parent object to query, or zero for the storage root
+ * @return the list of MtpObjectInfo
+ */
+ public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ if (objectHandle == 0) {
+ // all objects in root of storage
+ objectHandle = 0xFFFFFFFF;
+ }
+ int[] handles = device.getObjectHandles(storageId, 0, objectHandle);
+ if (handles == null) {
+ return null;
+ }
+
+ int length = handles.length;
+ ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length);
+ for (int i = 0; i < length; i++) {
+ MtpObjectInfo info = device.getObjectInfo(handles[i]);
+ if (info == null) {
+ Log.w(TAG, "getObjectInfo failed");
+ } else {
+ objectList.add(info);
+ }
+ }
+ return objectList;
+ }
+
+ /**
+ * Returns the data for an object as a byte array.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @param objectSize the size of the object (this should match
+ * {@link android.mtp.MtpObjectInfo#getCompressedSize}
+ * @return the object's data, or null if reading fails
+ */
+ public byte[] getObject(String deviceName, int objectHandle, int objectSize) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getObject(objectHandle, objectSize);
+ }
+
+ /**
+ * Returns the thumbnail data for an object as a byte array.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @return the object's thumbnail, or null if reading fails
+ */
+ public byte[] getThumbnail(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getThumbnail(objectHandle);
+ }
+
+ /**
+ * Copies the data for an object to a file in external storage.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @param destPath path to destination for the file transfer.
+ * This path should be in the external storage as defined by
+ * {@link android.os.Environment#getExternalStorageDirectory}
+ * @return true if the file transfer succeeds
+ */
+ public boolean importFile(String deviceName, int objectHandle, String destPath) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return false;
+ }
+ return device.importFile(objectHandle, destPath);
+ }
+}
diff --git a/src/com/android/gallery3d/data/PanoramaMetadataJob.java b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
new file mode 100644
index 000000000..ab99d6a81
--- /dev/null
+++ b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class PanoramaMetadataJob implements Job<PanoramaMetadata> {
+ Context mContext;
+ Uri mUri;
+
+ public PanoramaMetadataJob(Context context, Uri uri) {
+ mContext = context;
+ mUri = uri;
+ }
+
+ @Override
+ public PanoramaMetadata run(JobContext jc) {
+ return LightCycleHelper.getPanoramaMetadata(mContext, mUri);
+ }
+}
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
new file mode 100644
index 000000000..fcae65e66
--- /dev/null
+++ b/src/com/android/gallery3d/data/Path.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IdentityCache;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class Path {
+ private static final String TAG = "Path";
+ private static Path sRoot = new Path(null, "ROOT");
+
+ private final Path mParent;
+ private final String mSegment;
+ private WeakReference<MediaObject> mObject;
+ private IdentityCache<String, Path> mChildren;
+
+ private Path(Path parent, String segment) {
+ mParent = parent;
+ mSegment = segment;
+ }
+
+ public Path getChild(String segment) {
+ synchronized (Path.class) {
+ if (mChildren == null) {
+ mChildren = new IdentityCache<String, Path>();
+ } else {
+ Path p = mChildren.get(segment);
+ if (p != null) return p;
+ }
+
+ Path p = new Path(this, segment);
+ mChildren.put(segment, p);
+ return p;
+ }
+ }
+
+ public Path getParent() {
+ synchronized (Path.class) {
+ return mParent;
+ }
+ }
+
+ public Path getChild(int segment) {
+ return getChild(String.valueOf(segment));
+ }
+
+ public Path getChild(long segment) {
+ return getChild(String.valueOf(segment));
+ }
+
+ public void setObject(MediaObject object) {
+ synchronized (Path.class) {
+ Utils.assertTrue(mObject == null || mObject.get() == null);
+ mObject = new WeakReference<MediaObject>(object);
+ }
+ }
+
+ MediaObject getObject() {
+ synchronized (Path.class) {
+ return (mObject == null) ? null : mObject.get();
+ }
+ }
+
+ @Override
+ // TODO: toString() should be more efficient, will fix it later
+ public String toString() {
+ synchronized (Path.class) {
+ StringBuilder sb = new StringBuilder();
+ String[] segments = split();
+ for (int i = 0; i < segments.length; i++) {
+ sb.append("/");
+ sb.append(segments[i]);
+ }
+ return sb.toString();
+ }
+ }
+
+ public boolean equalsIgnoreCase (String p) {
+ String path = toString();
+ return path.equalsIgnoreCase(p);
+ }
+
+ public static Path fromString(String s) {
+ synchronized (Path.class) {
+ String[] segments = split(s);
+ Path current = sRoot;
+ for (int i = 0; i < segments.length; i++) {
+ current = current.getChild(segments[i]);
+ }
+ return current;
+ }
+ }
+
+ public String[] split() {
+ synchronized (Path.class) {
+ int n = 0;
+ for (Path p = this; p != sRoot; p = p.mParent) {
+ n++;
+ }
+ String[] segments = new String[n];
+ int i = n - 1;
+ for (Path p = this; p != sRoot; p = p.mParent) {
+ segments[i--] = p.mSegment;
+ }
+ return segments;
+ }
+ }
+
+ public static String[] split(String s) {
+ int n = s.length();
+ if (n == 0) return new String[0];
+ if (s.charAt(0) != '/') {
+ throw new RuntimeException("malformed path:" + s);
+ }
+ ArrayList<String> segments = new ArrayList<String>();
+ int i = 1;
+ while (i < n) {
+ int brace = 0;
+ int j;
+ for (j = i; j < n; j++) {
+ char c = s.charAt(j);
+ if (c == '{') ++brace;
+ else if (c == '}') --brace;
+ else if (brace == 0 && c == '/') break;
+ }
+ if (brace != 0) {
+ throw new RuntimeException("unbalanced brace in path:" + s);
+ }
+ segments.add(s.substring(i, j));
+ i = j + 1;
+ }
+ String[] result = new String[segments.size()];
+ segments.toArray(result);
+ return result;
+ }
+
+ // Splits a string to an array of strings.
+ // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}.
+ public static String[] splitSequence(String s) {
+ int n = s.length();
+ if (s.charAt(0) != '{' || s.charAt(n-1) != '}') {
+ throw new RuntimeException("bad sequence: " + s);
+ }
+ ArrayList<String> segments = new ArrayList<String>();
+ int i = 1;
+ while (i < n - 1) {
+ int brace = 0;
+ int j;
+ for (j = i; j < n - 1; j++) {
+ char c = s.charAt(j);
+ if (c == '{') ++brace;
+ else if (c == '}') --brace;
+ else if (brace == 0 && c == ',') break;
+ }
+ if (brace != 0) {
+ throw new RuntimeException("unbalanced brace in path:" + s);
+ }
+ segments.add(s.substring(i, j));
+ i = j + 1;
+ }
+ String[] result = new String[segments.size()];
+ segments.toArray(result);
+ return result;
+ }
+
+ public String getPrefix() {
+ if (this == sRoot) return "";
+ return getPrefixPath().mSegment;
+ }
+
+ public Path getPrefixPath() {
+ synchronized (Path.class) {
+ Path current = this;
+ if (current == sRoot) {
+ throw new IllegalStateException();
+ }
+ while (current.mParent != sRoot) {
+ current = current.mParent;
+ }
+ return current;
+ }
+ }
+
+ public String getSuffix() {
+ // We don't need lock because mSegment is final.
+ return mSegment;
+ }
+
+ // Below are for testing/debugging only
+ static void clearAll() {
+ synchronized (Path.class) {
+ sRoot = new Path(null, "");
+ }
+ }
+
+ static void dumpAll() {
+ dumpAll(sRoot, "", "");
+ }
+
+ static void dumpAll(Path p, String prefix1, String prefix2) {
+ synchronized (Path.class) {
+ MediaObject obj = p.getObject();
+ Log.d(TAG, prefix1 + p.mSegment + ":"
+ + (obj == null ? "null" : obj.getClass().getSimpleName()));
+ if (p.mChildren != null) {
+ ArrayList<String> childrenKeys = p.mChildren.keys();
+ int i = 0, n = childrenKeys.size();
+ for (String key : childrenKeys) {
+ Path child = p.mChildren.get(key);
+ if (child == null) {
+ ++i;
+ continue;
+ }
+ Log.d(TAG, prefix2 + "|");
+ if (++i < n) {
+ dumpAll(child, prefix2 + "+-- ", prefix2 + "| ");
+ } else {
+ dumpAll(child, prefix2 + "+-- ", prefix2 + " ");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java
new file mode 100644
index 000000000..9c6b840d5
--- /dev/null
+++ b/src/com/android/gallery3d/data/PathMatcher.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class PathMatcher {
+ public static final int NOT_FOUND = -1;
+
+ private ArrayList<String> mVariables = new ArrayList<String>();
+ private Node mRoot = new Node();
+
+ public PathMatcher() {
+ mRoot = new Node();
+ }
+
+ public void add(String pattern, int kind) {
+ String[] segments = Path.split(pattern);
+ Node current = mRoot;
+ for (int i = 0; i < segments.length; i++) {
+ current = current.addChild(segments[i]);
+ }
+ current.setKind(kind);
+ }
+
+ public int match(Path path) {
+ String[] segments = path.split();
+ mVariables.clear();
+ Node current = mRoot;
+ for (int i = 0; i < segments.length; i++) {
+ Node next = current.getChild(segments[i]);
+ if (next == null) {
+ next = current.getChild("*");
+ if (next != null) {
+ mVariables.add(segments[i]);
+ } else {
+ return NOT_FOUND;
+ }
+ }
+ current = next;
+ }
+ return current.getKind();
+ }
+
+ public String getVar(int index) {
+ return mVariables.get(index);
+ }
+
+ public int getIntVar(int index) {
+ return Integer.parseInt(mVariables.get(index));
+ }
+
+ public long getLongVar(int index) {
+ return Long.parseLong(mVariables.get(index));
+ }
+
+ private static class Node {
+ private HashMap<String, Node> mMap;
+ private int mKind = NOT_FOUND;
+
+ Node addChild(String segment) {
+ if (mMap == null) {
+ mMap = new HashMap<String, Node>();
+ } else {
+ Node node = mMap.get(segment);
+ if (node != null) return node;
+ }
+
+ Node n = new Node();
+ mMap.put(segment, n);
+ return n;
+ }
+
+ Node getChild(String segment) {
+ if (mMap == null) return null;
+ return mMap.get(segment);
+ }
+
+ void setKind(int kind) {
+ mKind = kind;
+ }
+
+ int getKind() {
+ return mKind;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/SecureAlbum.java b/src/com/android/gallery3d/data/SecureAlbum.java
new file mode 100644
index 000000000..204f848f8
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureAlbum.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.util.ArrayList;
+
+// This class lists all media items added by the client.
+public class SecureAlbum extends MediaSet implements StitchingChangeListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SecureAlbum";
+ private static final String[] PROJECTION = {MediaColumns._ID};
+ private int mMinImageId = Integer.MAX_VALUE; // the smallest id of images
+ private int mMaxImageId = Integer.MIN_VALUE; // the biggest id in images
+ private int mMinVideoId = Integer.MAX_VALUE; // the smallest id of videos
+ private int mMaxVideoId = Integer.MIN_VALUE; // the biggest id of videos
+ // All the media items added by the client.
+ private ArrayList<Path> mAllItems = new ArrayList<Path>();
+ // The types of items in mAllItems. True is video and false is image.
+ private ArrayList<Boolean> mAllItemTypes = new ArrayList<Boolean>();
+ private ArrayList<Path> mExistingItems = new ArrayList<Path>();
+ private Context mContext;
+ private DataManager mDataManager;
+ private static final Uri[] mWatchUris =
+ {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+ private final ChangeNotifier mNotifier;
+ // A placeholder image in the end of secure album. When it is tapped, it
+ // will take the user to the lock screen.
+ private MediaItem mUnlockItem;
+ private boolean mShowUnlockItem;
+
+ public SecureAlbum(Path path, GalleryApp application, MediaItem unlock) {
+ super(path, nextVersionNumber());
+ mContext = application.getAndroidContext();
+ mDataManager = application.getDataManager();
+ mNotifier = new ChangeNotifier(this, mWatchUris, application);
+ mUnlockItem = unlock;
+ mShowUnlockItem = (!isCameraBucketEmpty(Images.Media.EXTERNAL_CONTENT_URI)
+ || !isCameraBucketEmpty(Video.Media.EXTERNAL_CONTENT_URI));
+ }
+
+ public void addMediaItem(boolean isVideo, int id) {
+ Path pathBase;
+ if (isVideo) {
+ pathBase = LocalVideo.ITEM_PATH;
+ mMinVideoId = Math.min(mMinVideoId, id);
+ mMaxVideoId = Math.max(mMaxVideoId, id);
+ } else {
+ pathBase = LocalImage.ITEM_PATH;
+ mMinImageId = Math.min(mMinImageId, id);
+ mMaxImageId = Math.max(mMaxImageId, id);
+ }
+ Path path = pathBase.getChild(id);
+ if (!mAllItems.contains(path)) {
+ mAllItems.add(path);
+ mAllItemTypes.add(isVideo);
+ mNotifier.fakeChange();
+ }
+ }
+
+ // The sequence is stitching items, local media items, and unlock image.
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ int existingCount = mExistingItems.size();
+ if (start >= existingCount + 1) {
+ return new ArrayList<MediaItem>();
+ }
+
+ // Add paths of requested stitching items.
+ int end = Math.min(start + count, existingCount);
+ ArrayList<Path> subset = new ArrayList<Path>(mExistingItems.subList(start, end));
+
+ // Convert paths to media items.
+ final MediaItem[] buf = new MediaItem[end - start];
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ buf[index] = item;
+ }
+ };
+ mDataManager.mapMediaItems(subset, consumer, 0);
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
+ for (int i = 0; i < buf.length; i++) {
+ result.add(buf[i]);
+ }
+ if (mShowUnlockItem) result.add(mUnlockItem);
+ return result;
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return (mExistingItems.size() + (mShowUnlockItem ? 1 : 0));
+ }
+
+ @Override
+ public String getName() {
+ return "secure";
+ }
+
+ @Override
+ public long reload() {
+ if (mNotifier.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ updateExistingItems();
+ }
+ return mDataVersion;
+ }
+
+ private ArrayList<Integer> queryExistingIds(Uri uri, int minId, int maxId) {
+ ArrayList<Integer> ids = new ArrayList<Integer>();
+ if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) return ids;
+
+ String[] selectionArgs = {String.valueOf(minId), String.valueOf(maxId)};
+ Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+ "_id BETWEEN ? AND ?", selectionArgs, null);
+ if (cursor == null) return ids;
+ try {
+ while (cursor.moveToNext()) {
+ ids.add(cursor.getInt(0));
+ }
+ } finally {
+ cursor.close();
+ }
+ return ids;
+ }
+
+ private boolean isCameraBucketEmpty(Uri baseUri) {
+ Uri uri = baseUri.buildUpon()
+ .appendQueryParameter("limit", "1").build();
+ String[] selection = {String.valueOf(MediaSetUtils.CAMERA_BUCKET_ID)};
+ Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+ "bucket_id = ?", selection, null);
+ if (cursor == null) return true;
+ try {
+ return (cursor.getCount() == 0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void updateExistingItems() {
+ if (mAllItems.size() == 0) return;
+
+ // Query existing ids.
+ ArrayList<Integer> imageIds = queryExistingIds(
+ Images.Media.EXTERNAL_CONTENT_URI, mMinImageId, mMaxImageId);
+ ArrayList<Integer> videoIds = queryExistingIds(
+ Video.Media.EXTERNAL_CONTENT_URI, mMinVideoId, mMaxVideoId);
+
+ // Construct the existing items list.
+ mExistingItems.clear();
+ for (int i = mAllItems.size() - 1; i >= 0; i--) {
+ Path path = mAllItems.get(i);
+ boolean isVideo = mAllItemTypes.get(i);
+ int id = Integer.parseInt(path.getSuffix());
+ if (isVideo) {
+ if (videoIds.contains(id)) mExistingItems.add(path);
+ } else {
+ if (imageIds.contains(id)) mExistingItems.add(path);
+ }
+ }
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ int id = Integer.parseInt(uri.getLastPathSegment());
+ addMediaItem(false, id);
+ }
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, final int progress) {
+ }
+}
diff --git a/src/com/android/gallery3d/data/SecureSource.java b/src/com/android/gallery3d/data/SecureSource.java
new file mode 100644
index 000000000..6bc8cc295
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureSource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class SecureSource extends MediaSource {
+ private GalleryApp mApplication;
+ private static PathMatcher mMatcher = new PathMatcher();
+ private static final int SECURE_ALBUM = 0;
+ private static final int SECURE_UNLOCK = 1;
+
+ static {
+ mMatcher.add("/secure/all/*", SECURE_ALBUM);
+ mMatcher.add("/secure/unlock", SECURE_UNLOCK);
+ }
+
+ public SecureSource(GalleryApp context) {
+ super("secure");
+ mApplication = context;
+ }
+
+ public static boolean isSecurePath(String path) {
+ return (SECURE_ALBUM == mMatcher.match(Path.fromString(path)));
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ switch (mMatcher.match(path)) {
+ case SECURE_ALBUM: {
+ DataManager dataManager = mApplication.getDataManager();
+ MediaItem unlock = (MediaItem) dataManager.getMediaObject(
+ "/secure/unlock");
+ return new SecureAlbum(path, mApplication, unlock);
+ }
+ case SECURE_UNLOCK:
+ return new UnlockImage(path, mApplication);
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/SingleItemAlbum.java b/src/com/android/gallery3d/data/SingleItemAlbum.java
new file mode 100644
index 000000000..a0093e0c3
--- /dev/null
+++ b/src/com/android/gallery3d/data/SingleItemAlbum.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class SingleItemAlbum extends MediaSet {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SingleItemAlbum";
+ private final MediaItem mItem;
+ private final String mName;
+
+ public SingleItemAlbum(Path path, MediaItem item) {
+ super(path, nextVersionNumber());
+ mItem = item;
+ mName = "SingleItemAlbum("+mItem.getClass().getSimpleName()+")";
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return 1;
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+ // If [start, start+count) contains the index 0, return the item.
+ if (start <= 0 && start + count > 0) {
+ result.add(mItem);
+ }
+
+ return result;
+ }
+
+ public MediaItem getItem() {
+ return mItem;
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public long reload() {
+ return mDataVersion;
+ }
+}
diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java
new file mode 100644
index 000000000..b809c841b
--- /dev/null
+++ b/src/com/android/gallery3d/data/SizeClustering.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class SizeClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SizeClustering";
+
+ private Context mContext;
+ private ArrayList<Path>[] mClusters;
+ private String[] mNames;
+ private long mMinSizes[];
+
+ private static final long MEGA_BYTES = 1024L*1024;
+ private static final long GIGA_BYTES = 1024L*1024*1024;
+
+ private static final long[] SIZE_LEVELS = {
+ 0,
+ 1 * MEGA_BYTES,
+ 10 * MEGA_BYTES,
+ 100 * MEGA_BYTES,
+ 1 * GIGA_BYTES,
+ 2 * GIGA_BYTES,
+ 4 * GIGA_BYTES,
+ };
+
+ public SizeClustering(Context context) {
+ mContext = context;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run(MediaSet baseSet) {
+ @SuppressWarnings("unchecked")
+ final ArrayList<Path>[] group = new ArrayList[SIZE_LEVELS.length];
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ // Find the cluster this item belongs to.
+ long size = item.getSize();
+ int i;
+ for (i = 0; i < SIZE_LEVELS.length - 1; i++) {
+ if (size < SIZE_LEVELS[i + 1]) {
+ break;
+ }
+ }
+
+ ArrayList<Path> list = group[i];
+ if (list == null) {
+ list = new ArrayList<Path>();
+ group[i] = list;
+ }
+ list.add(item.getPath());
+ }
+ });
+
+ int count = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (group[i] != null) {
+ count++;
+ }
+ }
+
+ mClusters = new ArrayList[count];
+ mNames = new String[count];
+ mMinSizes = new long[count];
+
+ Resources res = mContext.getResources();
+ int k = 0;
+ // Go through group in the reverse order, so the group with the largest
+ // size will show first.
+ for (int i = group.length - 1; i >= 0; i--) {
+ if (group[i] == null) continue;
+
+ mClusters[k] = group[i];
+ if (i == 0) {
+ mNames[k] = String.format(
+ res.getString(R.string.size_below), getSizeString(i + 1));
+ } else if (i == group.length - 1) {
+ mNames[k] = String.format(
+ res.getString(R.string.size_above), getSizeString(i));
+ } else {
+ String minSize = getSizeString(i);
+ String maxSize = getSizeString(i + 1);
+ mNames[k] = String.format(
+ res.getString(R.string.size_between), minSize, maxSize);
+ }
+ mMinSizes[k] = SIZE_LEVELS[i];
+ k++;
+ }
+ }
+
+ private String getSizeString(int index) {
+ long bytes = SIZE_LEVELS[index];
+ if (bytes >= GIGA_BYTES) {
+ return (bytes / GIGA_BYTES) + "GB";
+ } else {
+ return (bytes / MEGA_BYTES) + "MB";
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.length;
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters[index];
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+
+ public long getMinSize(int index) {
+ return mMinSizes[index];
+ }
+}
diff --git a/src/com/android/gallery3d/data/SnailAlbum.java b/src/com/android/gallery3d/data/SnailAlbum.java
new file mode 100644
index 000000000..7bce7a695
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailAlbum.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// This is a simple MediaSet which contains only one MediaItem -- a SnailItem.
+public class SnailAlbum extends SingleItemAlbum {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailAlbum";
+ private AtomicBoolean mDirty = new AtomicBoolean(false);
+
+ public SnailAlbum(Path path, SnailItem item) {
+ super(path, item);
+ }
+
+ @Override
+ public long reload() {
+ if (mDirty.compareAndSet(true, false)) {
+ ((SnailItem) getItem()).updateVersion();
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ public void notifyChange() {
+ mDirty.set(true);
+ notifyContentChanged();
+ }
+}
diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java
new file mode 100644
index 000000000..3586d2cab
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailItem.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+// SnailItem is a MediaItem which can provide a ScreenNail. This is
+// used so we can show an foreign component (like an
+// android.view.View) instead of a Bitmap.
+public class SnailItem extends MediaItem {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailItem";
+ private ScreenNail mScreenNail;
+
+ public SnailItem(Path path) {
+ super(path, nextVersionNumber());
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ // nothing to return
+ return new Job<Bitmap>() {
+ @Override
+ public Bitmap run(JobContext jc) {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ // nothing to return
+ return new Job<BitmapRegionDecoder>() {
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ return null;
+ }
+ };
+ }
+
+ // We do not provide requestImage or requestLargeImage, instead we
+ // provide a ScreenNail.
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public String getMimeType() {
+ return "";
+ }
+
+ // Returns width and height of the media item.
+ // Returns 0, 0 if the information is not available.
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Extra methods for SnailItem
+ //////////////////////////////////////////////////////////////////////////
+
+ public void setScreenNail(ScreenNail screenNail) {
+ mScreenNail = screenNail;
+ }
+
+ public void updateVersion() {
+ mDataVersion = nextVersionNumber();
+ }
+}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
new file mode 100644
index 000000000..5c690ccdb
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class SnailSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailSource";
+ private static final int SNAIL_ALBUM = 0;
+ private static final int SNAIL_ITEM = 1;
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+ private static int sNextId;
+
+ public SnailSource(GalleryApp application) {
+ super("snail");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/snail/set/*", SNAIL_ALBUM);
+ mMatcher.add("/snail/item/*", SNAIL_ITEM);
+ }
+
+ // The only path we accept is "/snail/set/id" and "/snail/item/id"
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ DataManager dataManager = mApplication.getDataManager();
+ switch (mMatcher.match(path)) {
+ case SNAIL_ALBUM:
+ String itemPath = "/snail/item/" + mMatcher.getVar(0);
+ SnailItem item =
+ (SnailItem) dataManager.getMediaObject(itemPath);
+ return new SnailAlbum(path, item);
+ case SNAIL_ITEM: {
+ int id = mMatcher.getIntVar(0);
+ return new SnailItem(path);
+ }
+ }
+ return null;
+ }
+
+ // Registers a new SnailAlbum containing a SnailItem and returns the id of
+ // them. You can obtain the Path of the SnailAlbum and SnailItem associated
+ // with the id by getSetPath and getItemPath().
+ public static synchronized int newId() {
+ return sNextId++;
+ }
+
+ public static Path getSetPath(int id) {
+ return Path.fromString("/snail/set").getChild(id);
+ }
+
+ public static Path getItemPath(int id) {
+ return Path.fromString("/snail/item").getChild(id);
+ }
+}
diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java
new file mode 100644
index 000000000..407ca84c4
--- /dev/null
+++ b/src/com/android/gallery3d/data/TagClustering.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class TagClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "TagClustering";
+
+ private ArrayList<ArrayList<Path>> mClusters;
+ private String[] mNames;
+ private String mUntaggedString;
+
+ public TagClustering(Context context) {
+ mUntaggedString = context.getResources().getString(R.string.untagged);
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final TreeMap<String, ArrayList<Path>> map =
+ new TreeMap<String, ArrayList<Path>>();
+ final ArrayList<Path> untagged = new ArrayList<Path>();
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ Path path = item.getPath();
+
+ String[] tags = item.getTags();
+ if (tags == null || tags.length == 0) {
+ untagged.add(path);
+ return;
+ }
+ for (int j = 0; j < tags.length; j++) {
+ String key = tags[j];
+ ArrayList<Path> list = map.get(key);
+ if (list == null) {
+ list = new ArrayList<Path>();
+ map.put(key, list);
+ }
+ list.add(path);
+ }
+ }
+ });
+
+ int m = map.size();
+ mClusters = new ArrayList<ArrayList<Path>>();
+ mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)];
+ int i = 0;
+ for (Map.Entry<String, ArrayList<Path>> entry : map.entrySet()) {
+ mNames[i++] = entry.getKey();
+ mClusters.add(entry.getValue());
+ }
+ if (untagged.size() > 0) {
+ mNames[i++] = mUntaggedString;
+ mClusters.add(untagged);
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.size();
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters.get(index);
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+}
diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java
new file mode 100644
index 000000000..35cbab1ee
--- /dev/null
+++ b/src/com/android/gallery3d/data/TimeClustering.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class TimeClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "TimeClustering";
+
+ // If 2 items are greater than 25 miles apart, they will be in different
+ // clusters.
+ private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20;
+
+ // Do not want to split based on anything under 1 min.
+ private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
+
+ // Disregard a cluster split time of anything over 2 hours.
+ private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
+
+ // Try and get around 9 clusters (best-effort for the common case).
+ private static final int NUM_CLUSTERS_TARGETED = 9;
+
+ // Try and merge 2 clusters if they are both smaller than min cluster size.
+ // The min cluster size can range from 8 to 15.
+ private static final int MIN_MIN_CLUSTER_SIZE = 8;
+ private static final int MAX_MIN_CLUSTER_SIZE = 15;
+
+ // Try and split a cluster if it is bigger than max cluster size.
+ // The max cluster size can range from 20 to 50.
+ private static final int MIN_MAX_CLUSTER_SIZE = 20;
+ private static final int MAX_MAX_CLUSTER_SIZE = 50;
+
+ // Initially put 2 items in the same cluster as long as they are within
+ // 3 cluster frequencies of each other.
+ private static int CLUSTER_SPLIT_MULTIPLIER = 3;
+
+ // The minimum change factor in the time between items to consider a
+ // partition.
+ // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
+ private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
+
+ // Make the cluster split time of a large cluster half that of a regular
+ // cluster.
+ private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
+
+ private Context mContext;
+ private ArrayList<Cluster> mClusters;
+ private String[] mNames;
+ private Cluster mCurrCluster;
+
+ private long mClusterSplitTime =
+ (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
+ private long mLargeClusterSplitTime =
+ mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+ private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
+ private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
+
+
+ private static final Comparator<SmallItem> sDateComparator =
+ new DateComparator();
+
+ private static class DateComparator implements Comparator<SmallItem> {
+ @Override
+ public int compare(SmallItem item1, SmallItem item2) {
+ return -Utils.compare(item1.dateInMs, item2.dateInMs);
+ }
+ }
+
+ public TimeClustering(Context context) {
+ mContext = context;
+ mClusters = new ArrayList<Cluster>();
+ mCurrCluster = new Cluster();
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final int total = baseSet.getTotalMediaItemCount();
+ final SmallItem[] buf = new SmallItem[total];
+ final double[] latLng = new double[2];
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if (index < 0 || index >= total) return;
+ SmallItem s = new SmallItem();
+ s.path = item.getPath();
+ s.dateInMs = item.getDateInMs();
+ item.getLatLong(latLng);
+ s.lat = latLng[0];
+ s.lng = latLng[1];
+ buf[index] = s;
+ }
+ });
+
+ ArrayList<SmallItem> items = new ArrayList<SmallItem>(total);
+ for (int i = 0; i < total; i++) {
+ if (buf[i] != null) {
+ items.add(buf[i]);
+ }
+ }
+
+ Collections.sort(items, sDateComparator);
+
+ int n = items.size();
+ long minTime = 0;
+ long maxTime = 0;
+ for (int i = 0; i < n; i++) {
+ long t = items.get(i).dateInMs;
+ if (t == 0) continue;
+ if (minTime == 0) {
+ minTime = maxTime = t;
+ } else {
+ minTime = Math.min(minTime, t);
+ maxTime = Math.max(maxTime, t);
+ }
+ }
+
+ setTimeRange(maxTime - minTime, n);
+
+ for (int i = 0; i < n; i++) {
+ compute(items.get(i));
+ }
+
+ compute(null);
+
+ int m = mClusters.size();
+ mNames = new String[m];
+ for (int i = 0; i < m; i++) {
+ mNames[i] = mClusters.get(i).generateCaption(mContext);
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.size();
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ ArrayList<SmallItem> items = mClusters.get(index).getItems();
+ ArrayList<Path> result = new ArrayList<Path>(items.size());
+ for (int i = 0, n = items.size(); i < n; i++) {
+ result.add(items.get(i).path);
+ }
+ return result;
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+
+ private void setTimeRange(long timeRange, int numItems) {
+ if (numItems != 0) {
+ int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
+ // Heuristic to get min and max cluster size - half and double the
+ // desired items per cluster.
+ mMinClusterSize = meanItemsPerCluster / 2;
+ mMaxClusterSize = meanItemsPerCluster * 2;
+ mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
+ }
+ mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
+ mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+ mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
+ mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
+ }
+
+ private void compute(SmallItem currentItem) {
+ if (currentItem != null) {
+ int numClusters = mClusters.size();
+ int numCurrClusterItems = mCurrCluster.size();
+ boolean geographicallySeparateItem = false;
+ boolean itemAddedToCurrentCluster = false;
+
+ // Determine if this item should go in the current cluster or be the
+ // start of a new cluster.
+ if (numCurrClusterItems == 0) {
+ mCurrCluster.addItem(currentItem);
+ } else {
+ SmallItem prevItem = mCurrCluster.getLastItem();
+ if (isGeographicallySeparated(prevItem, currentItem)) {
+ mClusters.add(mCurrCluster);
+ geographicallySeparateItem = true;
+ } else if (numCurrClusterItems > mMaxClusterSize) {
+ splitAndAddCurrentCluster();
+ } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
+ mCurrCluster.addItem(currentItem);
+ itemAddedToCurrentCluster = true;
+ } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+ && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+ mergeAndAddCurrentCluster();
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+
+ // Creating a new cluster and adding the current item to it.
+ if (!itemAddedToCurrentCluster) {
+ mCurrCluster = new Cluster();
+ if (geographicallySeparateItem) {
+ mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
+ }
+ mCurrCluster.addItem(currentItem);
+ }
+ }
+ } else {
+ if (mCurrCluster.size() > 0) {
+ int numClusters = mClusters.size();
+ int numCurrClusterItems = mCurrCluster.size();
+
+ // The last cluster may potentially be too big or too small.
+ if (numCurrClusterItems > mMaxClusterSize) {
+ splitAndAddCurrentCluster();
+ } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+ && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+ mergeAndAddCurrentCluster();
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ mCurrCluster = new Cluster();
+ }
+ }
+ }
+
+ private void splitAndAddCurrentCluster() {
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
+ if (secondPartitionStartIndex != -1) {
+ Cluster partitionedCluster = new Cluster();
+ for (int j = 0; j < secondPartitionStartIndex; j++) {
+ partitionedCluster.addItem(currClusterItems.get(j));
+ }
+ mClusters.add(partitionedCluster);
+ partitionedCluster = new Cluster();
+ for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
+ partitionedCluster.addItem(currClusterItems.get(j));
+ }
+ mClusters.add(partitionedCluster);
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ }
+
+ private int getPartitionIndexForCurrentCluster() {
+ int partitionIndex = -1;
+ float largestChange = MIN_PARTITION_CHANGE_FACTOR;
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ int minClusterSize = mMinClusterSize;
+
+ // Could be slightly more efficient here but this code seems cleaner.
+ if (numCurrClusterItems > minClusterSize + 1) {
+ for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
+ SmallItem prevItem = currClusterItems.get(i - 1);
+ SmallItem currItem = currClusterItems.get(i);
+ SmallItem nextItem = currClusterItems.get(i + 1);
+
+ long timeNext = nextItem.dateInMs;
+ long timeCurr = currItem.dateInMs;
+ long timePrev = prevItem.dateInMs;
+
+ if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue;
+
+ long diff1 = Math.abs(timeNext - timeCurr);
+ long diff2 = Math.abs(timeCurr - timePrev);
+
+ float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
+ if (change > largestChange) {
+ if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
+ partitionIndex = i;
+ largestChange = change;
+ } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
+ partitionIndex = i + 1;
+ largestChange = change;
+ }
+ }
+ }
+ }
+ return partitionIndex;
+ }
+
+ private void mergeAndAddCurrentCluster() {
+ int numClusters = mClusters.size();
+ Cluster prevCluster = mClusters.get(numClusters - 1);
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ if (prevCluster.size() < mMinClusterSize) {
+ for (int i = 0; i < numCurrClusterItems; i++) {
+ prevCluster.addItem(currClusterItems.get(i));
+ }
+ mClusters.set(numClusters - 1, prevCluster);
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ }
+
+ // Returns true if a, b are sufficiently geographically separated.
+ private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) {
+ if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng)
+ || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) {
+ return false;
+ }
+
+ double distance = GalleryUtils.fastDistanceMeters(
+ Math.toRadians(itemA.lat),
+ Math.toRadians(itemA.lng),
+ Math.toRadians(itemB.lat),
+ Math.toRadians(itemB.lng));
+ return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES);
+ }
+
+ // Returns the time interval between the two items in milliseconds.
+ private static long timeDistance(SmallItem a, SmallItem b) {
+ return Math.abs(a.dateInMs - b.dateInMs);
+ }
+}
+
+class SmallItem {
+ Path path;
+ long dateInMs;
+ double lat, lng;
+}
+
+class Cluster {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Cluster";
+ private static final String MMDDYY_FORMAT = "MMddyy";
+
+ // This is for TimeClustering only.
+ public boolean mGeographicallySeparatedFromPrevCluster = false;
+
+ private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>();
+
+ public Cluster() {
+ }
+
+ public void addItem(SmallItem item) {
+ mItems.add(item);
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+
+ public SmallItem getLastItem() {
+ int n = mItems.size();
+ return (n == 0) ? null : mItems.get(n - 1);
+ }
+
+ public ArrayList<SmallItem> getItems() {
+ return mItems;
+ }
+
+ public String generateCaption(Context context) {
+ int n = mItems.size();
+ long minTimestamp = 0;
+ long maxTimestamp = 0;
+
+ for (int i = 0; i < n; i++) {
+ long t = mItems.get(i).dateInMs;
+ if (t == 0) continue;
+ if (minTimestamp == 0) {
+ minTimestamp = maxTimestamp = t;
+ } else {
+ minTimestamp = Math.min(minTimestamp, t);
+ maxTimestamp = Math.max(maxTimestamp, t);
+ }
+ }
+ if (minTimestamp == 0) return "";
+
+ String caption;
+ String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp)
+ .toString();
+ String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp)
+ .toString();
+
+ if (minDay.substring(4).equals(maxDay.substring(4))) {
+ // The items are from the same year - show at least as
+ // much granularity as abbrev_all allows.
+ caption = DateUtils.formatDateRange(context, minTimestamp,
+ maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
+
+ // Get a more granular date range string if the min and
+ // max timestamp are on the same day and from the
+ // current year.
+ if (minDay.equals(maxDay)) {
+ int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+ // Contains the year only if the date does not
+ // correspond to the current year.
+ String dateRangeWithOptionalYear = DateUtils.formatDateTime(
+ context, minTimestamp, flags);
+ String dateRangeWithYear = DateUtils.formatDateTime(
+ context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
+ if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
+ // This means both dates are from the same year
+ // - show the time.
+ // Not enough room to display the time range.
+ // Pick the mid-point.
+ long midTimestamp = (minTimestamp + maxTimestamp) / 2;
+ caption = DateUtils.formatDateRange(context, midTimestamp,
+ midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
+ }
+ }
+ } else {
+ // The items are not from the same year - only show
+ // month and year.
+ int flags = DateUtils.FORMAT_NO_MONTH_DAY
+ | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+ caption = DateUtils.formatDateRange(context, minTimestamp,
+ maxTimestamp, flags);
+ }
+
+ return caption;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UnlockImage.java b/src/com/android/gallery3d/data/UnlockImage.java
new file mode 100644
index 000000000..ed3b485c4
--- /dev/null
+++ b/src/com/android/gallery3d/data/UnlockImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class UnlockImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UnlockImage";
+
+ public UnlockImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_locked);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_UNLOCK;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
new file mode 100644
index 000000000..e8875b572
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+
+public class UriImage extends MediaItem {
+ private static final String TAG = "UriImage";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_DOWNLOADING = 1;
+ private static final int STATE_DOWNLOADED = 2;
+ private static final int STATE_ERROR = -1;
+
+ private final Uri mUri;
+ private final String mContentType;
+
+ private DownloadCache.Entry mCacheEntry;
+ private ParcelFileDescriptor mFileDescriptor;
+ private int mState = STATE_INIT;
+ private int mWidth;
+ private int mHeight;
+ private int mRotation;
+ private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+ private GalleryApp mApplication;
+
+ public UriImage(GalleryApp application, Path path, Uri uri, String contentType) {
+ super(path, nextVersionNumber());
+ mUri = uri;
+ mApplication = Utils.checkNotNull(application);
+ mContentType = contentType;
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new BitmapJob(type);
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new RegionDecoderJob();
+ }
+
+ private void openFileOrDownloadTempFile(JobContext jc) {
+ int state = openOrDownloadInner(jc);
+ synchronized (this) {
+ mState = state;
+ if (mState != STATE_DOWNLOADED) {
+ if (mFileDescriptor != null) {
+ Utils.closeSilently(mFileDescriptor);
+ mFileDescriptor = null;
+ }
+ }
+ notifyAll();
+ }
+ }
+
+ private int openOrDownloadInner(JobContext jc) {
+ String scheme = mUri.getScheme();
+ if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ try {
+ if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+ InputStream is = mApplication.getContentResolver()
+ .openInputStream(mUri);
+ mRotation = Exif.getOrientation(is);
+ Utils.closeSilently(is);
+ }
+ mFileDescriptor = mApplication.getContentResolver()
+ .openFileDescriptor(mUri, "r");
+ if (jc.isCancelled()) return STATE_INIT;
+ return STATE_DOWNLOADED;
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "fail to open: " + mUri, e);
+ return STATE_ERROR;
+ }
+ } else {
+ try {
+ URL url = new URI(mUri.toString()).toURL();
+ mCacheEntry = mApplication.getDownloadCache().download(jc, url);
+ if (jc.isCancelled()) return STATE_INIT;
+ if (mCacheEntry == null) {
+ Log.w(TAG, "download failed " + url);
+ return STATE_ERROR;
+ }
+ if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+ InputStream is = new FileInputStream(mCacheEntry.cacheFile);
+ mRotation = Exif.getOrientation(is);
+ Utils.closeSilently(is);
+ }
+ mFileDescriptor = ParcelFileDescriptor.open(
+ mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ return STATE_DOWNLOADED;
+ } catch (Throwable t) {
+ Log.w(TAG, "download error", t);
+ return STATE_ERROR;
+ }
+ }
+ }
+
+ private boolean prepareInputFile(JobContext jc) {
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ });
+
+ while (true) {
+ synchronized (this) {
+ if (jc.isCancelled()) return false;
+ if (mState == STATE_INIT) {
+ mState = STATE_DOWNLOADING;
+ // Then leave the synchronized block and continue.
+ } else if (mState == STATE_ERROR) {
+ return false;
+ } else if (mState == STATE_DOWNLOADED) {
+ return true;
+ } else /* if (mState == STATE_DOWNLOADING) */ {
+ try {
+ wait();
+ } catch (InterruptedException ex) {
+ // ignored.
+ }
+ continue;
+ }
+ }
+ // This is only reached for STATE_INIT->STATE_DOWNLOADING
+ openFileOrDownloadTempFile(jc);
+ }
+ }
+
+ private class RegionDecoderJob implements Job<BitmapRegionDecoder> {
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
+ jc, mFileDescriptor.getFileDescriptor(), false);
+ mWidth = decoder.getWidth();
+ mHeight = decoder.getHeight();
+ return decoder;
+ }
+ }
+
+ private class BitmapJob implements Job<Bitmap> {
+ private int mType;
+
+ protected BitmapJob(int type) {
+ mType = type;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ int targetSize = MediaItem.getTargetSize(mType);
+ Options options = new Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
+ mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
+
+ if (jc.isCancelled() || bitmap == null) {
+ return null;
+ }
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
+ }
+ return bitmap;
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ int supported = SUPPORT_EDIT | SUPPORT_SETAS;
+ if (isSharable()) supported |= SUPPORT_SHARE;
+ if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
+ supported |= SUPPORT_FULL_IMAGE;
+ }
+ return supported;
+ }
+
+ @Override
+ public void getPanoramaSupport(PanoramaSupportCallback callback) {
+ mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+ }
+
+ @Override
+ public void clearCachedPanoramaSupport() {
+ mPanoramaMetadata.clearCachedValues();
+ }
+
+ private boolean isSharable() {
+ // We cannot grant read permission to the receiver since we put
+ // the data URI in EXTRA_STREAM instead of the data part of an intent
+ // And there are issues in MediaUploader and Bluetooth file sender to
+ // share a general image data. So, we only share for local file.
+ return ContentResolver.SCHEME_FILE.equals(mUri.getScheme());
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_IMAGE;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ return mUri;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ if (mWidth != 0 && mHeight != 0) {
+ details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
+ details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
+ }
+ if (mContentType != null) {
+ details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
+ }
+ if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
+ String filePath = mUri.getPath();
+ details.addDetail(MediaDetails.INDEX_PATH, filePath);
+ MediaDetails.extractExifInfo(details, filePath);
+ }
+ return details;
+ }
+
+ @Override
+ public String getMimeType() {
+ return mContentType;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mFileDescriptor != null) {
+ Utils.closeSilently(mFileDescriptor);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java
new file mode 100644
index 000000000..f66bacd7b
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriSource.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+class UriSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UriSource";
+ private static final String IMAGE_TYPE_PREFIX = "image/";
+ private static final String IMAGE_TYPE_ANY = "image/*";
+ private static final String CHARSET_UTF_8 = "utf-8";
+
+ private GalleryApp mApplication;
+
+ public UriSource(GalleryApp context) {
+ super("uri");
+ mApplication = context;
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ String segment[] = path.split();
+ if (segment.length != 3) {
+ throw new RuntimeException("bad path: " + path);
+ }
+ try {
+ String uri = URLDecoder.decode(segment[1], CHARSET_UTF_8);
+ String type = URLDecoder.decode(segment[2], CHARSET_UTF_8);
+ return new UriImage(mApplication, path, Uri.parse(uri), type);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private String getMimeType(Uri uri) {
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ String extension =
+ MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String type = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(extension.toLowerCase());
+ if (type != null) return type;
+ }
+ // Assume the type is image if the type cannot be resolved
+ // This could happen for "http" URI.
+ String type = mApplication.getContentResolver().getType(uri);
+ if (type == null) type = "image/*";
+ return type;
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri, String type) {
+ String mimeType = getMimeType(uri);
+
+ // Try to find a most specific type but it has to be started with "image/"
+ if ((type == null) || (IMAGE_TYPE_ANY.equals(type)
+ && mimeType.startsWith(IMAGE_TYPE_PREFIX))) {
+ type = mimeType;
+ }
+
+ if (type.startsWith(IMAGE_TYPE_PREFIX)) {
+ try {
+ return Path.fromString("/uri/"
+ + URLEncoder.encode(uri.toString(), CHARSET_UTF_8)
+ + "/" +URLEncoder.encode(type, CHARSET_UTF_8));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+ // We have no clues that it is an image
+ return null;
+ }
+}