summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOwen Lin <owenlin@google.com>2011-08-17 22:07:43 +0800
committerOwen Lin <owenlin@google.com>2011-08-18 13:33:50 +0800
commita2fba687d4d2dbb3b2db8866b054ecb0e42871b2 (patch)
treedacc5a60ed945fe989aebf1f227f72bc90ebc4b8
parenta053a3179cfee3d2bb666eff5f4f03a96b092e04 (diff)
downloadandroid_packages_apps_Snap-a2fba687d4d2dbb3b2db8866b054ecb0e42871b2.tar.gz
android_packages_apps_Snap-a2fba687d4d2dbb3b2db8866b054ecb0e42871b2.tar.bz2
android_packages_apps_Snap-a2fba687d4d2dbb3b2db8866b054ecb0e42871b2.zip
Initial code for Gallery2.
fix: 5176434 Change-Id: I041e282b9c7b34ceb1db8b033be2b853bb3a992c
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java285
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/BlobCache.java653
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/Entry.java56
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/EntrySchema.java529
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/FileCache.java303
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/Fingerprint.java191
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java134
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/LruCache.java90
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/Utils.java407
-rw-r--r--src/com/android/gallery3d/anim/AlphaAnimation.java48
-rw-r--r--src/com/android/gallery3d/anim/Animation.java92
-rw-r--r--src/com/android/gallery3d/anim/AnimationSet.java76
-rw-r--r--src/com/android/gallery3d/anim/CanvasAnimation.java25
-rw-r--r--src/com/android/gallery3d/anim/FloatAnimation.java40
-rw-r--r--src/com/android/gallery3d/app/AbstractGalleryActivity.java198
-rw-r--r--src/com/android/gallery3d/app/ActivityState.java136
-rw-r--r--src/com/android/gallery3d/app/AlbumDataAdapter.java367
-rw-r--r--src/com/android/gallery3d/app/AlbumPage.java602
-rw-r--r--src/com/android/gallery3d/app/AlbumPicker.java68
-rw-r--r--src/com/android/gallery3d/app/AlbumSetDataAdapter.java384
-rw-r--r--src/com/android/gallery3d/app/AlbumSetPage.java586
-rw-r--r--src/com/android/gallery3d/app/Config.java140
-rw-r--r--src/com/android/gallery3d/app/CropImage.java850
-rw-r--r--src/com/android/gallery3d/app/DialogPicker.java68
-rw-r--r--src/com/android/gallery3d/app/EyePosition.java218
-rw-r--r--src/com/android/gallery3d/app/FilterUtils.java296
-rw-r--r--src/com/android/gallery3d/app/Gallery.java232
-rw-r--r--src/com/android/gallery3d/app/GalleryActionBar.java218
-rw-r--r--src/com/android/gallery3d/app/GalleryActivity.java28
-rw-r--r--src/com/android/gallery3d/app/GalleryApp.java39
-rw-r--r--src/com/android/gallery3d/app/GalleryAppImpl.java90
-rw-r--r--src/com/android/gallery3d/app/GalleryContext.java38
-rw-r--r--src/com/android/gallery3d/app/LoadingListener.java22
-rw-r--r--src/com/android/gallery3d/app/Log.java53
-rw-r--r--src/com/android/gallery3d/app/ManageCachePage.java271
-rw-r--r--src/com/android/gallery3d/app/MovieActivity.java129
-rw-r--r--src/com/android/gallery3d/app/MoviePlayer.java291
-rw-r--r--src/com/android/gallery3d/app/PackagesMonitor.java50
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java794
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java581
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoDataAdapter.java181
-rw-r--r--src/com/android/gallery3d/app/SlideshowDataAdapter.java187
-rw-r--r--src/com/android/gallery3d/app/SlideshowDream.java28
-rw-r--r--src/com/android/gallery3d/app/SlideshowPage.java338
-rw-r--r--src/com/android/gallery3d/app/StateManager.java277
-rw-r--r--src/com/android/gallery3d/app/UsbDeviceActivity.java47
-rw-r--r--src/com/android/gallery3d/app/Wallpaper.java116
-rw-r--r--src/com/android/gallery3d/data/ChangeNotifier.java54
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbum.java129
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbumSet.java152
-rw-r--r--src/com/android/gallery3d/data/ClusterSource.java86
-rw-r--r--src/com/android/gallery3d/data/Clustering.java26
-rw-r--r--src/com/android/gallery3d/data/ComboAlbum.java87
-rw-r--r--src/com/android/gallery3d/data/ComboAlbumSet.java80
-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.java333
-rw-r--r--src/com/android/gallery3d/data/DecodeUtils.java173
-rw-r--r--src/com/android/gallery3d/data/DownloadCache.java398
-rw-r--r--src/com/android/gallery3d/data/DownloadEntry.java72
-rw-r--r--src/com/android/gallery3d/data/DownloadUtils.java95
-rw-r--r--src/com/android/gallery3d/data/Face.java56
-rw-r--r--src/com/android/gallery3d/data/FaceClustering.java94
-rw-r--r--src/com/android/gallery3d/data/FilterSet.java137
-rw-r--r--src/com/android/gallery3d/data/FilterSource.java52
-rw-r--r--src/com/android/gallery3d/data/ImageCacheRequest.java89
-rw-r--r--src/com/android/gallery3d/data/ImageCacheService.java105
-rw-r--r--src/com/android/gallery3d/data/LocalAlbum.java252
-rw-r--r--src/com/android/gallery3d/data/LocalAlbumSet.java263
-rw-r--r--src/com/android/gallery3d/data/LocalImage.java289
-rw-r--r--src/com/android/gallery3d/data/LocalMediaItem.java103
-rw-r--r--src/com/android/gallery3d/data/LocalMergeAlbum.java226
-rw-r--r--src/com/android/gallery3d/data/LocalSource.java272
-rw-r--r--src/com/android/gallery3d/data/LocalVideo.java213
-rw-r--r--src/com/android/gallery3d/data/LocationClustering.java304
-rw-r--r--src/com/android/gallery3d/data/Log.java53
-rw-r--r--src/com/android/gallery3d/data/MediaDetails.java162
-rw-r--r--src/com/android/gallery3d/data/MediaItem.java75
-rw-r--r--src/com/android/gallery3d/data/MediaObject.java130
-rw-r--r--src/com/android/gallery3d/data/MediaSet.java219
-rw-r--r--src/com/android/gallery3d/data/MediaSource.java93
-rw-r--r--src/com/android/gallery3d/data/MtpClient.java442
-rw-r--r--src/com/android/gallery3d/data/MtpContext.java141
-rw-r--r--src/com/android/gallery3d/data/MtpDevice.java174
-rw-r--r--src/com/android/gallery3d/data/MtpDeviceSet.java109
-rw-r--r--src/com/android/gallery3d/data/MtpImage.java166
-rw-r--r--src/com/android/gallery3d/data/MtpSource.java71
-rw-r--r--src/com/android/gallery3d/data/Path.java237
-rw-r--r--src/com/android/gallery3d/data/PathMatcher.java102
-rw-r--r--src/com/android/gallery3d/data/SizeClustering.java138
-rw-r--r--src/com/android/gallery3d/data/TagClustering.java94
-rw-r--r--src/com/android/gallery3d/data/TimeClustering.java436
-rw-r--r--src/com/android/gallery3d/data/UriImage.java266
-rw-r--r--src/com/android/gallery3d/data/UriSource.java58
-rw-r--r--src/com/android/gallery3d/provider/GalleryProvider.java224
-rw-r--r--src/com/android/gallery3d/ui/AbstractDisplayItem.java114
-rw-r--r--src/com/android/gallery3d/ui/ActionModeHandler.java246
-rw-r--r--src/com/android/gallery3d/ui/AdaptiveBackground.java128
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java543
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetView.java240
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlidingWindow.java433
-rw-r--r--src/com/android/gallery3d/ui/AlbumView.java197
-rw-r--r--src/com/android/gallery3d/ui/BasicTexture.java164
-rw-r--r--src/com/android/gallery3d/ui/BitmapTexture.java49
-rw-r--r--src/com/android/gallery3d/ui/BitmapTileProvider.java91
-rw-r--r--src/com/android/gallery3d/ui/BoxBlurFilter.java100
-rw-r--r--src/com/android/gallery3d/ui/CacheBarView.java270
-rw-r--r--src/com/android/gallery3d/ui/CanvasTexture.java52
-rw-r--r--src/com/android/gallery3d/ui/ColorTexture.java58
-rw-r--r--src/com/android/gallery3d/ui/Config.java31
-rw-r--r--src/com/android/gallery3d/ui/CropView.java801
-rw-r--r--src/com/android/gallery3d/ui/CustomMenu.java126
-rw-r--r--src/com/android/gallery3d/ui/DetailsWindow.java451
-rw-r--r--src/com/android/gallery3d/ui/DisplayItem.java45
-rw-r--r--src/com/android/gallery3d/ui/DownUpDetector.java61
-rw-r--r--src/com/android/gallery3d/ui/DrawableTexture.java38
-rw-r--r--src/com/android/gallery3d/ui/FilmStripView.java261
-rw-r--r--src/com/android/gallery3d/ui/GLCanvas.java138
-rw-r--r--src/com/android/gallery3d/ui/GLCanvasImpl.java913
-rw-r--r--src/com/android/gallery3d/ui/GLPaint.java65
-rw-r--r--src/com/android/gallery3d/ui/GLRoot.java37
-rw-r--r--src/com/android/gallery3d/ui/GLRootView.java414
-rw-r--r--src/com/android/gallery3d/ui/GLView.java431
-rw-r--r--src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java126
-rw-r--r--src/com/android/gallery3d/ui/GridDrawer.java109
-rw-r--r--src/com/android/gallery3d/ui/HighlightDrawer.java73
-rw-r--r--src/com/android/gallery3d/ui/Icon.java59
-rw-r--r--src/com/android/gallery3d/ui/IconDrawer.java112
-rw-r--r--src/com/android/gallery3d/ui/ImportCompleteListener.java57
-rw-r--r--src/com/android/gallery3d/ui/Label.java84
-rw-r--r--src/com/android/gallery3d/ui/Log.java53
-rw-r--r--src/com/android/gallery3d/ui/ManageCacheDrawer.java126
-rw-r--r--src/com/android/gallery3d/ui/MeasureHelper.java65
-rw-r--r--src/com/android/gallery3d/ui/MenuExecutor.java398
-rw-r--r--src/com/android/gallery3d/ui/MultiLineTexture.java50
-rw-r--r--src/com/android/gallery3d/ui/NinePatchChunk.java82
-rw-r--r--src/com/android/gallery3d/ui/NinePatchTexture.java401
-rw-r--r--src/com/android/gallery3d/ui/OnSelectedListener.java21
-rw-r--r--src/com/android/gallery3d/ui/Paper.java112
-rw-r--r--src/com/android/gallery3d/ui/PhotoView.java1191
-rw-r--r--src/com/android/gallery3d/ui/PositionProvider.java23
-rw-r--r--src/com/android/gallery3d/ui/PositionRepository.java139
-rw-r--r--src/com/android/gallery3d/ui/ProgressBar.java65
-rw-r--r--src/com/android/gallery3d/ui/ProgressSpinner.java78
-rw-r--r--src/com/android/gallery3d/ui/RawTexture.java54
-rw-r--r--src/com/android/gallery3d/ui/ResourceTexture.java52
-rw-r--r--src/com/android/gallery3d/ui/ScrollBarView.java135
-rw-r--r--src/com/android/gallery3d/ui/ScrollView.java99
-rw-r--r--src/com/android/gallery3d/ui/ScrollerHelper.java93
-rw-r--r--src/com/android/gallery3d/ui/SelectionDrawer.java89
-rw-r--r--src/com/android/gallery3d/ui/SelectionManager.java221
-rw-r--r--src/com/android/gallery3d/ui/SlideshowView.java165
-rw-r--r--src/com/android/gallery3d/ui/SlotView.java607
-rw-r--r--src/com/android/gallery3d/ui/StaticBackground.java62
-rw-r--r--src/com/android/gallery3d/ui/StringTexture.java92
-rw-r--r--src/com/android/gallery3d/ui/StripDrawer.java57
-rw-r--r--src/com/android/gallery3d/ui/SynchronizedHandler.java41
-rw-r--r--src/com/android/gallery3d/ui/TextButton.java91
-rw-r--r--src/com/android/gallery3d/ui/Texture.java44
-rw-r--r--src/com/android/gallery3d/ui/TileImageView.java693
-rw-r--r--src/com/android/gallery3d/ui/TileImageViewAdapter.java144
-rw-r--r--src/com/android/gallery3d/ui/UploadedTexture.java285
-rw-r--r--src/com/android/gallery3d/ui/UserInteractionListener.java26
-rw-r--r--src/com/android/gallery3d/util/CacheManager.java82
-rw-r--r--src/com/android/gallery3d/util/Future.java35
-rw-r--r--src/com/android/gallery3d/util/FutureListener.java21
-rw-r--r--src/com/android/gallery3d/util/FutureTask.java86
-rw-r--r--src/com/android/gallery3d/util/GalleryUtils.java327
-rw-r--r--src/com/android/gallery3d/util/IdentityCache.java74
-rw-r--r--src/com/android/gallery3d/util/IntArray.java54
-rw-r--r--src/com/android/gallery3d/util/InterruptableOutputStream.java67
-rw-r--r--src/com/android/gallery3d/util/LinkedNode.java75
-rw-r--r--src/com/android/gallery3d/util/Log.java53
-rw-r--r--src/com/android/gallery3d/util/MediaSetUtils.java56
-rw-r--r--src/com/android/gallery3d/util/PriorityThreadFactory.java48
-rw-r--r--src/com/android/gallery3d/util/ReverseGeocoder.java417
-rw-r--r--src/com/android/gallery3d/util/ThreadPool.java252
-rw-r--r--src/com/android/gallery3d/util/UpdateHelper.java67
-rw-r--r--src/com/android/gallery3d/widget/LocalPhotoSource.java202
-rw-r--r--src/com/android/gallery3d/widget/MediaSetSource.java113
-rw-r--r--src/com/android/gallery3d/widget/WidgetClickHandler.java59
-rw-r--r--src/com/android/gallery3d/widget/WidgetConfigure.java167
-rw-r--r--src/com/android/gallery3d/widget/WidgetDatabaseHelper.java187
-rw-r--r--src/com/android/gallery3d/widget/WidgetProvider.java109
-rw-r--r--src/com/android/gallery3d/widget/WidgetService.java169
-rw-r--r--src/com/android/gallery3d/widget/WidgetSource.java31
-rw-r--r--src/com/android/gallery3d/widget/WidgetTypeChooser.java59
-rw-r--r--src/com/android/gallery3d/widget/WidgetUtils.java80
-rw-r--r--src_pd/com/android/gallery3d/picasasource/PicasaSource.java125
-rw-r--r--src_pd/com/android/gallery3d/settings/GallerySettings.java23
-rw-r--r--tests/src/com/android/gallery3d/anim/AnimationTest.java80
-rw-r--r--tests/src/com/android/gallery3d/common/BlobCacheTest.java738
-rw-r--r--tests/src/com/android/gallery3d/common/UtilsTest.java244
-rw-r--r--tests/src/com/android/gallery3d/data/GalleryAppMock.java50
-rw-r--r--tests/src/com/android/gallery3d/data/GalleryAppStub.java47
-rw-r--r--tests/src/com/android/gallery3d/data/LocalDataTest.java461
-rw-r--r--tests/src/com/android/gallery3d/data/MediaSetTest.java63
-rw-r--r--tests/src/com/android/gallery3d/data/MockItem.java43
-rw-r--r--tests/src/com/android/gallery3d/data/MockSet.java88
-rw-r--r--tests/src/com/android/gallery3d/data/MockSource.java48
-rw-r--r--tests/src/com/android/gallery3d/data/PathTest.java82
-rw-r--r--tests/src/com/android/gallery3d/data/RealDataTest.java110
-rw-r--r--tests/src/com/android/gallery3d/ui/GLCanvasMock.java68
-rw-r--r--tests/src/com/android/gallery3d/ui/GLCanvasStub.java79
-rw-r--r--tests/src/com/android/gallery3d/ui/GLCanvasTest.java778
-rw-r--r--tests/src/com/android/gallery3d/ui/GLMock.java195
-rw-r--r--tests/src/com/android/gallery3d/ui/GLRootMock.java37
-rw-r--r--tests/src/com/android/gallery3d/ui/GLRootStub.java30
-rw-r--r--tests/src/com/android/gallery3d/ui/GLStub.java1490
-rw-r--r--tests/src/com/android/gallery3d/ui/GLViewMock.java85
-rw-r--r--tests/src/com/android/gallery3d/ui/GLViewTest.java424
-rw-r--r--tests/src/com/android/gallery3d/ui/PointerInfo.java222
-rw-r--r--tests/src/com/android/gallery3d/ui/TextureTest.java208
-rw-r--r--tests/src/com/android/gallery3d/util/IntArrayTest.java60
214 files changed, 40197 insertions, 0 deletions
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
new file mode 100644
index 000000000..04cdc6142
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -0,0 +1,285 @@
+/*
+ * 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.common;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.os.Build;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class BitmapUtils {
+ private static final String TAG = "BitmapUtils";
+ public static final int UNCONSTRAINED = -1;
+ private static final int COMPRESS_JPEG_QUALITY = 90;
+
+ private BitmapUtils(){}
+
+ /*
+ * Compute the sample size as a function of minSideLength
+ * and maxNumOfPixels.
+ * minSideLength is used to specify that minimal width or height of a
+ * bitmap.
+ * maxNumOfPixels is used to specify the maximal size in pixels that is
+ * tolerable in terms of memory usage.
+ *
+ * The function returns a sample size based on the constraints.
+ * Both size and minSideLength can be passed in as UNCONSTRAINED,
+ * which indicates no care of the corresponding constraint.
+ * The functions prefers returning a sample size that
+ * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
+ *
+ * Also, the function rounds up the sample size to a power of 2 or multiple
+ * of 8 because BitmapFactory only honors sample size this way.
+ * For example, BitmapFactory downsamples an image by 2 even though the
+ * request is 3. So we round up the sample size to avoid OOM.
+ */
+ public static int computeSampleSize(int width, int height,
+ int minSideLength, int maxNumOfPixels) {
+ int initialSize = computeInitialSampleSize(
+ width, height, minSideLength, maxNumOfPixels);
+
+ return initialSize <= 8
+ ? Utils.nextPowerOf2(initialSize)
+ : (initialSize + 7) / 8 * 8;
+ }
+
+ private static int computeInitialSampleSize(int w, int h,
+ int minSideLength, int maxNumOfPixels) {
+ if (maxNumOfPixels == UNCONSTRAINED
+ && minSideLength == UNCONSTRAINED) return 1;
+
+ int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
+ (int) Math.ceil(Math.sqrt((double) (w * h) / maxNumOfPixels));
+
+ if (minSideLength == UNCONSTRAINED) {
+ return lowerBound;
+ } else {
+ int sampleSize = Math.min(w / minSideLength, h / minSideLength);
+ return Math.max(sampleSize, lowerBound);
+ }
+ }
+
+ // This computes a sample size which makes the longer side at least
+ // minSideLength long. If that's not possible, return 1.
+ public static int computeSampleSizeLarger(int w, int h,
+ int minSideLength) {
+ int initialSize = Math.min(w / minSideLength, h / minSideLength);
+ if (initialSize <= 1) return 1;
+
+ return initialSize <= 8
+ ? Utils.prevPowerOf2(initialSize)
+ : initialSize / 8 * 8;
+ }
+
+ // Fin the min x that 1 / x <= scale
+ public static int computeSampleSizeLarger(float scale) {
+ int initialSize = (int) Math.floor(1f / scale);
+ if (initialSize <= 1) return 1;
+
+ return initialSize <= 8
+ ? Utils.prevPowerOf2(initialSize)
+ : initialSize / 8 * 8;
+ }
+
+ // Find the max x that 1 / x >= scale.
+ public static int computeSampleSize(float scale) {
+ Utils.assertTrue(scale > 0);
+ int initialSize = Math.max(1, (int) Math.ceil(1 / scale));
+ return initialSize <= 8
+ ? Utils.nextPowerOf2(initialSize)
+ : (initialSize + 7) / 8 * 8;
+ }
+
+ public static Bitmap resizeDownToPixels(
+ Bitmap bitmap, int targetPixels, boolean recycle) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ float scale = (float) Math.sqrt(
+ (double) targetPixels / (width * height));
+ if (scale >= 1.0f) return bitmap;
+ return resizeBitmapByScale(bitmap, scale, recycle);
+ }
+
+ public static Bitmap resizeBitmapByScale(
+ Bitmap bitmap, float scale, boolean recycle) {
+ int width = Math.round(bitmap.getWidth() * scale);
+ int height = Math.round(bitmap.getHeight() * scale);
+ if (width == bitmap.getWidth()
+ && height == bitmap.getHeight()) return bitmap;
+ Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
+ Canvas canvas = new Canvas(target);
+ canvas.scale(scale, scale);
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+ if (recycle) bitmap.recycle();
+ return target;
+ }
+
+ private static Bitmap.Config getConfig(Bitmap bitmap) {
+ Bitmap.Config config = bitmap.getConfig();
+ if (config == null) {
+ config = Bitmap.Config.ARGB_8888;
+ }
+ return config;
+ }
+
+ public static Bitmap resizeDownBySideLength(
+ Bitmap bitmap, int maxLength, boolean recycle) {
+ int srcWidth = bitmap.getWidth();
+ int srcHeight = bitmap.getHeight();
+ float scale = Math.min(
+ (float) maxLength / srcWidth, (float) maxLength / srcHeight);
+ if (scale >= 1.0f) return bitmap;
+ return resizeBitmapByScale(bitmap, scale, recycle);
+ }
+
+ // Crops a square from the center of the original image.
+ public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ if (width == height) return bitmap;
+ int size = Math.min(width, height);
+
+ Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
+ Canvas canvas = new Canvas(target);
+ canvas.translate((size - width) / 2, (size - height) / 2);
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+ if (recycle) bitmap.recycle();
+ return target;
+ }
+
+ public static Bitmap resizeDownAndCropCenter(Bitmap bitmap, int size,
+ boolean recycle) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ int minSide = Math.min(w, h);
+ if (w == h && minSide <= size) return bitmap;
+ size = Math.min(size, minSide);
+
+ float scale = Math.max((float) size / bitmap.getWidth(),
+ (float) size / bitmap.getHeight());
+ Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
+ int width = Math.round(scale * bitmap.getWidth());
+ int height = Math.round(scale * bitmap.getHeight());
+ Canvas canvas = new Canvas(target);
+ canvas.translate((size - width) / 2f, (size - height) / 2f);
+ canvas.scale(scale, scale);
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+ if (recycle) bitmap.recycle();
+ return target;
+ }
+
+ public static void recycleSilently(Bitmap bitmap) {
+ if (bitmap == null) return;
+ try {
+ bitmap.recycle();
+ } catch (Throwable t) {
+ Log.w(TAG, "unable recycle bitmap", t);
+ }
+ }
+
+ public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
+ int w = source.getWidth();
+ int h = source.getHeight();
+ Matrix m = new Matrix();
+ m.postRotate(rotation);
+ Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
+ if (recycle) source.recycle();
+ return bitmap;
+ }
+
+ public static Bitmap createVideoThumbnail(String filePath) {
+ // MediaMetadataRetriever is available on API Level 8
+ // but is hidden until API Level 10
+ Class<?> clazz = null;
+ Object instance = null;
+ try {
+ clazz = Class.forName("android.media.MediaMetadataRetriever");
+ instance = clazz.newInstance();
+
+ Method method = clazz.getMethod("setDataSource", String.class);
+ method.invoke(instance, filePath);
+
+ // The method name changes between API Level 9 and 10.
+ if (Build.VERSION.SDK_INT <= 9) {
+ return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
+ } else {
+ return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
+ }
+ } catch (IllegalArgumentException ex) {
+ // Assume this is a corrupt video file
+ } catch (RuntimeException ex) {
+ // Assume this is a corrupt video file.
+ } catch (InstantiationException e) {
+ Log.e(TAG, "createVideoThumbnail", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "createVideoThumbnail", e);
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "createVideoThumbnail", e);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "createVideoThumbnail", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "createVideoThumbnail", e);
+ } finally {
+ try {
+ if (instance != null) {
+ clazz.getMethod("release").invoke(instance);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ return null;
+ }
+
+ public static byte[] compressBitmap(Bitmap bitmap) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG,
+ COMPRESS_JPEG_QUALITY, os);
+ return os.toByteArray();
+ }
+
+ public static boolean isSupportedByRegionDecoder(String mimeType) {
+ if (mimeType == null) return false;
+ mimeType = mimeType.toLowerCase();
+ return mimeType.startsWith("image/") &&
+ (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
+ }
+
+ public static boolean isRotationSupported(String mimeType) {
+ if (mimeType == null) return false;
+ mimeType = mimeType.toLowerCase();
+ return mimeType.equals("image/jpeg");
+ }
+
+ public static byte[] compressToBytes(Bitmap bitmap, int quality) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
+ bitmap.compress(CompressFormat.JPEG, quality, baos);
+ return baos.toByteArray();
+
+ }
+
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/BlobCache.java b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java
new file mode 100644
index 000000000..19a2e3090
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java
@@ -0,0 +1,653 @@
+/*
+ * 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.
+ */
+
+// This is an on-disk cache which maps a 64-bits key to a byte array.
+//
+// It consists of three files: one index file and two data files. One of the
+// data files is "active", and the other is "inactive". New entries are
+// appended into the active region until it reaches the size limit. At that
+// point the active file and the inactive file are swapped, and the new active
+// file is truncated to empty (and the index for that file is also cleared).
+// The index is a hash table with linear probing. When the load factor reaches
+// 0.5, it does the same thing like when the size limit is reached.
+//
+// The index file format: (all numbers are stored in little-endian)
+// [0] Magic number: 0xB3273030
+// [4] MaxEntries: Max number of hash entries per region.
+// [8] MaxBytes: Max number of data bytes per region (including header).
+// [12] ActiveRegion: The active growing region: 0 or 1.
+// [16] ActiveEntries: The number of hash entries used in the active region.
+// [20] ActiveBytes: The number of data bytes used in the active region.
+// [24] Version number.
+// [28] Checksum of [0..28).
+// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes).
+// [32 + X] Hash entries for region 1. The size is also X.
+//
+// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data
+// file. The offset is 0 when the slot is free. Note that 0 is a valid value
+// for key. The keys are used directly as index into a hash table, so they
+// should be suitably distributed.
+//
+// Each data file stores data for one region. The data file is concatenated
+// blobs followed by the magic number 0xBD248510.
+//
+// The blob format:
+// [0] Key of this blob
+// [8] Checksum of this blob
+// [12] Offset of this blob
+// [16] Length of this blob (not including header)
+// [20] Blob
+//
+// Below are the interface for BlobCache. The instance of this class does not
+// support concurrent use by multiple threads.
+//
+// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException;
+// public void insert(long key, byte[] data) throws IOException;
+// public byte[] lookup(long key) throws IOException;
+// public void lookup(LookupRequest req) throws IOException;
+// public void close();
+// public void syncIndex();
+// public void syncAll();
+// public static void deleteFiles(String path);
+//
+package com.android.gallery3d.common;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.zip.Adler32;
+
+public class BlobCache {
+ private static final String TAG = "BlobCache";
+
+ private static final int MAGIC_INDEX_FILE = 0xB3273030;
+ private static final int MAGIC_DATA_FILE = 0xBD248510;
+
+ // index header offset
+ private static final int IH_MAGIC = 0;
+ private static final int IH_MAX_ENTRIES = 4;
+ private static final int IH_MAX_BYTES = 8;
+ private static final int IH_ACTIVE_REGION = 12;
+ private static final int IH_ACTIVE_ENTRIES = 16;
+ private static final int IH_ACTIVE_BYTES = 20;
+ private static final int IH_VERSION = 24;
+ private static final int IH_CHECKSUM = 28;
+ private static final int INDEX_HEADER_SIZE = 32;
+
+ private static final int DATA_HEADER_SIZE = 4;
+
+ // blob header offset
+ private static final int BH_KEY = 0;
+ private static final int BH_CHECKSUM = 8;
+ private static final int BH_OFFSET = 12;
+ private static final int BH_LENGTH = 16;
+ private static final int BLOB_HEADER_SIZE = 20;
+
+ private RandomAccessFile mIndexFile;
+ private RandomAccessFile mDataFile0;
+ private RandomAccessFile mDataFile1;
+ private FileChannel mIndexChannel;
+ private MappedByteBuffer mIndexBuffer;
+
+ private int mMaxEntries;
+ private int mMaxBytes;
+ private int mActiveRegion;
+ private int mActiveEntries;
+ private int mActiveBytes;
+ private int mVersion;
+
+ private RandomAccessFile mActiveDataFile;
+ private RandomAccessFile mInactiveDataFile;
+ private int mActiveHashStart;
+ private int mInactiveHashStart;
+ private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE];
+ private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE];
+ private Adler32 mAdler32 = new Adler32();
+
+ // Creates the cache. Three files will be created:
+ // path + ".idx", path + ".0", and path + ".1"
+ // The ".0" file and the ".1" file each stores data for a region. Each of
+ // them can grow to the size specified by maxBytes. The maxEntries parameter
+ // specifies the maximum number of entries each region can have. If the
+ // "reset" parameter is true, the cache will be cleared before use.
+ public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
+ throws IOException {
+ this(path, maxEntries, maxBytes, reset, 0);
+ }
+
+ public BlobCache(String path, int maxEntries, int maxBytes, boolean reset,
+ int version) throws IOException {
+ mIndexFile = new RandomAccessFile(path + ".idx", "rw");
+ mDataFile0 = new RandomAccessFile(path + ".0", "rw");
+ mDataFile1 = new RandomAccessFile(path + ".1", "rw");
+ mVersion = version;
+
+ if (!reset && loadIndex()) {
+ return;
+ }
+
+ resetCache(maxEntries, maxBytes);
+
+ if (!loadIndex()) {
+ closeAll();
+ throw new IOException("unable to load index");
+ }
+ }
+
+ // Delete the files associated with the given path previously created
+ // by the BlobCache constructor.
+ public static void deleteFiles(String path) {
+ deleteFileSilently(path + ".idx");
+ deleteFileSilently(path + ".0");
+ deleteFileSilently(path + ".1");
+ }
+
+ private static void deleteFileSilently(String path) {
+ try {
+ new File(path).delete();
+ } catch (Throwable t) {
+ // ignore;
+ }
+ }
+
+ // Close the cache. All resources are released. No other method should be
+ // called after this is called.
+ public void close() {
+ syncAll();
+ closeAll();
+ }
+
+ private void closeAll() {
+ closeSilently(mIndexChannel);
+ closeSilently(mIndexFile);
+ closeSilently(mDataFile0);
+ closeSilently(mDataFile1);
+ }
+
+ // Returns true if loading index is successful. After this method is called,
+ // mIndexHeader and index header in file should be kept sync.
+ private boolean loadIndex() {
+ try {
+ mIndexFile.seek(0);
+ mDataFile0.seek(0);
+ mDataFile1.seek(0);
+
+ byte[] buf = mIndexHeader;
+ if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) {
+ Log.w(TAG, "cannot read header");
+ return false;
+ }
+
+ if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
+ Log.w(TAG, "cannot read header magic");
+ return false;
+ }
+
+ if (readInt(buf, IH_VERSION) != mVersion) {
+ Log.w(TAG, "version mismatch");
+ return false;
+ }
+
+ mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
+ mMaxBytes = readInt(buf, IH_MAX_BYTES);
+ mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
+ mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
+ mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);
+
+ int sum = readInt(buf, IH_CHECKSUM);
+ if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
+ Log.w(TAG, "header checksum does not match");
+ return false;
+ }
+
+ // Sanity check
+ if (mMaxEntries <= 0) {
+ Log.w(TAG, "invalid max entries");
+ return false;
+ }
+ if (mMaxBytes <= 0) {
+ Log.w(TAG, "invalid max bytes");
+ return false;
+ }
+ if (mActiveRegion != 0 && mActiveRegion != 1) {
+ Log.w(TAG, "invalid active region");
+ return false;
+ }
+ if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
+ Log.w(TAG, "invalid active entries");
+ return false;
+ }
+ if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
+ Log.w(TAG, "invalid active bytes");
+ return false;
+ }
+ if (mIndexFile.length() !=
+ INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) {
+ Log.w(TAG, "invalid index file length");
+ return false;
+ }
+
+ // Make sure data file has magic
+ byte[] magic = new byte[4];
+ if (mDataFile0.read(magic) != 4) {
+ Log.w(TAG, "cannot read data file magic");
+ return false;
+ }
+ if (readInt(magic, 0) != MAGIC_DATA_FILE) {
+ Log.w(TAG, "invalid data file magic");
+ return false;
+ }
+ if (mDataFile1.read(magic) != 4) {
+ Log.w(TAG, "cannot read data file magic");
+ return false;
+ }
+ if (readInt(magic, 0) != MAGIC_DATA_FILE) {
+ Log.w(TAG, "invalid data file magic");
+ return false;
+ }
+
+ // Map index file to memory
+ mIndexChannel = mIndexFile.getChannel();
+ mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
+ 0, mIndexFile.length());
+ mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ setActiveVariables();
+ return true;
+ } catch (IOException ex) {
+ Log.e(TAG, "loadIndex failed.", ex);
+ return false;
+ }
+ }
+
+ private void setActiveVariables() throws IOException {
+ mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
+ mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
+ mActiveDataFile.setLength(mActiveBytes);
+ mActiveDataFile.seek(mActiveBytes);
+
+ mActiveHashStart = INDEX_HEADER_SIZE;
+ mInactiveHashStart = INDEX_HEADER_SIZE;
+
+ if (mActiveRegion == 0) {
+ mInactiveHashStart += mMaxEntries * 12;
+ } else {
+ mActiveHashStart += mMaxEntries * 12;
+ }
+ }
+
+ private void resetCache(int maxEntries, int maxBytes) throws IOException {
+ mIndexFile.setLength(0); // truncate to zero the index
+ mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2);
+ mIndexFile.seek(0);
+ byte[] buf = mIndexHeader;
+ writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE);
+ writeInt(buf, IH_MAX_ENTRIES, maxEntries);
+ writeInt(buf, IH_MAX_BYTES, maxBytes);
+ writeInt(buf, IH_ACTIVE_REGION, 0);
+ writeInt(buf, IH_ACTIVE_ENTRIES, 0);
+ writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE);
+ writeInt(buf, IH_VERSION, mVersion);
+ writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM));
+ mIndexFile.write(buf);
+ // This is only needed if setLength does not zero the extended part.
+ // writeZero(mIndexFile, maxEntries * 12 * 2);
+
+ mDataFile0.setLength(0);
+ mDataFile1.setLength(0);
+ mDataFile0.seek(0);
+ mDataFile1.seek(0);
+ writeInt(buf, 0, MAGIC_DATA_FILE);
+ mDataFile0.write(buf, 0, 4);
+ mDataFile1.write(buf, 0, 4);
+ }
+
+ // Flip the active region and the inactive region.
+ private void flipRegion() throws IOException {
+ mActiveRegion = 1 - mActiveRegion;
+ mActiveEntries = 0;
+ mActiveBytes = DATA_HEADER_SIZE;
+
+ writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion);
+ writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+ writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
+ updateIndexHeader();
+
+ setActiveVariables();
+ clearHash(mActiveHashStart);
+ syncIndex();
+ }
+
+ // Sync mIndexHeader to the index file.
+ private void updateIndexHeader() {
+ writeInt(mIndexHeader, IH_CHECKSUM,
+ checkSum(mIndexHeader, 0, IH_CHECKSUM));
+ mIndexBuffer.position(0);
+ mIndexBuffer.put(mIndexHeader);
+ }
+
+ // Clear the hash table starting from the specified offset.
+ private void clearHash(int hashStart) {
+ byte[] zero = new byte[1024];
+ mIndexBuffer.position(hashStart);
+ for (int count = mMaxEntries * 12; count > 0;) {
+ int todo = Math.min(count, 1024);
+ mIndexBuffer.put(zero, 0, todo);
+ count -= todo;
+ }
+ }
+
+ // Inserts a (key, data) pair into the cache.
+ public void insert(long key, byte[] data) throws IOException {
+ if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) {
+ throw new RuntimeException("blob is too large!");
+ }
+
+ if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
+ || mActiveEntries * 2 >= mMaxEntries) {
+ flipRegion();
+ }
+
+ if (!lookupInternal(key, mActiveHashStart)) {
+ // If we don't have an existing entry with the same key, increase
+ // the entry count.
+ mActiveEntries++;
+ writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+ }
+
+ insertInternal(key, data, data.length);
+ updateIndexHeader();
+ }
+
+ // Appends the data to the active file. It also updates the hash entry.
+ // The proper hash entry (suitable for insertion or replacement) must be
+ // pointed by mSlotOffset.
+ private void insertInternal(long key, byte[] data, int length)
+ throws IOException {
+ byte[] header = mBlobHeader;
+ int sum = checkSum(data);
+ writeLong(header, BH_KEY, key);
+ writeInt(header, BH_CHECKSUM, sum);
+ writeInt(header, BH_OFFSET, mActiveBytes);
+ writeInt(header, BH_LENGTH, length);
+ mActiveDataFile.write(header);
+ mActiveDataFile.write(data, 0, length);
+
+ mIndexBuffer.putLong(mSlotOffset, key);
+ mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes);
+ mActiveBytes += BLOB_HEADER_SIZE + length;
+ writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
+ }
+
+ public static class LookupRequest {
+ public long key; // input: the key to find
+ public byte[] buffer; // input/output: the buffer to store the blob
+ public int length; // output: the length of the blob
+ }
+
+ // This method is for one-off lookup. For repeated lookup, use the version
+ // accepting LookupRequest to avoid repeated memory allocation.
+ private LookupRequest mLookupRequest = new LookupRequest();
+ public byte[] lookup(long key) throws IOException {
+ mLookupRequest.key = key;
+ mLookupRequest.buffer = null;
+ if (lookup(mLookupRequest)) {
+ return mLookupRequest.buffer;
+ } else {
+ return null;
+ }
+ }
+
+ // Returns true if the associated blob for the given key is available.
+ // The blob is stored in the buffer pointed by req.buffer, and the length
+ // is in stored in the req.length variable.
+ //
+ // The user can input a non-null value in req.buffer, and this method will
+ // try to use that buffer. If that buffer is not large enough, this method
+ // will allocate a new buffer and assign it to req.buffer.
+ //
+ // This method tries not to throw IOException even if the data file is
+ // corrupted, but it can still throw IOException if things get strange.
+ public boolean lookup(LookupRequest req) throws IOException {
+ // Look up in the active region first.
+ if (lookupInternal(req.key, mActiveHashStart)) {
+ if (getBlob(mActiveDataFile, mFileOffset, req)) {
+ return true;
+ }
+ }
+
+ // We want to copy the data from the inactive file to the active file
+ // if it's available. So we keep the offset of the hash entry so we can
+ // avoid looking it up again.
+ int insertOffset = mSlotOffset;
+
+ // Look up in the inactive region.
+ if (lookupInternal(req.key, mInactiveHashStart)) {
+ if (getBlob(mInactiveDataFile, mFileOffset, req)) {
+ // If we don't have enough space to insert this blob into
+ // the active file, just return it.
+ if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
+ || mActiveEntries * 2 >= mMaxEntries) {
+ return true;
+ }
+ // Otherwise copy it over.
+ mSlotOffset = insertOffset;
+ try {
+ insertInternal(req.key, req.buffer, req.length);
+ mActiveEntries++;
+ writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+ updateIndexHeader();
+ } catch (Throwable t) {
+ Log.e(TAG, "cannot copy over");
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ // Copies the blob for the specified offset in the specified file to
+ // req.buffer. If req.buffer is null or too small, allocate a buffer and
+ // assign it to req.buffer.
+ // Returns false if the blob is not available (either the index file is
+ // not sync with the data file, or one of them is corrupted). The length
+ // of the blob is stored in the req.length variable.
+ private boolean getBlob(RandomAccessFile file, int offset,
+ LookupRequest req) throws IOException {
+ byte[] header = mBlobHeader;
+ long oldPosition = file.getFilePointer();
+ try {
+ file.seek(offset);
+ if (file.read(header) != BLOB_HEADER_SIZE) {
+ Log.w(TAG, "cannot read blob header");
+ return false;
+ }
+ long blobKey = readLong(header, BH_KEY);
+ if (blobKey != req.key) {
+ Log.w(TAG, "blob key does not match: " + blobKey);
+ return false;
+ }
+ int sum = readInt(header, BH_CHECKSUM);
+ int blobOffset = readInt(header, BH_OFFSET);
+ if (blobOffset != offset) {
+ Log.w(TAG, "blob offset does not match: " + blobOffset);
+ return false;
+ }
+ int length = readInt(header, BH_LENGTH);
+ if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
+ Log.w(TAG, "invalid blob length: " + length);
+ return false;
+ }
+ if (req.buffer == null || req.buffer.length < length) {
+ req.buffer = new byte[length];
+ }
+
+ byte[] blob = req.buffer;
+ req.length = length;
+
+ if (file.read(blob, 0, length) != length) {
+ Log.w(TAG, "cannot read blob data");
+ return false;
+ }
+ if (checkSum(blob, 0, length) != sum) {
+ Log.w(TAG, "blob checksum does not match: " + sum);
+ return false;
+ }
+ return true;
+ } catch (Throwable t) {
+ Log.e(TAG, "getBlob failed.", t);
+ return false;
+ } finally {
+ file.seek(oldPosition);
+ }
+ }
+
+ // Tries to look up a key in the specified hash region.
+ // Returns true if the lookup is successful.
+ // The slot offset in the index file is saved in mSlotOffset. If the lookup
+ // is successful, it's the slot found. Otherwise it's the slot suitable for
+ // insertion.
+ // If the lookup is successful, the file offset is also saved in
+ // mFileOffset.
+ private int mSlotOffset;
+ private int mFileOffset;
+ private boolean lookupInternal(long key, int hashStart) {
+ int slot = (int) (key % mMaxEntries);
+ if (slot < 0) slot += mMaxEntries;
+ int slotBegin = slot;
+ while (true) {
+ int offset = hashStart + slot * 12;
+ long candidateKey = mIndexBuffer.getLong(offset);
+ int candidateOffset = mIndexBuffer.getInt(offset + 8);
+ if (candidateOffset == 0) {
+ mSlotOffset = offset;
+ return false;
+ } else if (candidateKey == key) {
+ mSlotOffset = offset;
+ mFileOffset = candidateOffset;
+ return true;
+ } else {
+ if (++slot >= mMaxEntries) {
+ slot = 0;
+ }
+ if (slot == slotBegin) {
+ Log.w(TAG, "corrupted index: clear the slot.");
+ mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
+ }
+ }
+ }
+ }
+
+ public void syncIndex() {
+ try {
+ mIndexBuffer.force();
+ } catch (Throwable t) {
+ Log.w(TAG, "sync index failed", t);
+ }
+ }
+
+ public void syncAll() {
+ syncIndex();
+ try {
+ mDataFile0.getFD().sync();
+ } catch (Throwable t) {
+ Log.w(TAG, "sync data file 0 failed", t);
+ }
+ try {
+ mDataFile1.getFD().sync();
+ } catch (Throwable t) {
+ Log.w(TAG, "sync data file 1 failed", t);
+ }
+ }
+
+ // This is for testing only.
+ //
+ // Returns the active count (mActiveEntries). This also verifies that
+ // the active count matches matches what's inside the hash region.
+ int getActiveCount() {
+ int count = 0;
+ for (int i = 0; i < mMaxEntries; i++) {
+ int offset = mActiveHashStart + i * 12;
+ long candidateKey = mIndexBuffer.getLong(offset);
+ int candidateOffset = mIndexBuffer.getInt(offset + 8);
+ if (candidateOffset != 0) ++count;
+ }
+ if (count == mActiveEntries) {
+ return count;
+ } else {
+ Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count);
+ return -1; // signal failure.
+ }
+ }
+
+ int checkSum(byte[] data) {
+ mAdler32.reset();
+ mAdler32.update(data);
+ return (int) mAdler32.getValue();
+ }
+
+ int checkSum(byte[] data, int offset, int nbytes) {
+ mAdler32.reset();
+ mAdler32.update(data, offset, nbytes);
+ return (int) mAdler32.getValue();
+ }
+
+ static void closeSilently(Closeable c) {
+ if (c == null) return;
+ try {
+ c.close();
+ } catch (Throwable t) {
+ // do nothing
+ }
+ }
+
+ static int readInt(byte[] buf, int offset) {
+ return (buf[offset] & 0xff)
+ | ((buf[offset + 1] & 0xff) << 8)
+ | ((buf[offset + 2] & 0xff) << 16)
+ | ((buf[offset + 3] & 0xff) << 24);
+ }
+
+ static long readLong(byte[] buf, int offset) {
+ long result = buf[offset + 7] & 0xff;
+ for (int i = 6; i >= 0; i--) {
+ result = (result << 8) | (buf[offset + i] & 0xff);
+ }
+ return result;
+ }
+
+ static void writeInt(byte[] buf, int offset, int value) {
+ for (int i = 0; i < 4; i++) {
+ buf[offset + i] = (byte) (value & 0xff);
+ value >>= 8;
+ }
+ }
+
+ static void writeLong(byte[] buf, int offset, long value) {
+ for (int i = 0; i < 8; i++) {
+ buf[offset + i] = (byte) (value & 0xff);
+ value >>= 8;
+ }
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Entry.java b/gallerycommon/src/com/android/gallery3d/common/Entry.java
new file mode 100644
index 000000000..b8cc51205
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 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.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public abstract class Entry {
+ public static final String[] ID_PROJECTION = { "_id" };
+
+ public static interface Columns {
+ public static final String ID = "_id";
+ }
+
+ // The primary key of the entry.
+ @Column("_id")
+ public long id = 0;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface Table {
+ String value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Column {
+ String value();
+
+ boolean indexed() default false;
+
+ boolean fullText() default false;
+
+ String defaultValue() default "";
+ }
+
+ public void clear() {
+ id = 0;
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
new file mode 100644
index 000000000..d652ac98a
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2009 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.common;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+public final class EntrySchema {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EntrySchema";
+
+ private static final int TYPE_STRING = 0;
+ private static final int TYPE_BOOLEAN = 1;
+ private static final int TYPE_SHORT = 2;
+ private static final int TYPE_INT = 3;
+ private static final int TYPE_LONG = 4;
+ private static final int TYPE_FLOAT = 5;
+ private static final int TYPE_DOUBLE = 6;
+ private static final int TYPE_BLOB = 7;
+ private static final String SQLITE_TYPES[] = {
+ "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };
+
+ private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext";
+
+ private final String mTableName;
+ private final ColumnInfo[] mColumnInfo;
+ private final String[] mProjection;
+ private final boolean mHasFullTextIndex;
+
+ public EntrySchema(Class<? extends Entry> clazz) {
+ // Get table and column metadata from reflection.
+ ColumnInfo[] columns = parseColumnInfo(clazz);
+ mTableName = parseTableName(clazz);
+ mColumnInfo = columns;
+
+ // Cache the list of projection columns and check for full-text columns.
+ String[] projection = {};
+ boolean hasFullTextIndex = false;
+ if (columns != null) {
+ projection = new String[columns.length];
+ for (int i = 0; i != columns.length; ++i) {
+ ColumnInfo column = columns[i];
+ projection[i] = column.name;
+ if (column.fullText) {
+ hasFullTextIndex = true;
+ }
+ }
+ }
+ mProjection = projection;
+ mHasFullTextIndex = hasFullTextIndex;
+ }
+
+ public String getTableName() {
+ return mTableName;
+ }
+
+ public ColumnInfo[] getColumnInfo() {
+ return mColumnInfo;
+ }
+
+ public String[] getProjection() {
+ return mProjection;
+ }
+
+ public int getColumnIndex(String columnName) {
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.name.equals(columnName)) {
+ return column.projectionIndex;
+ }
+ }
+ return -1;
+ }
+
+ private ColumnInfo getColumn(String columnName) {
+ int index = getColumnIndex(columnName);
+ return (index < 0) ? null : mColumnInfo[index];
+ }
+
+ private void logExecSql(SQLiteDatabase db, String sql) {
+ db.execSQL(sql);
+ }
+
+ public <T extends Entry> T cursorToObject(Cursor cursor, T object) {
+ try {
+ for (ColumnInfo column : mColumnInfo) {
+ int columnIndex = column.projectionIndex;
+ Field field = column.field;
+ switch (column.type) {
+ case TYPE_STRING:
+ field.set(object, cursor.isNull(columnIndex)
+ ? null
+ : cursor.getString(columnIndex));
+ break;
+ case TYPE_BOOLEAN:
+ field.setBoolean(object, cursor.getShort(columnIndex) == 1);
+ break;
+ case TYPE_SHORT:
+ field.setShort(object, cursor.getShort(columnIndex));
+ break;
+ case TYPE_INT:
+ field.setInt(object, cursor.getInt(columnIndex));
+ break;
+ case TYPE_LONG:
+ field.setLong(object, cursor.getLong(columnIndex));
+ break;
+ case TYPE_FLOAT:
+ field.setFloat(object, cursor.getFloat(columnIndex));
+ break;
+ case TYPE_DOUBLE:
+ field.setDouble(object, cursor.getDouble(columnIndex));
+ break;
+ case TYPE_BLOB:
+ field.set(object, cursor.isNull(columnIndex)
+ ? null
+ : cursor.getBlob(columnIndex));
+ break;
+ }
+ }
+ return object;
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setIfNotNull(Field field, Object object, Object value)
+ throws IllegalAccessException {
+ if (value != null) field.set(object, value);
+ }
+
+ /**
+ * Converts the ContentValues to the object. The ContentValues may not
+ * contain values for all the fields in the object.
+ */
+ public <T extends Entry> T valuesToObject(ContentValues values, T object) {
+ try {
+ for (ColumnInfo column : mColumnInfo) {
+ String columnName = column.name;
+ Field field = column.field;
+ switch (column.type) {
+ case TYPE_STRING:
+ setIfNotNull(field, object, values.getAsString(columnName));
+ break;
+ case TYPE_BOOLEAN:
+ setIfNotNull(field, object, values.getAsBoolean(columnName));
+ break;
+ case TYPE_SHORT:
+ setIfNotNull(field, object, values.getAsShort(columnName));
+ break;
+ case TYPE_INT:
+ setIfNotNull(field, object, values.getAsInteger(columnName));
+ break;
+ case TYPE_LONG:
+ setIfNotNull(field, object, values.getAsLong(columnName));
+ break;
+ case TYPE_FLOAT:
+ setIfNotNull(field, object, values.getAsFloat(columnName));
+ break;
+ case TYPE_DOUBLE:
+ setIfNotNull(field, object, values.getAsDouble(columnName));
+ break;
+ case TYPE_BLOB:
+ setIfNotNull(field, object, values.getAsByteArray(columnName));
+ break;
+ }
+ }
+ return object;
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void objectToValues(Entry object, ContentValues values) {
+ try {
+ for (ColumnInfo column : mColumnInfo) {
+ String columnName = column.name;
+ Field field = column.field;
+ switch (column.type) {
+ case TYPE_STRING:
+ values.put(columnName, (String) field.get(object));
+ break;
+ case TYPE_BOOLEAN:
+ values.put(columnName, field.getBoolean(object));
+ break;
+ case TYPE_SHORT:
+ values.put(columnName, field.getShort(object));
+ break;
+ case TYPE_INT:
+ values.put(columnName, field.getInt(object));
+ break;
+ case TYPE_LONG:
+ values.put(columnName, field.getLong(object));
+ break;
+ case TYPE_FLOAT:
+ values.put(columnName, field.getFloat(object));
+ break;
+ case TYPE_DOUBLE:
+ values.put(columnName, field.getDouble(object));
+ break;
+ case TYPE_BLOB:
+ values.put(columnName, (byte[]) field.get(object));
+ break;
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String toDebugString(Entry entry) {
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append("ID=").append(entry.id);
+ for (ColumnInfo column : mColumnInfo) {
+ String columnName = column.name;
+ Field field = column.field;
+ Object value = field.get(entry);
+ sb.append(" ").append(columnName).append("=")
+ .append((value == null) ? "null" : value.toString());
+ }
+ return sb.toString();
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String toDebugString(Entry entry, String... columnNames) {
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append("ID=").append(entry.id);
+ for (String columnName : columnNames) {
+ ColumnInfo column = getColumn(columnName);
+ Field field = column.field;
+ Object value = field.get(entry);
+ sb.append(" ").append(columnName).append("=")
+ .append((value == null) ? "null" : value.toString());
+ }
+ return sb.toString();
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Cursor queryAll(SQLiteDatabase db) {
+ return db.query(mTableName, mProjection, null, null, null, null, null);
+ }
+
+ public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) {
+ Cursor cursor = db.query(mTableName, mProjection, "_id=?",
+ new String[] {Long.toString(id)}, null, null, null);
+ boolean success = false;
+ if (cursor.moveToFirst()) {
+ cursorToObject(cursor, entry);
+ success = true;
+ }
+ cursor.close();
+ return success;
+ }
+
+ public long insertOrReplace(SQLiteDatabase db, Entry entry) {
+ ContentValues values = new ContentValues();
+ objectToValues(entry, values);
+ if (entry.id == 0) {
+ values.remove("_id");
+ }
+ long id = db.replace(mTableName, "_id", values);
+ entry.id = id;
+ return id;
+ }
+
+ public boolean deleteWithId(SQLiteDatabase db, long id) {
+ return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1;
+ }
+
+ public void createTables(SQLiteDatabase db) {
+ // Wrapped class must have a @Table.Definition.
+ String tableName = mTableName;
+ Utils.assertTrue(tableName != null);
+
+ // Add the CREATE TABLE statement for the main table.
+ StringBuilder sql = new StringBuilder("CREATE TABLE ");
+ sql.append(tableName);
+ sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT");
+ for (ColumnInfo column : mColumnInfo) {
+ if (!column.isId()) {
+ sql.append(',');
+ sql.append(column.name);
+ sql.append(' ');
+ sql.append(SQLITE_TYPES[column.type]);
+ if (!TextUtils.isEmpty(column.defaultValue)) {
+ sql.append(" DEFAULT ");
+ sql.append(column.defaultValue);
+ }
+ }
+ }
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Create indexes for all indexed columns.
+ for (ColumnInfo column : mColumnInfo) {
+ // Create an index on the indexed columns.
+ if (column.indexed) {
+ sql.append("CREATE INDEX ");
+ sql.append(tableName);
+ sql.append("_index_");
+ sql.append(column.name);
+ sql.append(" ON ");
+ sql.append(tableName);
+ sql.append(" (");
+ sql.append(column.name);
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+ }
+ }
+
+ if (mHasFullTextIndex) {
+ // Add an FTS virtual table if using full-text search.
+ String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX;
+ sql.append("CREATE VIRTUAL TABLE ");
+ sql.append(ftsTableName);
+ sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ // Add the column to the FTS table.
+ String columnName = column.name;
+ sql.append(',');
+ sql.append(columnName);
+ sql.append(" TEXT");
+ }
+ }
+ sql.append(");");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Build an insert statement that will automatically keep the FTS
+ // table in sync.
+ StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO ");
+ insertSql.append(ftsTableName);
+ insertSql.append(" (_id");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ insertSql.append(',');
+ insertSql.append(column.name);
+ }
+ }
+ insertSql.append(") VALUES (new._id");
+ for (ColumnInfo column : mColumnInfo) {
+ if (column.fullText) {
+ insertSql.append(",new.");
+ insertSql.append(column.name);
+ }
+ }
+ insertSql.append(");");
+ String insertSqlString = insertSql.toString();
+
+ // Add an insert trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_insert_trigger AFTER INSERT ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN ");
+ sql.append(insertSqlString);
+ sql.append("END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Add an update trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_update_trigger AFTER UPDATE ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN ");
+ sql.append(insertSqlString);
+ sql.append("END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ // Add a delete trigger.
+ sql.append("CREATE TRIGGER ");
+ sql.append(tableName);
+ sql.append("_delete_trigger AFTER DELETE ON ");
+ sql.append(tableName);
+ sql.append(" FOR EACH ROW BEGIN DELETE FROM ");
+ sql.append(ftsTableName);
+ sql.append(" WHERE _id = old._id; END;");
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+ }
+ }
+
+ public void dropTables(SQLiteDatabase db) {
+ String tableName = mTableName;
+ StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
+ sql.append(tableName);
+ sql.append(';');
+ logExecSql(db, sql.toString());
+ sql.setLength(0);
+
+ if (mHasFullTextIndex) {
+ sql.append("DROP TABLE IF EXISTS ");
+ sql.append(tableName);
+ sql.append(FULL_TEXT_INDEX_SUFFIX);
+ sql.append(';');
+ logExecSql(db, sql.toString());
+ }
+
+ }
+
+ public void deleteAll(SQLiteDatabase db) {
+ StringBuilder sql = new StringBuilder("DELETE FROM ");
+ sql.append(mTableName);
+ sql.append(";");
+ logExecSql(db, sql.toString());
+ }
+
+ private String parseTableName(Class<? extends Object> clazz) {
+ // Check for a table annotation.
+ Entry.Table table = clazz.getAnnotation(Entry.Table.class);
+ if (table == null) {
+ return null;
+ }
+
+ // Return the table name.
+ return table.value();
+ }
+
+ private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) {
+ ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
+ while (clazz != null) {
+ parseColumnInfo(clazz, columns);
+ clazz = clazz.getSuperclass();
+ }
+
+ // Return a list.
+ ColumnInfo[] columnList = new ColumnInfo[columns.size()];
+ columns.toArray(columnList);
+ return columnList;
+ }
+
+ private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) {
+ // Gather metadata from each annotated field.
+ Field[] fields = clazz.getDeclaredFields(); // including non-public fields
+ for (int i = 0; i != fields.length; ++i) {
+ // Get column metadata from the annotation.
+ Field field = fields[i];
+ Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class);
+ if (info == null) continue;
+
+ // Determine the field type.
+ int type;
+ Class<?> fieldType = field.getType();
+ if (fieldType == String.class) {
+ type = TYPE_STRING;
+ } else if (fieldType == boolean.class) {
+ type = TYPE_BOOLEAN;
+ } else if (fieldType == short.class) {
+ type = TYPE_SHORT;
+ } else if (fieldType == int.class) {
+ type = TYPE_INT;
+ } else if (fieldType == long.class) {
+ type = TYPE_LONG;
+ } else if (fieldType == float.class) {
+ type = TYPE_FLOAT;
+ } else if (fieldType == double.class) {
+ type = TYPE_DOUBLE;
+ } else if (fieldType == byte[].class) {
+ type = TYPE_BLOB;
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported field type for column: " + fieldType.getName());
+ }
+
+ // Add the column to the array.
+ int index = columns.size();
+ columns.add(new ColumnInfo(info.value(), type, info.indexed(),
+ info.fullText(), info.defaultValue(), field, index));
+ }
+ }
+
+ public static final class ColumnInfo {
+ private static final String ID_KEY = "_id";
+
+ public final String name;
+ public final int type;
+ public final boolean indexed;
+ public final boolean fullText;
+ public final String defaultValue;
+ public final Field field;
+ public final int projectionIndex;
+
+ public ColumnInfo(String name, int type, boolean indexed,
+ boolean fullText, String defaultValue, Field field, int projectionIndex) {
+ this.name = name.toLowerCase();
+ this.type = type;
+ this.indexed = indexed;
+ this.fullText = fullText;
+ this.defaultValue = defaultValue;
+ this.field = field;
+ this.projectionIndex = projectionIndex;
+
+ field.setAccessible(true); // in order to set non-public fields
+ }
+
+ public boolean isId() {
+ return ID_KEY.equals(name);
+ }
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/FileCache.java b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
new file mode 100644
index 000000000..a69d6e170
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
@@ -0,0 +1,303 @@
+/*
+ * 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.common;
+
+import com.android.gallery3d.common.Entry.Table;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileCache {
+ private static final int LRU_CAPACITY = 4;
+ private static final int MAX_DELETE_COUNT = 16;
+
+ private static final String TAG = "FileCache";
+ private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName();
+ private static final String FILE_PREFIX = "download";
+ private static final String FILE_POSTFIX = ".tmp";
+
+ private static final String QUERY_WHERE =
+ FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?";
+ private static final String ID_WHERE = FileEntry.Columns.ID + "=?";
+ private static final String[] PROJECTION_SIZE_SUM =
+ {String.format("sum(%s)", FileEntry.Columns.SIZE)};
+ private static final String FREESPACE_PROJECTION[] = {
+ FileEntry.Columns.ID, FileEntry.Columns.FILENAME,
+ FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE};
+ private static final String FREESPACE_ORDER_BY =
+ String.format("%s ASC", FileEntry.Columns.LAST_ACCESS);
+
+ private final LruCache<String, CacheEntry> mEntryMap =
+ new LruCache<String, CacheEntry>(LRU_CAPACITY);
+
+ private File mRootDir;
+ private long mCapacity;
+ private boolean mInitialized = false;
+ private long mTotalBytes;
+
+ private DatabaseHelper mDbHelper;
+
+ public static final class CacheEntry {
+ private long id;
+ public String contentUrl;
+ public File cacheFile;
+
+ private CacheEntry(long id, String contentUrl, File cacheFile) {
+ this.id = id;
+ this.contentUrl = contentUrl;
+ this.cacheFile = cacheFile;
+ }
+ }
+
+ public static void deleteFiles(Context context, File rootDir, String dbName) {
+ try {
+ context.getDatabasePath(dbName).delete();
+ File[] files = rootDir.listFiles();
+ if (files == null) return;
+ for (File file : rootDir.listFiles()) {
+ String name = file.getName();
+ if (file.isFile() && name.startsWith(FILE_PREFIX)
+ && name.endsWith(FILE_POSTFIX)) file.delete();
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot reset database", t);
+ }
+ }
+
+ public FileCache(Context context, File rootDir, String dbName, long capacity) {
+ mRootDir = Utils.checkNotNull(rootDir);
+ mCapacity = capacity;
+ mDbHelper = new DatabaseHelper(context, dbName);
+ }
+
+ public void store(String downloadUrl, File file) {
+ if (!mInitialized) initialize();
+
+ Utils.assertTrue(file.getParentFile().equals(mRootDir));
+ FileEntry entry = new FileEntry();
+ entry.hashCode = Utils.crc64Long(downloadUrl);
+ entry.contentUrl = downloadUrl;
+ entry.filename = file.getName();
+ entry.size = file.length();
+ entry.lastAccess = System.currentTimeMillis();
+ if (entry.size >= mCapacity) {
+ file.delete();
+ throw new IllegalArgumentException("file too large: " + entry.size);
+ }
+ synchronized (this) {
+ FileEntry original = queryDatabase(downloadUrl);
+ if (original != null) {
+ file.delete();
+ entry.filename = original.filename;
+ entry.size = original.size;
+ } else {
+ mTotalBytes += entry.size;
+ }
+ FileEntry.SCHEMA.insertOrReplace(
+ mDbHelper.getWritableDatabase(), entry);
+ if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+ }
+
+ public CacheEntry lookup(String downloadUrl) {
+ if (!mInitialized) initialize();
+ CacheEntry entry;
+ synchronized (mEntryMap) {
+ entry = mEntryMap.get(downloadUrl);
+ }
+
+ if (entry != null) {
+ synchronized (this) {
+ updateLastAccess(entry.id);
+ }
+ return entry;
+ }
+
+ synchronized (this) {
+ FileEntry file = queryDatabase(downloadUrl);
+ if (file == null) return null;
+ entry = new CacheEntry(
+ file.id, downloadUrl, new File(mRootDir, file.filename));
+ if (!entry.cacheFile.isFile()) { // file has been removed
+ try {
+ mDbHelper.getWritableDatabase().delete(
+ TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)});
+ mTotalBytes -= file.size;
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot delete entry: " + file.filename, t);
+ }
+ return null;
+ }
+ synchronized (mEntryMap) {
+ mEntryMap.put(downloadUrl, entry);
+ }
+ return entry;
+ }
+ }
+
+ private FileEntry queryDatabase(String downloadUrl) {
+ long hash = Utils.crc64Long(downloadUrl);
+ String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl};
+ Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME,
+ FileEntry.SCHEMA.getProjection(),
+ QUERY_WHERE, whereArgs, null, null, null);
+ try {
+ if (!cursor.moveToNext()) return null;
+ FileEntry entry = new FileEntry();
+ FileEntry.SCHEMA.cursorToObject(cursor, entry);
+ updateLastAccess(entry.id);
+ return entry;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void updateLastAccess(long id) {
+ ContentValues values = new ContentValues();
+ values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis());
+ mDbHelper.getWritableDatabase().update(TABLE_NAME,
+ values, ID_WHERE, new String[] {String.valueOf(id)});
+ }
+
+ public File createFile() throws IOException {
+ return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir);
+ }
+
+ private synchronized void initialize() {
+ if (mInitialized) return;
+ mInitialized = true;
+
+ if (!mRootDir.isDirectory()) {
+ mRootDir.mkdirs();
+ if (!mRootDir.isDirectory()) {
+ throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath());
+ }
+ }
+
+ Cursor cursor = mDbHelper.getReadableDatabase().query(
+ TABLE_NAME, PROJECTION_SIZE_SUM,
+ null, null, null, null, null);
+ try {
+ if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0);
+ } finally {
+ cursor.close();
+ }
+ if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+
+ private void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
+ Cursor cursor = mDbHelper.getReadableDatabase().query(
+ TABLE_NAME, FREESPACE_PROJECTION,
+ null, null, null, null, FREESPACE_ORDER_BY);
+ try {
+ while (maxDeleteFileCount > 0
+ && mTotalBytes > mCapacity && cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String path = cursor.getString(1);
+ String url = cursor.getString(2);
+ long size = cursor.getLong(3);
+
+ synchronized (mEntryMap) {
+ // if some one still uses it
+ if (mEntryMap.containsKey(url)) continue;
+ }
+
+ --maxDeleteFileCount;
+ if (new File(mRootDir, path).delete()) {
+ mTotalBytes -= size;
+ mDbHelper.getWritableDatabase().delete(TABLE_NAME,
+ ID_WHERE, new String[]{String.valueOf(id)});
+ } else {
+ Log.w(TAG, "unable to delete file: " + path);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Table("files")
+ private static class FileEntry extends Entry {
+ public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class);
+
+ public 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 FILENAME = "filename";
+ public static final String SIZE = "size";
+ public static final String LAST_ACCESS = "last_access";
+ }
+
+ @Column(value = Columns.HASH_CODE, indexed = true)
+ public long hashCode;
+
+ @Column(Columns.CONTENT_URL)
+ public String contentUrl;
+
+ @Column(Columns.FILENAME)
+ public String filename;
+
+ @Column(Columns.SIZE)
+ public long size;
+
+ @Column(value = Columns.LAST_ACCESS, indexed = true)
+ public long lastAccess;
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("hash_code: ").append(hashCode).append(", ")
+ .append("content_url").append(contentUrl).append(", ")
+ .append("last_access").append(lastAccess).append(", ")
+ .append("filename").append(filename).toString();
+ }
+ }
+
+ private final class DatabaseHelper extends SQLiteOpenHelper {
+ public static final int DATABASE_VERSION = 1;
+
+ public DatabaseHelper(Context context, String dbName) {
+ super(context, dbName, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ FileEntry.SCHEMA.createTables(db);
+
+ // delete old files
+ for (File file : mRootDir.listFiles()) {
+ if (!file.delete()) {
+ Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
+ }
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ //reset everything
+ FileEntry.SCHEMA.dropTables(db);
+ onCreate(db);
+ }
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java
new file mode 100644
index 000000000..39fcf9e09
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java
@@ -0,0 +1,191 @@
+/*
+ * 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.common;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * MD5-based digest Wrapper.
+ */
+public class Fingerprint {
+ // Instance of the MessageDigest using our specified digest algorithm.
+ private static final MessageDigest DIGESTER;
+
+ /**
+ * Name of the digest algorithm we use in {@link java.security.MessageDigest}
+ */
+ private static final String DIGEST_MD5 = "md5";
+
+ // Version 1 streamId prefix.
+ // Hard coded stream id length limit is 40-chars. Don't ask!
+ private static final String STREAM_ID_CS_PREFIX = "cs_01_";
+
+ // 16 bytes for 128-bit fingerprint
+ private static final int FINGERPRINT_BYTE_LENGTH;
+
+ // length of prefix + 32 hex chars for 128-bit fingerprint
+ private static final int STREAM_ID_CS_01_LENGTH;
+
+ static {
+ try {
+ DIGESTER = MessageDigest.getInstance(DIGEST_MD5);
+ FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength();
+ STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length()
+ + (FINGERPRINT_BYTE_LENGTH * 2);
+ } catch (NoSuchAlgorithmException e) {
+ // can't continue, but really shouldn't happen
+ throw new IllegalStateException(e);
+ }
+ }
+
+ // md5 digest bytes.
+ private final byte[] mMd5Digest;
+
+ /**
+ * Creates a new Fingerprint.
+ */
+ public Fingerprint(byte[] bytes) {
+ if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) {
+ throw new IllegalArgumentException();
+ }
+ mMd5Digest = bytes;
+ }
+
+ /**
+ * Creates a Fingerprint based on the contents of a file.
+ *
+ * Note that this will close() stream after calculating the digest.
+ * @param byteCount length of original data will be stored at byteCount[0] as a side product
+ * of the fingerprint calculation
+ */
+ public static Fingerprint fromInputStream(InputStream stream, long[] byteCount)
+ throws IOException {
+ DigestInputStream in = null;
+ long count = 0;
+ try {
+ in = new DigestInputStream(stream, DIGESTER);
+ byte[] bytes = new byte[8192];
+ while (true) {
+ // scan through file to compute a fingerprint.
+ int n = in.read(bytes);
+ if (n < 0) break;
+ count += n;
+ }
+ } finally {
+ if (in != null) in.close();
+ }
+ if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count;
+ return new Fingerprint(in.getMessageDigest().digest());
+ }
+
+ /**
+ * Decodes a string stream id to a 128-bit fingerprint.
+ */
+ public static Fingerprint fromStreamId(String streamId) {
+ if ((streamId == null)
+ || !streamId.startsWith(STREAM_ID_CS_PREFIX)
+ || (streamId.length() != STREAM_ID_CS_01_LENGTH)) {
+ throw new IllegalArgumentException("bad streamId: " + streamId);
+ }
+
+ // decode the hex bytes of the fingerprint portion
+ byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH];
+ int byteIdx = 0;
+ for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH;
+ idx += 2) {
+ int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1);
+ bytes[byteIdx++] = (byte) (value & 0xff);
+ }
+ return new Fingerprint(bytes);
+ }
+
+ /**
+ * Scans a list of strings for a valid streamId.
+ *
+ * @param streamIdList list of stream id's to be scanned
+ * @return valid fingerprint or null if it can't be found
+ */
+ public static Fingerprint extractFingerprint(List<String> streamIdList) {
+ for (String streamId : streamIdList) {
+ if (streamId.startsWith(STREAM_ID_CS_PREFIX)) {
+ return fromStreamId(streamId);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Encodes a 128-bit fingerprint as a string stream id.
+ *
+ * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and
+ * underscores.
+ */
+ public String toStreamId() {
+ StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX);
+ appendHexFingerprint(streamId, mMd5Digest);
+ return streamId.toString();
+ }
+
+ public byte[] getBytes() {
+ return mMd5Digest;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Fingerprint)) return false;
+ Fingerprint other = (Fingerprint) obj;
+ return Arrays.equals(mMd5Digest, other.mMd5Digest);
+ }
+
+ public boolean equals(byte[] md5Digest) {
+ return Arrays.equals(mMd5Digest, md5Digest);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mMd5Digest);
+ }
+
+ // Utility methods.
+
+ private static int toDigit(String streamId, int index) {
+ int digit = Character.digit(streamId.charAt(index), 16);
+ if (digit < 0) {
+ throw new IllegalArgumentException("illegal hex digit in " + streamId);
+ }
+ return digit;
+ }
+
+ private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) {
+ for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) {
+ int value = bytes[idx];
+ sb.append(Integer.toHexString((value >> 4) & 0x0f));
+ sb.append(Integer.toHexString(value& 0x0f));
+ }
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java
new file mode 100644
index 000000000..cb95e3329
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java
@@ -0,0 +1,134 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.util.Log;
+
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.params.HttpParams;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Constructs {@link HttpClient} instances and isolates client code from API
+ * level differences.
+ */
+public final class HttpClientFactory {
+ // TODO: migrate GDataClient to use this util method instead of apache's
+ // DefaultHttpClient.
+ /**
+ * Creates an HttpClient with the userAgent string constructed from the
+ * package name contained in the context.
+ * @return the client
+ */
+ public static HttpClient newHttpClient(Context context) {
+ return HttpClientFactory.newHttpClient(getUserAgent(context));
+ }
+
+ /**
+ * Creates an HttpClient with the specified userAgent string.
+ * @param userAgent the userAgent string
+ * @return the client
+ */
+ public static HttpClient newHttpClient(String userAgent) {
+ // AndroidHttpClient is available on all platform releases,
+ // but is hidden until API Level 8
+ try {
+ Class<?> clazz = Class.forName("android.net.http.AndroidHttpClient");
+ Method newInstance = clazz.getMethod("newInstance", String.class);
+ Object instance = newInstance.invoke(null, userAgent);
+
+ HttpClient client = (HttpClient) instance;
+
+ // ensure we default to HTTP 1.1
+ HttpParams params = client.getParams();
+ params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
+
+ // AndroidHttpClient sets these two parameters thusly by default:
+ // HttpConnectionParams.setSoTimeout(params, 60 * 1000);
+ // HttpConnectionParams.setConnectionTimeout(params, 60 * 1000);
+
+ // however it doesn't set this one...
+ ConnManagerParams.setTimeout(params, 60 * 1000);
+
+ return client;
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Closes an HttpClient.
+ */
+ public static void close(HttpClient client) {
+ // AndroidHttpClient is available on all platform releases,
+ // but is hidden until API Level 8
+ try {
+ Class<?> clazz = client.getClass();
+ Method method = clazz.getMethod("close", (Class<?>[]) null);
+ method.invoke(client, (Object[]) null);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String sUserAgent = null;
+
+ private static String getUserAgent(Context context) {
+ if (sUserAgent == null) {
+ PackageInfo pi;
+ try {
+ pi = context.getPackageManager().getPackageInfo(
+ context.getPackageName(), 0);
+ } catch (NameNotFoundException e) {
+ throw new IllegalStateException("getPackageInfo failed");
+ }
+ sUserAgent = String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
+ pi.packageName,
+ pi.versionName,
+ Build.BRAND,
+ Build.DEVICE,
+ Build.MODEL,
+ Build.ID,
+ Build.VERSION.SDK,
+ Build.VERSION.RELEASE,
+ Build.VERSION.INCREMENTAL);
+ }
+ return sUserAgent;
+ }
+
+ private HttpClientFactory() {
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/LruCache.java b/gallerycommon/src/com/android/gallery3d/common/LruCache.java
new file mode 100644
index 000000000..81dabf773
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/LruCache.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2009 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.common;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * An LRU cache which stores recently inserted entries and all entries ever
+ * inserted which still has a strong reference elsewhere.
+ */
+public class LruCache<K, V> {
+
+ private final HashMap<K, V> mLruMap;
+ private final HashMap<K, Entry<K, V>> mWeakMap =
+ new HashMap<K, Entry<K, V>>();
+ private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+ @SuppressWarnings("serial")
+ public LruCache(final int capacity) {
+ mLruMap = new LinkedHashMap<K, V>(16, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > capacity;
+ }
+ };
+ }
+
+ private static class Entry<K, V> extends WeakReference<V> {
+ K mKey;
+
+ public Entry(K key, V value, ReferenceQueue<V> queue) {
+ super(value, queue);
+ mKey = key;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void cleanUpWeakMap() {
+ Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+ while (entry != null) {
+ mWeakMap.remove(entry.mKey);
+ entry = (Entry<K, V>) mQueue.poll();
+ }
+ }
+
+ public synchronized boolean containsKey(K key) {
+ cleanUpWeakMap();
+ return mWeakMap.containsKey(key);
+ }
+
+ public synchronized V put(K key, V value) {
+ cleanUpWeakMap();
+ mLruMap.put(key, value);
+ Entry<K, V> entry = mWeakMap.put(
+ key, new Entry<K, V>(key, value, mQueue));
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized V get(K key) {
+ cleanUpWeakMap();
+ V value = mLruMap.get(key);
+ if (value != null) return value;
+ Entry<K, V> entry = mWeakMap.get(key);
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized void clear() {
+ mLruMap.clear();
+ mWeakMap.clear();
+ mQueue = new ReferenceQueue<V>();
+ }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java
new file mode 100644
index 000000000..efe2be213
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java
@@ -0,0 +1,407 @@
+/*
+ * 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.common;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.StatFs;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.InterruptedIOException;
+import java.util.Random;
+
+public class Utils {
+ private static final String TAG = "Utils";
+ private static final String DEBUG_TAG = "GalleryDebug";
+
+ private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
+ private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
+
+ private static long[] sCrcTable = new long[256];
+
+ // Throws AssertionError if the input is false.
+ public static void assertTrue(boolean cond) {
+ if (!cond) {
+ throw new AssertionError();
+ }
+ }
+
+ // Throws AssertionError if the input is false.
+ public static void assertTrue(boolean cond, String message, Object ... args) {
+ if (!cond) {
+ throw new AssertionError(
+ args.length == 0 ? message : String.format(message, args));
+ }
+ }
+
+ // Throws NullPointerException if the input is null.
+ public static <T> T checkNotNull(T object) {
+ if (object == null) throw new NullPointerException();
+ return object;
+ }
+
+ // Returns true if two input Object are both null or equal
+ // to each other.
+ public static boolean equals(Object a, Object b) {
+ return (a == b) || (a == null ? false : a.equals(b));
+ }
+
+ // Returns true if the input is power of 2.
+ // Throws IllegalArgumentException if the input is <= 0.
+ public static boolean isPowerOf2(int n) {
+ if (n <= 0) throw new IllegalArgumentException();
+ return (n & -n) == n;
+ }
+
+ // Returns the next power of two.
+ // Returns the input if it is already power of 2.
+ // Throws IllegalArgumentException if the input is <= 0 or
+ // the answer overflows.
+ public static int nextPowerOf2(int n) {
+ if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException();
+ n -= 1;
+ n |= n >> 16;
+ n |= n >> 8;
+ n |= n >> 4;
+ n |= n >> 2;
+ n |= n >> 1;
+ return n + 1;
+ }
+
+ // Returns the previous power of two.
+ // Returns the input if it is already power of 2.
+ // Throws IllegalArgumentException if the input is <= 0
+ public static int prevPowerOf2(int n) {
+ if (n <= 0) throw new IllegalArgumentException();
+ return Integer.highestOneBit(n);
+ }
+
+ // Returns the euclidean distance between (x, y) and (sx, sy).
+ public static float distance(float x, float y, float sx, float sy) {
+ float dx = x - sx;
+ float dy = y - sy;
+ return (float) Math.hypot(dx, dy);
+ }
+
+ // Returns the input value x clamped to the range [min, max].
+ public static int clamp(int x, int min, int max) {
+ if (x > max) return max;
+ if (x < min) return min;
+ return x;
+ }
+
+ // Returns the input value x clamped to the range [min, max].
+ public static float clamp(float x, float min, float max) {
+ if (x > max) return max;
+ if (x < min) return min;
+ return x;
+ }
+
+ // Returns the input value x clamped to the range [min, max].
+ public static long clamp(long x, long min, long max) {
+ if (x > max) return max;
+ if (x < min) return min;
+ return x;
+ }
+
+ public static boolean isOpaque(int color) {
+ return color >>> 24 == 0xFF;
+ }
+
+ public static <T> void swap(T[] array, int i, int j) {
+ T temp = array[i];
+ array[i] = array[j];
+ array[j] = temp;
+ }
+
+ public static void swap(int[] array, int i, int j) {
+ int temp = array[i];
+ array[i] = array[j];
+ array[j] = temp;
+ }
+
+ /**
+ * A function thats returns a 64-bit crc for string
+ *
+ * @param in input string
+ * @return a 64-bit crc value
+ */
+ public static final long crc64Long(String in) {
+ if (in == null || in.length() == 0) {
+ return 0;
+ }
+ return crc64Long(getBytes(in));
+ }
+
+ static {
+ // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
+ long part;
+ for (int i = 0; i < 256; i++) {
+ part = i;
+ for (int j = 0; j < 8; j++) {
+ long x = ((int) part & 1) != 0 ? POLY64REV : 0;
+ part = (part >> 1) ^ x;
+ }
+ sCrcTable[i] = part;
+ }
+ }
+
+ public static final long crc64Long(byte[] buffer) {
+ long crc = INITIALCRC;
+ for (int k = 0, n = buffer.length; k < n; ++k) {
+ crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8);
+ }
+ return crc;
+ }
+
+ public static byte[] getBytes(String in) {
+ byte[] result = new byte[in.length() * 2];
+ int output = 0;
+ for (char ch : in.toCharArray()) {
+ result[output++] = (byte) (ch & 0xFF);
+ result[output++] = (byte) (ch >> 8);
+ }
+ return result;
+ }
+
+ public static void closeSilently(Closeable c) {
+ if (c == null) return;
+ try {
+ c.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "close fail", t);
+ }
+ }
+
+ public static int compare(long a, long b) {
+ return a < b ? -1 : a == b ? 0 : 1;
+ }
+
+ public static int ceilLog2(float value) {
+ int i;
+ for (i = 0; i < 31; i++) {
+ if ((1 << i) >= value) break;
+ }
+ return i;
+ }
+
+ public static int floorLog2(float value) {
+ int i;
+ for (i = 0; i < 31; i++) {
+ if ((1 << i) > value) break;
+ }
+ return i - 1;
+ }
+
+ public static void closeSilently(ParcelFileDescriptor fd) {
+ try {
+ if (fd != null) fd.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static void closeSilently(Cursor cursor) {
+ try {
+ if (cursor != null) cursor.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static float interpolateAngle(
+ float source, float target, float progress) {
+ // interpolate the angle from source to target
+ // We make the difference in the range of [-179, 180], this is the
+ // shortest path to change source to target.
+ float diff = target - source;
+ if (diff < 0) diff += 360f;
+ if (diff > 180) diff -= 360f;
+
+ float result = source + diff * progress;
+ return result < 0 ? result + 360f : result;
+ }
+
+ public static float interpolateScale(
+ float source, float target, float progress) {
+ return source + progress * (target - source);
+ }
+
+ public static String ensureNotNull(String value) {
+ return value == null ? "" : value;
+ }
+
+ // Used for debugging. Should be removed before submitting.
+ public static void debug(String format, Object ... args) {
+ if (args.length == 0) {
+ Log.d(DEBUG_TAG, format);
+ } else {
+ Log.d(DEBUG_TAG, String.format(format, args));
+ }
+ }
+
+ public static float parseFloatSafely(String content, float defaultValue) {
+ if (content == null) return defaultValue;
+ try {
+ return Float.parseFloat(content);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "invalid float: " + content, e);
+ return defaultValue;
+ }
+ }
+
+ public static int parseIntSafely(String content, int defaultValue) {
+ if (content == null) return defaultValue;
+ try {
+ return Integer.parseInt(content);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "invalid int: " + content, e);
+ return defaultValue;
+ }
+ }
+
+ public static boolean isNullOrEmpty(String exifMake) {
+ return TextUtils.isEmpty(exifMake);
+ }
+
+ public static boolean hasSpaceForSize(long size) {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return false;
+ }
+
+ String path = Environment.getExternalStorageDirectory().getPath();
+ try {
+ StatFs stat = new StatFs(path);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return false;
+ }
+
+ public static void waitWithoutInterrupt(Object object) {
+ try {
+ object.wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "unexpected interrupt: " + object);
+ }
+ }
+
+ public static void shuffle(int array[], Random random) {
+ for (int i = array.length; i > 0; --i) {
+ int t = random.nextInt(i);
+ if (t == i - 1) continue;
+ int tmp = array[i - 1];
+ array[i - 1] = array[t];
+ array[t] = tmp;
+ }
+ }
+
+ public static boolean handleInterrruptedException(Throwable e) {
+ // A helper to deal with the interrupt exception
+ // If an interrupt detected, we will setup the bit again.
+ if (e instanceof InterruptedIOException
+ || e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return String with special XML characters escaped.
+ */
+ public static String escapeXml(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, len = s.length(); i < len; ++i) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '<': sb.append("&lt;"); break;
+ case '>': sb.append("&gt;"); break;
+ case '\"': sb.append("&quot;"); break;
+ case '\'': sb.append("&#039;"); break;
+ case '&': sb.append("&amp;"); break;
+ default: sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String getUserAgent(Context context) {
+ PackageInfo packageInfo;
+ try {
+ packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ } catch (NameNotFoundException e) {
+ throw new IllegalStateException("getPackageInfo failed");
+ }
+ return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
+ packageInfo.packageName,
+ packageInfo.versionName,
+ Build.BRAND,
+ Build.DEVICE,
+ Build.MODEL,
+ Build.ID,
+ Build.VERSION.SDK,
+ Build.VERSION.RELEASE,
+ Build.VERSION.INCREMENTAL);
+ }
+
+ public static String[] copyOf(String[] source, int newSize) {
+ String[] result = new String[newSize];
+ newSize = Math.min(source.length, newSize);
+ System.arraycopy(source, 0, result, 0, newSize);
+ return result;
+ }
+
+ public static PendingIntent deserializePendingIntent(byte[] rawPendingIntent) {
+ Parcel parcel = null;
+ try {
+ if (rawPendingIntent != null) {
+ parcel = Parcel.obtain();
+ parcel.unmarshall(rawPendingIntent, 0, rawPendingIntent.length);
+ return PendingIntent.readPendingIntentOrNullFromParcel(parcel);
+ } else {
+ return null;
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("error parsing PendingIntent");
+ } finally {
+ if (parcel != null) parcel.recycle();
+ }
+ }
+
+ public static byte[] serializePendingIntent(PendingIntent pendingIntent) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, parcel);
+ return parcel.marshall();
+ } finally {
+ if (parcel != null) parcel.recycle();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java
new file mode 100644
index 000000000..cb17527b8
--- /dev/null
+++ b/src/com/android/gallery3d/anim/AlphaAnimation.java
@@ -0,0 +1,48 @@
+/*
+ * 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.anim;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.GLCanvas;
+
+public class AlphaAnimation extends CanvasAnimation {
+ private final float mStartAlpha;
+ private final float mEndAlpha;
+ private float mCurrentAlpha;
+
+ public AlphaAnimation(float from, float to) {
+ mStartAlpha = from;
+ mEndAlpha = to;
+ mCurrentAlpha = from;
+ }
+
+ @Override
+ public void apply(GLCanvas canvas) {
+ canvas.multiplyAlpha(mCurrentAlpha);
+ }
+
+ @Override
+ public int getCanvasSaveFlags() {
+ return GLCanvas.SAVE_FLAG_ALPHA;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrentAlpha = Utils.clamp(mStartAlpha
+ + (mEndAlpha - mStartAlpha) * progress, 0f, 1f);
+ }
+}
diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java
new file mode 100644
index 000000000..bd5a6cd72
--- /dev/null
+++ b/src/com/android/gallery3d/anim/Animation.java
@@ -0,0 +1,92 @@
+/*
+ * 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.anim;
+
+import com.android.gallery3d.common.Utils;
+
+import android.view.animation.Interpolator;
+
+// Animation calculates a value according to the current input time.
+//
+// 1. First we need to use setDuration(int) to set the duration of the
+// animation. The duration is in milliseconds.
+// 2. Then we should call start(). The actual start time is the first value
+// passed to calculate(long).
+// 3. Each time we want to get an animation value, we call
+// calculate(long currentTimeMillis) to ask the Animation to calculate it.
+// The parameter passed to calculate(long) should be nonnegative.
+// 4. Use get() to get that value.
+//
+// In step 3, onCalculate(float progress) is called so subclasses can calculate
+// the value according to progress (progress is a value in [0,1]).
+//
+// Before onCalculate(float) is called, There is an optional interpolator which
+// can change the progress value. The interpolator can be set by
+// setInterpolator(Interpolator). If the interpolator is used, the value passed
+// to onCalculate may be (for example, the overshoot effect).
+//
+// The isActive() method returns true after the animation start() is called and
+// before calculate is passed a value which reaches the duration of the
+// animation.
+//
+// The start() method can be called again to restart the Animation.
+//
+abstract public class Animation {
+ private static final long ANIMATION_START = -1;
+ private static final long NO_ANIMATION = -2;
+
+ private long mStartTime = NO_ANIMATION;
+ private int mDuration;
+ private Interpolator mInterpolator;
+
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ public void setDuration(int duration) {
+ mDuration = duration;
+ }
+
+ public void start() {
+ mStartTime = ANIMATION_START;
+ }
+
+ public void setStartTime(long time) {
+ mStartTime = time;
+ }
+
+ public boolean isActive() {
+ return mStartTime != NO_ANIMATION;
+ }
+
+ public void forceStop() {
+ mStartTime = NO_ANIMATION;
+ }
+
+ public boolean calculate(long currentTimeMillis) {
+ if (mStartTime == NO_ANIMATION) return false;
+ if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis;
+ int elapse = (int) (currentTimeMillis - mStartTime);
+ float x = Utils.clamp((float) elapse / mDuration, 0f, 1f);
+ Interpolator i = mInterpolator;
+ onCalculate(i != null ? i.getInterpolation(x) : x);
+ if (elapse >= mDuration) mStartTime = NO_ANIMATION;
+ return mStartTime != NO_ANIMATION;
+ }
+
+ abstract protected void onCalculate(float progress);
+}
diff --git a/src/com/android/gallery3d/anim/AnimationSet.java b/src/com/android/gallery3d/anim/AnimationSet.java
new file mode 100644
index 000000000..773cb4314
--- /dev/null
+++ b/src/com/android/gallery3d/anim/AnimationSet.java
@@ -0,0 +1,76 @@
+/*
+ * 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.anim;
+
+import com.android.gallery3d.ui.GLCanvas;
+
+import java.util.ArrayList;
+
+public class AnimationSet extends CanvasAnimation {
+
+ private final ArrayList<CanvasAnimation> mAnimations =
+ new ArrayList<CanvasAnimation>();
+ private int mSaveFlags = 0;
+
+
+ public void addAnimation(CanvasAnimation anim) {
+ mAnimations.add(anim);
+ mSaveFlags |= anim.getCanvasSaveFlags();
+ }
+
+ @Override
+ public void apply(GLCanvas canvas) {
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).apply(canvas);
+ }
+ }
+
+ @Override
+ public int getCanvasSaveFlags() {
+ return mSaveFlags;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ // DO NOTHING
+ }
+
+ @Override
+ public boolean calculate(long currentTimeMillis) {
+ boolean more = false;
+ for (CanvasAnimation anim : mAnimations) {
+ more |= anim.calculate(currentTimeMillis);
+ }
+ return more;
+ }
+
+ @Override
+ public void start() {
+ for (CanvasAnimation anim : mAnimations) {
+ anim.start();
+ }
+ }
+
+ @Override
+ public boolean isActive() {
+ for (CanvasAnimation anim : mAnimations) {
+ if (anim.isActive()) return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/gallery3d/anim/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java
new file mode 100644
index 000000000..4c8bcc825
--- /dev/null
+++ b/src/com/android/gallery3d/anim/CanvasAnimation.java
@@ -0,0 +1,25 @@
+/*
+ * 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.anim;
+
+import com.android.gallery3d.ui.GLCanvas;
+
+public abstract class CanvasAnimation extends Animation {
+
+ public abstract int getCanvasSaveFlags();
+ public abstract void apply(GLCanvas canvas);
+}
diff --git a/src/com/android/gallery3d/anim/FloatAnimation.java b/src/com/android/gallery3d/anim/FloatAnimation.java
new file mode 100644
index 000000000..1294ec2f4
--- /dev/null
+++ b/src/com/android/gallery3d/anim/FloatAnimation.java
@@ -0,0 +1,40 @@
+/*
+ * 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.anim;
+
+public class FloatAnimation extends Animation {
+
+ private final float mFrom;
+ private final float mTo;
+ private float mCurrent;
+
+ public FloatAnimation(float from, float to, int duration) {
+ mFrom = from;
+ mTo = to;
+ mCurrent = from;
+ setDuration(duration);
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrent = mFrom + (mTo - mFrom) * progress;
+ }
+
+ public float get() {
+ return mCurrent;
+ }
+}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
new file mode 100644
index 000000000..d0d7b0fad
--- /dev/null
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -0,0 +1,198 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+public class AbstractGalleryActivity extends Activity implements GalleryActivity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AbstractGalleryActivity";
+ private GLRootView mGLRootView;
+ private StateManager mStateManager;
+ private PositionRepository mPositionRepository = new PositionRepository();
+
+ private AlertDialog mAlertDialog = null;
+ private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (getExternalCacheDir() != null) onStorageReady();
+ }
+ };
+ private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ mGLRootView.lockRenderThread();
+ try {
+ super.onSaveInstanceState(outState);
+ getStateManager().saveState(outState);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ public ImageCacheService getImageCacheService() {
+ return ((GalleryApp) getApplication()).getImageCacheService();
+ }
+
+ public DataManager getDataManager() {
+ return ((GalleryApp) getApplication()).getDataManager();
+ }
+
+ public ThreadPool getThreadPool() {
+ return ((GalleryApp) getApplication()).getThreadPool();
+ }
+
+ public GalleryApp getGalleryApplication() {
+ return (GalleryApp) getApplication();
+ }
+
+ public synchronized StateManager getStateManager() {
+ if (mStateManager == null) {
+ mStateManager = new StateManager(this);
+ }
+ return mStateManager;
+ }
+
+ public GLRoot getGLRoot() {
+ return mGLRootView;
+ }
+
+ public PositionRepository getPositionRepository() {
+ return mPositionRepository;
+ }
+
+ @Override
+ public void setContentView(int resId) {
+ super.setContentView(resId);
+ mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
+ }
+
+ public int getActionBarHeight() {
+ ActionBar actionBar = getActionBar();
+ return actionBar != null ? actionBar.getHeight() : 0;
+ }
+
+ protected void onStorageReady() {
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ unregisterReceiver(mMountReceiver);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getExternalCacheDir() == null) {
+ OnCancelListener onCancel = new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ };
+ OnClickListener onClick = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ };
+ mAlertDialog = new AlertDialog.Builder(this)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle("No Storage")
+ .setMessage("No external storage available.")
+ .setNegativeButton(android.R.string.cancel, onClick)
+ .setOnCancelListener(onCancel)
+ .show();
+ registerReceiver(mMountReceiver, mMountFilter);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mAlertDialog != null) {
+ unregisterReceiver(mMountReceiver);
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().resume();
+ getDataManager().resume();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ mGLRootView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mGLRootView.onPause();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().pause();
+ getDataManager().pause();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().notifyActivityResult(
+ requestCode, resultCode, data);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public GalleryActionBar getGalleryActionBar() {
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
new file mode 100644
index 000000000..bfacc5484
--- /dev/null
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -0,0 +1,136 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.ui.GLView;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+
+abstract public class ActivityState {
+ public static final int FLAG_HIDE_ACTION_BAR = 1;
+ public static final int FLAG_HIDE_STATUS_BAR = 2;
+
+ protected GalleryActivity mActivity;
+ protected Bundle mData;
+ protected int mFlags;
+
+ protected ResultEntry mReceivedResults;
+ protected ResultEntry mResult;
+
+ protected static class ResultEntry {
+ public int requestCode;
+ public int resultCode = Activity.RESULT_CANCELED;
+ public Intent resultData;
+ ResultEntry next;
+ }
+
+ protected ActivityState() {
+ }
+
+ protected void setContentPane(GLView content) {
+ mActivity.getGLRoot().setContentPane(content);
+ }
+
+ void initialize(GalleryActivity activity, Bundle data) {
+ mActivity = activity;
+ mData = data;
+ }
+
+ public Bundle getData() {
+ return mData;
+ }
+
+ protected void onBackPressed() {
+ mActivity.getStateManager().finishState(this);
+ }
+
+ protected void setStateResult(int resultCode, Intent data) {
+ if (mResult == null) return;
+ mResult.resultCode = resultCode;
+ mResult.resultData = data;
+ }
+
+ protected void onSaveState(Bundle outState) {
+ }
+
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ protected void onCreate(Bundle data, Bundle storedState) {
+ }
+
+ protected void onPause() {
+ }
+
+ // should only be called by StateManager
+ void resume() {
+ Activity activity = (Activity) mActivity;
+ ActionBar actionBar = activity.getActionBar();
+ if (actionBar != null) {
+ if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) {
+ actionBar.hide();
+ } else {
+ actionBar.show();
+ }
+ int stateCount = mActivity.getStateManager().getStateCount();
+ actionBar.setDisplayOptions(
+ stateCount == 1 ? 0 : ActionBar.DISPLAY_HOME_AS_UP,
+ ActionBar.DISPLAY_HOME_AS_UP);
+ }
+
+ activity.invalidateOptionsMenu();
+
+ if ((mFlags & FLAG_HIDE_STATUS_BAR) != 0) {
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.STATUS_BAR_HIDDEN;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ } else {
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.STATUS_BAR_VISIBLE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ }
+
+ ResultEntry entry = mReceivedResults;
+ if (entry != null) {
+ mReceivedResults = null;
+ onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
+ }
+ onResume();
+ }
+
+ // a subclass of ActivityState should override the method to resume itself
+ protected void onResume() {
+ }
+
+ protected boolean onCreateActionBar(Menu menu) {
+ return false;
+ }
+
+ protected boolean onItemSelected(MenuItem item) {
+ return false;
+ }
+
+ protected void onDestroy() {
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataAdapter.java
new file mode 100644
index 000000000..9934cf88c
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumDataAdapter.java
@@ -0,0 +1,367 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumDataAdapter implements AlbumView.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumDataAdapter";
+ private static final int DATA_CACHE_SIZE = 1000;
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ private static final int MIN_LOAD_COUNT = 32;
+ private static final int MAX_LOAD_COUNT = 64;
+
+ private final MediaItem[] mData;
+ private final long[] mItemVersion;
+ private final long[] mSetVersion;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private final MediaSet mSource;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+ private final Handler mMainHandler;
+ private int mSize = 0;
+
+ private AlbumView.ModelListener mModelListener;
+ private MySourceListener mSourceListener = new MySourceListener();
+ private LoadingListener mLoadingListener;
+
+ private ReloadTask mReloadTask;
+
+ public AlbumDataAdapter(GalleryActivity context, MediaSet mediaSet) {
+ mSource = mediaSet;
+
+ mData = new MediaItem[DATA_CACHE_SIZE];
+ mItemVersion = new long[DATA_CACHE_SIZE];
+ mSetVersion = new long[DATA_CACHE_SIZE];
+ Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+ Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START:
+ if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+ return;
+ case MSG_LOAD_FINISH:
+ if (mLoadingListener != null) mLoadingListener.onLoadingFinished();
+ return;
+ }
+ }
+ };
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public MediaItem get(int index) {
+ if (!isActive(index)) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ return mData[index % mData.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public int getActiveEnd() {
+ return mActiveEnd;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+ int end = mContentEnd;
+ int start = mContentStart;
+
+ // We need change the content window before calling reloadData(...)
+ synchronized (this) {
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+ MediaItem[] data = mData;
+ long[] itemVersion = mItemVersion;
+ long[] setVersion = mSetVersion;
+ if (contentStart >= end || start >= contentEnd) {
+ for (int i = start, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ } else {
+ for (int i = start; i < contentStart; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ for (int i = contentEnd, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ }
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (start == mActiveStart && end == mActiveEnd) return;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ Utils.assertTrue(start <= end
+ && end - start <= mData.length && end <= mSize);
+
+ int length = mData.length;
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+ 0, Math.max(0, mSize - length));
+ int contentEnd = Math.min(contentStart + length, mSize);
+ if (mContentStart > start || mContentEnd < end
+ || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+ setContentWindow(contentStart, contentEnd);
+ }
+ }
+
+ private class MySourceListener implements ContentListener {
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(AlbumView.ModelListener listener) {
+ mModelListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int reloadStart;
+ public int reloadCount;
+
+ public int size;
+ public ArrayList<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ public UpdateInfo call() throws Exception {
+ UpdateInfo info = new UpdateInfo();
+ long version = mVersion;
+ info.version = mSourceVersion;
+ info.size = mSize;
+ long setVersion[] = mSetVersion;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ if (setVersion[index] != version) {
+ info.reloadStart = i;
+ info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
+ return info;
+ }
+ }
+ return mSourceVersion == mVersion ? null : info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+
+ private UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mModelListener != null) mModelListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ ArrayList<MediaItem> items = info.items;
+
+ if (items == null) return null;
+ int start = Math.max(info.reloadStart, mContentStart);
+ int end = Math.min(info.reloadStart + items.size(), mContentEnd);
+
+ for (int i = start; i < end; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ mSetVersion[index] = info.version;
+ MediaItem updateItem = items.get(i - info.reloadStart);
+ long itemVersion = updateItem.getDataVersion();
+ if (mItemVersion[index] != itemVersion) {
+ mItemVersion[index] = itemVersion;
+ mData[index] = updateItem;
+ if (mModelListener != null && i >= mActiveStart && i < mActiveEnd) {
+ mModelListener.onWindowContentChanged(i);
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /*
+ * The thread model of ReloadTask
+ * *
+ * [Reload Task] [Main Thread]
+ * | |
+ * getUpdateInfo() --> | (synchronous call)
+ * (wait) <---- getUpdateInfo()
+ * | |
+ * Load Data |
+ * | |
+ * updateContent() --> | (synchronous call)
+ * (wait) updateContent()
+ * | |
+ * | |
+ */
+ private class ReloadTask extends Thread {
+
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+ private boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+ long version;
+ synchronized (DataManager.LOCK) {
+ version = mSource.reload();
+ }
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ synchronized (DataManager.LOCK) {
+ if (info.version != version) {
+ info.size = mSource.getMediaItemCount();
+ info.version = version;
+ }
+ if (info.reloadCount > 0) {
+ info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
+ }
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ updateLoading(false);
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
new file mode 100644
index 000000000..5c09ce2d2
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -0,0 +1,602 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MtpDevice;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.GridDrawer;
+import com.android.gallery3d.ui.HighlightDrawer;
+import com.android.gallery3d.ui.PositionProvider;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.widget.Toast;
+
+import java.util.Random;
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+ SelectionManager.SelectionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumPage";
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_SET_CENTER = "set-center";
+ public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
+ public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_PHOTO = 2;
+ private static final int REQUEST_DO_ANIMATION = 3;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private boolean mIsActive = false;
+ private StaticBackground mStaticBackground;
+ private AlbumView mAlbumView;
+ private Path mMediaSetPath;
+
+ private AlbumDataAdapter mAlbumDataAdapter;
+
+ protected SelectionManager mSelectionManager;
+ private GridDrawer mGridDrawer;
+ private HighlightDrawer mHighlightDrawer;
+
+ private boolean mGetContent;
+ private boolean mShowClusterMenu;
+
+ private ActionMode mActionMode;
+ private ActionModeHandler mActionModeHandler;
+ private int mFocusIndex = 0;
+ private DetailsWindow mDetailsWindow;
+ private MediaSet mMediaSet;
+ private boolean mShowDetails;
+ private float mUserDistance; // in pixel
+
+ private ProgressDialog mProgressDialog;
+ private Future<?> mPendingTask;
+
+ private Future<Void> mSyncTask = null;
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int detailLeft = right - left - width;
+ slotViewRight = detailLeft;
+ mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width,
+ bottom - top);
+ } else {
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ }
+
+ mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+ PositionRepository.getInstance(mActivity).setOffset(
+ 0, slotViewTop);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ super.onBackPressed();
+ }
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) {
+ Log.w(TAG, "item not ready yet, ignore the click");
+ return;
+ }
+ if (mShowDetails) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ mDetailsWindow.reloadDetails(slotIndex);
+ } else if (!mSelectionManager.inSelectionMode()) {
+ if (mGetContent) {
+ onGetContent(item);
+ } else {
+ boolean playVideo =
+ (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
+ if (playVideo) {
+ // Play the video.
+ PhotoPage.playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
+ } else {
+ // Get into the PhotoPage.
+ Bundle data = new Bundle();
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mMediaSetPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+ item.getPath().toString());
+ mActivity.getStateManager().startStateForResult(
+ PhotoPage.class, REQUEST_PHOTO, data);
+ }
+ }
+ } else {
+ mSelectionManager.toggle(item.getPath());
+ mAlbumView.invalidate();
+ }
+ }
+
+ private void onGetContent(final MediaItem item) {
+ DataManager dm = mActivity.getDataManager();
+ Activity activity = (Activity) mActivity;
+ if (mData.getString(Gallery.EXTRA_CROP) != null) {
+ // TODO: Handle MtpImagew
+ Uri uri = dm.getContentUri(item.getPath());
+ Intent intent = new Intent(CropImage.ACTION_CROP, uri)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtras(getData());
+ if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+ intent.putExtra(CropImage.KEY_RETURN_DATA, true);
+ }
+ activity.startActivity(intent);
+ activity.finish();
+ } else {
+ activity.setResult(Activity.RESULT_OK,
+ new Intent(null, item.getContentUri()));
+ activity.finish();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent) return;
+ if (mShowDetails) {
+ onSingleTapUp(slotIndex);
+ } else {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(item.getPath());
+ mAlbumView.invalidate();
+ }
+ }
+
+ public void doCluster(int clusterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.newClusterPath(basePath, clusterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ if (mShowClusterMenu) {
+ Context context = mActivity.getAndroidContext();
+ data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName());
+ data.putString(AlbumSetPage.KEY_SET_SUBTITLE,
+ GalleryActionBar.getClusterByTypeString(context, clusterType));
+ }
+
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().startStateForResult(
+ AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+ }
+
+ public void doFilter(int filterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchFilterPath(basePath, filterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, newPath);
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ }
+
+ public void onOperationComplete() {
+ mAlbumView.invalidate();
+ // TODO: enable animation
+ }
+
+ @Override
+ protected void onCreate(Bundle data, Bundle restoreState) {
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ initializeViews();
+ initializeData(data);
+ mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+ mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false);
+
+ startTransition(data);
+
+ // Enable auto-select-all for mtp album
+ if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+ mSelectionManager.selectAll();
+ }
+ }
+
+ private void startTransition() {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ mAlbumView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p != null) return p;
+ mTempPosition.set(target);
+ mTempPosition.z = 128;
+ return mTempPosition;
+ }
+ });
+ }
+
+ private void startTransition(Bundle data) {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ final int[] center = data == null
+ ? null
+ : data.getIntArray(KEY_SET_CENTER);
+ final Random random = new Random();
+ mAlbumView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p != null) return p;
+ if (center != null) {
+ random.setSeed(identity);
+ mTempPosition.set(center[0], center[1],
+ 0, random.nextInt(60) - 30, 0);
+ } else {
+ mTempPosition.set(target);
+ mTempPosition.z = 128;
+ }
+ return mTempPosition;
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mAlbumDataAdapter.resume();
+ mAlbumView.resume();
+ mActionModeHandler.resume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mAlbumDataAdapter.pause();
+ mAlbumView.pause();
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ Future<?> task = mPendingTask;
+ if (task != null) {
+ // cancel on going task
+ task.cancel();
+ task.waitDone();
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ }
+ mActionModeHandler.pause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAlbumDataAdapter != null) {
+ mAlbumDataAdapter.setLoadingListener(null);
+ }
+ }
+
+ private void initializeViews() {
+ mStaticBackground = new StaticBackground((Context) mActivity);
+ mRootPane.addComponent(mStaticBackground);
+
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mSelectionManager.setSelectionListener(this);
+ mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
+ Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity);
+ mAlbumView = new AlbumView(mActivity,
+ config.slotWidth, config.slotHeight, config.displayItemSize);
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ mRootPane.addComponent(mAlbumView);
+ mAlbumView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ AlbumPage.this.onSingleTapUp(slotIndex);
+ }
+ @Override
+ public void onLongTap(int slotIndex) {
+ AlbumPage.this.onLongTap(slotIndex);
+ }
+ });
+ mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+ mActionModeHandler.setActionModeListener(new ActionModeListener() {
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ private void initializeData(Bundle data) {
+ mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+ mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+ Utils.assertTrue(mMediaSet != null,
+ "MediaSet is null. Path = %s", mMediaSetPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumDataAdapter = new AlbumDataAdapter(mActivity, mMediaSet);
+ mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumView.setModel(mAlbumDataAdapter);
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mAlbumView.setSelectionDrawer(mHighlightDrawer);
+ mDetailsWindow.show();
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ mDetailsWindow.hide();
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = (Activity) mActivity;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflater = activity.getMenuInflater();
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS,
+ DataManager.INCLUDE_IMAGE);
+
+ actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else {
+ inflater.inflate(R.menu.album, menu);
+ actionBar.setTitle(mMediaSet.getName());
+ if (mMediaSet instanceof MtpDevice) {
+ menu.findItem(R.id.action_slideshow).setVisible(false);
+ } else {
+ menu.findItem(R.id.action_slideshow).setVisible(true);
+ }
+
+ MenuItem groupBy = menu.findItem(R.id.action_group_by);
+ FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+ if (groupBy != null) {
+ groupBy.setVisible(mShowClusterMenu);
+ }
+
+ actionBar.setTitle(mMediaSet.getName());
+ }
+ actionBar.setSubtitle(null);
+
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_select:
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+ return true;
+ case R.id.action_group_by: {
+ mActivity.getGalleryActionBar().showClusterDialog(this);
+ return true;
+ }
+ case R.id.action_slideshow: {
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH,
+ mMediaSetPath.toString());
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ mActivity.getStateManager().startStateForResult(
+ SlideshowPage.class, REQUEST_SLIDESHOW, data);
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStateResult(int request, int result, Intent data) {
+ switch (request) {
+ case REQUEST_SLIDESHOW: {
+ // data could be null, if there is no images in the album
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+ mAlbumView.setCenterIndex(mFocusIndex);
+ break;
+ }
+ case REQUEST_PHOTO: {
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(PhotoPage.KEY_INDEX_HINT, 0);
+ mAlbumView.setCenterIndex(mFocusIndex);
+ startTransition();
+ break;
+ }
+ case REQUEST_DO_ANIMATION: {
+ startTransition(null);
+ break;
+ }
+ }
+ }
+
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionMode = mActionModeHandler.startActionMode();
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionMode.finish();
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ mActionModeHandler.setTitle(String.format(format, count));
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ Utils.assertTrue(mActionMode != null);
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ mActionModeHandler.setTitle(String.format(format, count));
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+
+ @Override
+ public void onLoadingFinished() {
+ if (!mIsActive) return;
+ if (mAlbumDataAdapter.size() == 0) {
+ if (mSyncTask == null) {
+ mSyncTask = mMediaSet.requestSync();
+ }
+ if (mSyncTask.isDone()){
+ Toast.makeText((Context) mActivity,
+ R.string.empty_album, Toast.LENGTH_LONG).show();
+ mActivity.getStateManager().finishState(AlbumPage.this);
+ }
+ }
+ if (mSyncTask == null || mSyncTask.isDone()) {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ }
+ }
+ }
+
+ private class MyDetailsSource implements DetailsWindow.DetailsSource {
+ private int mIndex;
+ public int size() {
+ return mAlbumDataAdapter.size();
+ }
+
+ // If requested index is out of active window, suggest a valid index.
+ // If there is no valid index available, return -1.
+ public int findIndex(int indexHint) {
+ if (mAlbumDataAdapter.isActive(indexHint)) {
+ mIndex = indexHint;
+ } else {
+ mIndex = mAlbumDataAdapter.getActiveStart();
+ if (!mAlbumDataAdapter.isActive(mIndex)) {
+ return -1;
+ }
+ }
+ return mIndex;
+ }
+
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumDataAdapter.get(mIndex);
+ if (item != null) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
new file mode 100644
index 000000000..b86aee879
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -0,0 +1,68 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class AlbumPicker extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ public static final String KEY_ALBUM_PATH = "album-path";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.dialog_picker);
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ findViewById(R.id.cancel).setOnClickListener(this);
+ setTitle(R.string.select_album);
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+ data.putBoolean(Gallery.KEY_GET_ALBUM, true);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().getTopState().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
new file mode 100644
index 000000000..9086ddbf4
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
@@ -0,0 +1,384 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumSetDataAdapter implements AlbumSetView.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetDataAdapter";
+
+ private static final int INDEX_NONE = -1;
+
+ private static final int MIN_LOAD_COUNT = 4;
+ private static final int MAX_COVER_COUNT = 4;
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ private static final MediaItem[] EMPTY_MEDIA_ITEMS = new MediaItem[0];
+
+ private final MediaSet[] mData;
+ private final MediaItem[][] mCoverData;
+ private final long[] mItemVersion;
+ private final long[] mSetVersion;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private final MediaSet mSource;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+ private int mSize;
+
+ private AlbumSetView.ModelListener mModelListener;
+ private LoadingListener mLoadingListener;
+ private ReloadTask mReloadTask;
+
+ private final Handler mMainHandler;
+
+ private MySourceListener mSourceListener = new MySourceListener();
+
+ public AlbumSetDataAdapter(GalleryActivity activity, MediaSet albumSet, int cacheSize) {
+ mSource = Utils.checkNotNull(albumSet);
+ mCoverData = new MediaItem[cacheSize][];
+ mData = new MediaSet[cacheSize];
+ mItemVersion = new long[cacheSize];
+ mSetVersion = new long[cacheSize];
+ Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+ Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START:
+ if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+ return;
+ case MSG_LOAD_FINISH:
+ if (mLoadingListener != null) mLoadingListener.onLoadingFinished();
+ return;
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ public MediaSet getMediaSet(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ return mData[index % mData.length];
+ }
+
+ public MediaItem[] getCoverItems(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ MediaItem[] result = mCoverData[index % mCoverData.length];
+
+ // If the result is not ready yet, return an empty array
+ return result == null ? EMPTY_MEDIA_ITEMS : result;
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public int getActiveEnd() {
+ return mActiveEnd;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mCoverData[slotIndex] = null;
+ mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+ MediaItem[][] data = mCoverData;
+ int length = data.length;
+
+ int start = this.mContentStart;
+ int end = this.mContentEnd;
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+
+ if (contentStart >= end || start >= contentEnd) {
+ for (int i = start, n = end; i < n; ++i) {
+ clearSlot(i % length);
+ }
+ } else {
+ for (int i = start; i < contentStart; ++i) {
+ clearSlot(i % length);
+ }
+ for (int i = contentEnd, n = end; i < n; ++i) {
+ clearSlot(i % length);
+ }
+ }
+ mReloadTask.notifyDirty();
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (start == mActiveStart && end == mActiveEnd) return;
+
+ Utils.assertTrue(start <= end
+ && end - start <= mCoverData.length && end <= mSize);
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ int length = mCoverData.length;
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+ 0, Math.max(0, mSize - length));
+ int contentEnd = Math.min(contentStart + length, mSize);
+ if (mContentStart > start || mContentEnd < end
+ || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+ setContentWindow(contentStart, contentEnd);
+ }
+ }
+
+ private class MySourceListener implements ContentListener {
+ public void onContentDirty() {
+ mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(AlbumSetView.ModelListener listener) {
+ mModelListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private static void getRepresentativeItems(MediaSet set, int wanted,
+ ArrayList<MediaItem> result) {
+ if (set.getMediaItemCount() > 0) {
+ result.addAll(set.getMediaItem(0, wanted));
+ }
+
+ int n = set.getSubMediaSetCount();
+ for (int i = 0; i < n && wanted > result.size(); i++) {
+ MediaSet subset = set.getSubMediaSet(i);
+ double perSet = (double) (wanted - result.size()) / (n - i);
+ int m = (int) Math.ceil(perSet);
+ getRepresentativeItems(subset, m, result);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int index;
+
+ public int size;
+ public MediaSet item;
+ public MediaItem covers[];
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ private int getInvalidIndex(long version) {
+ long setVersion[] = mSetVersion;
+ int length = setVersion.length;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ int index = i % length;
+ if (setVersion[i % length] != version) return i;
+ }
+ return INDEX_NONE;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ int index = getInvalidIndex(mVersion);
+ if (index == INDEX_NONE
+ && mSourceVersion == mVersion) return null;
+ UpdateInfo info = new UpdateInfo();
+ info.version = mSourceVersion;
+ info.index = index;
+ info.size = mSize;
+ return info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+ private UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ public Void call() {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mModelListener != null) mModelListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ // Note: info.index could be INDEX_NONE, i.e., -1
+ if (info.index >= mContentStart && info.index < mContentEnd) {
+ int pos = info.index % mCoverData.length;
+ mSetVersion[pos] = info.version;
+ long itemVersion = info.item.getDataVersion();
+ if (mItemVersion[pos] == itemVersion) return null;
+ mItemVersion[pos] = itemVersion;
+ mData[pos] = info.item;
+ mCoverData[pos] = info.covers;
+ if (mModelListener != null
+ && info.index >= mActiveStart && info.index < mActiveEnd) {
+ mModelListener.onWindowContentChanged(info.index);
+ }
+ }
+ return null;
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // TODO: load active range first
+ private class ReloadTask extends Thread {
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+ private volatile boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+
+ long version;
+ synchronized (DataManager.LOCK) {
+ version = mSource.reload();
+ }
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+
+ synchronized (DataManager.LOCK) {
+ if (info.version != version) {
+ info.version = version;
+ info.size = mSource.getSubMediaSetCount();
+ }
+ if (info.index != INDEX_NONE) {
+ info.item = mSource.getSubMediaSet(info.index);
+ if (info.item == null) continue;
+ ArrayList<MediaItem> covers = new ArrayList<MediaItem>();
+ getRepresentativeItems(info.item, MAX_COVER_COUNT, covers);
+ info.covers = covers.toArray(new MediaItem[covers.size()]);
+ }
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ updateLoading(false);
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+ }
+}
+
+
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
new file mode 100644
index 000000000..688ff81f2
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -0,0 +1,586 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.settings.GallerySettings;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.GridDrawer;
+import com.android.gallery3d.ui.HighlightDrawer;
+import com.android.gallery3d.ui.PositionProvider;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.widget.Toast;
+
+public class AlbumSetPage extends ActivityState implements
+ SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
+ EyePosition.EyePositionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetPage";
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_SET_TITLE = "set-title";
+ public static final String KEY_SET_SUBTITLE = "set-subtitle";
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int REQUEST_DO_ANIMATION = 1;
+ private static final int MSG_GOTO_MANAGE_CACHE_PAGE = 1;
+
+ private boolean mIsActive = false;
+ private StaticBackground mStaticBackground;
+ private AlbumSetView mAlbumSetView;
+
+ private MediaSet mMediaSet;
+ private String mTitle;
+ private String mSubtitle;
+ private boolean mShowClusterTabs;
+
+ protected SelectionManager mSelectionManager;
+ private AlbumSetDataAdapter mAlbumSetDataAdapter;
+ private GridDrawer mGridDrawer;
+ private HighlightDrawer mHighlightDrawer;
+
+ private boolean mGetContent;
+ private boolean mGetAlbum;
+ private ActionMode mActionMode;
+ private ActionModeHandler mActionModeHandler;
+ private DetailsWindow mDetailsWindow;
+ private boolean mShowDetails;
+ private EyePosition mEyePosition;
+
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private SynchronizedHandler mHandler;
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+ mEyePosition.resetPosition();
+
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int detailLeft = right - left - width;
+ slotViewRight = detailLeft;
+ mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width,
+ bottom - top);
+ } else {
+ mAlbumSetView.setSelectionDrawer(mGridDrawer);
+ }
+
+ mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ PositionRepository.getInstance(mActivity).setOffset(
+ 0, slotViewTop);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ @Override
+ public void onEyePositionChanged(float x, float y, float z) {
+ mRootPane.lockRendering();
+ mX = x;
+ mY = y;
+ mZ = z;
+ mRootPane.unlockRendering();
+ mRootPane.invalidate();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ mAlbumSetView.savePositions(
+ PositionRepository.getInstance(mActivity));
+ super.onBackPressed();
+ }
+ }
+
+ private void savePositions(int slotIndex, int center[]) {
+ Rect offset = new Rect();
+ mRootPane.getBoundsOf(mAlbumSetView, offset);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ Rect r = mAlbumSetView.getSlotRect(slotIndex);
+ int scrollX = mAlbumSetView.getScrollX();
+ int scrollY = mAlbumSetView.getScrollY();
+ center[0] = offset.left + (r.left + r.right) / 2 - scrollX;
+ center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY;
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+
+ if (mShowDetails) {
+ Path path = targetSet.getPath();
+ mHighlightDrawer.setHighlightItem(path);
+ mDetailsWindow.reloadDetails(slotIndex);
+ } else if (!mSelectionManager.inSelectionMode()) {
+ Bundle data = new Bundle(getData());
+ String mediaPath = targetSet.getPath().toString();
+ int[] center = new int[2];
+ savePositions(slotIndex, center);
+ data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+ if (mGetAlbum && targetSet.isLeafAlbum()) {
+ Activity activity = (Activity) mActivity;
+ Intent result = new Intent()
+ .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString());
+ activity.setResult(Activity.RESULT_OK, result);
+ activity.finish();
+ } else if (targetSet.getSubMediaSetCount() > 0) {
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+ mActivity.getStateManager().startStateForResult(
+ AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+ } else {
+ if (!mGetContent && (targetSet.getSupportedOperations()
+ & MediaObject.SUPPORT_IMPORT) != 0) {
+ data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true);
+ }
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ // We only show cluster menu in the first AlbumPage in stack
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+ mActivity.getStateManager().startStateForResult(
+ AlbumPage.class, REQUEST_DO_ANIMATION, data);
+ }
+ } else {
+ mSelectionManager.toggle(targetSet.getPath());
+ mAlbumSetView.invalidate();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent || mGetAlbum) return;
+ if (mShowDetails) {
+ onSingleTapUp(slotIndex);
+ } else {
+ MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (set == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(set.getPath());
+ mAlbumSetView.invalidate();
+ }
+ }
+
+ public void doCluster(int clusterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchClusterPath(basePath, clusterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ public void doFilter(int filterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchFilterPath(basePath, filterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ public void onOperationComplete() {
+ mAlbumSetView.invalidate();
+ // TODO: enable animation
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_GOTO_MANAGE_CACHE_PAGE);
+ Bundle data = new Bundle();
+ String mediaPath = mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+ mActivity.getStateManager().startState(ManageCachePage.class, data);
+ }
+ };
+
+ initializeViews();
+ initializeData(data);
+ mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+ mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false);
+ mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE);
+ mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE);
+ mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+
+ startTransition();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mActionModeHandler.pause();
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mEyePosition.pause();
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ if (actionBar != null) actionBar.hideClusterTabs();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mAlbumSetView.resume();
+ mEyePosition.resume();
+ mActionModeHandler.resume();
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ if (mShowClusterTabs && actionBar != null) actionBar.showClusterTabs(this);
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumSetDataAdapter = new AlbumSetDataAdapter(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+ mStaticBackground = new StaticBackground(mActivity.getAndroidContext());
+ mRootPane.addComponent(mStaticBackground);
+
+ mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
+ Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity);
+ mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer,
+ config.slotWidth, config.slotHeight,
+ config.displayItemSize, config.labelFontSize,
+ config.labelOffsetY, config.labelMargin);
+ mAlbumSetView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ AlbumSetPage.this.onSingleTapUp(slotIndex);
+ }
+ @Override
+ public void onLongTap(int slotIndex) {
+ AlbumSetPage.this.onLongTap(slotIndex);
+ }
+ });
+
+ mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+ mActionModeHandler.setActionModeListener(new ActionModeListener() {
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mRootPane.addComponent(mAlbumSetView);
+
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = (Activity) mActivity;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflater = activity.getMenuInflater();
+
+ final boolean inAlbum = mActivity.getStateManager().hasStateClass(
+ AlbumPage.class);
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(
+ Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE);
+ int id = R.string.select_image;
+ if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
+ id = (typeBits & DataManager.INCLUDE_IMAGE) == 0
+ ? R.string.select_video
+ : R.string.select_item;
+ }
+ actionBar.setTitle(id);
+ } else if (mGetAlbum) {
+ inflater.inflate(R.menu.pickup, menu);
+ actionBar.setTitle(R.string.select_album);
+ } else {
+ mShowClusterTabs = !inAlbum;
+ inflater.inflate(R.menu.albumset, menu);
+ if (mTitle != null) {
+ actionBar.setTitle(mTitle);
+ } else {
+ actionBar.setTitle(activity.getApplicationInfo().labelRes);
+ }
+ MenuItem selectItem = menu.findItem(R.id.action_select);
+
+ if (selectItem != null) {
+ boolean selectAlbums = !inAlbum &&
+ actionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+ if (selectAlbums) {
+ selectItem.setTitle(R.string.select_album);
+ } else {
+ selectItem.setTitle(R.string.select_group);
+ }
+ }
+
+ MenuItem switchCamera = menu.findItem(R.id.action_camera);
+ if (switchCamera != null) {
+ switchCamera.setVisible(GalleryUtils.isCameraAvailable(activity));
+ }
+
+ actionBar.setSubtitle(mSubtitle);
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ Activity activity = (Activity) mActivity;
+ switch (item.getItemId()) {
+ case R.id.action_select:
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+ return true;
+ case R.id.action_details:
+ if (mAlbumSetDataAdapter.size() != 0) {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ } else {
+ Toast.makeText(activity,
+ activity.getText(R.string.no_albums_alert),
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ case R.id.action_camera: {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ return true;
+ }
+ case R.id.action_manage_offline: {
+ mHandler.sendEmptyMessage(MSG_GOTO_MANAGE_CACHE_PAGE);
+ return true;
+ }
+ case R.id.action_sync_picasa_albums: {
+ PicasaSource.requestSync(activity);
+ return true;
+ }
+ case R.id.action_settings: {
+ activity.startActivity(new Intent(activity, GallerySettings.class));
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_DO_ANIMATION: {
+ startTransition();
+ }
+ }
+ }
+
+ private void startTransition() {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ mAlbumSetView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p == null) {
+ p = mTempPosition;
+ p.set(target.x, target.y, 128, target.theta, 1);
+ }
+ return p;
+ }
+ });
+ }
+
+ private String getSelectedString() {
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ int count = mSelectionManager.getSelectedCount();
+ int action = actionBar.getClusterTypeAction();
+ int string = action == FilterUtils.CLUSTER_BY_ALBUM
+ ? R.plurals.number_of_albums_selected
+ : R.plurals.number_of_groups_selected;
+ String format = mActivity.getResources().getQuantityString(string, count);
+ return String.format(format, count);
+ }
+
+ public void onSelectionModeChange(int mode) {
+
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActivity.getGalleryActionBar().hideClusterTabs();
+ mActionMode = mActionModeHandler.startActionMode();
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionMode.finish();
+ mActivity.getGalleryActionBar().showClusterTabs(this);
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.setTitle(getSelectedString());
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ Utils.assertTrue(mActionMode != null);
+ mActionModeHandler.setTitle(getSelectedString());
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mAlbumSetView.setSelectionDrawer(mGridDrawer);
+ mDetailsWindow.hide();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mAlbumSetView.setSelectionDrawer(mHighlightDrawer);
+ mDetailsWindow.show();
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+
+ public void onLoadingFinished() {
+ if (!mIsActive) return;
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ if (mAlbumSetDataAdapter.size() == 0) {
+ Toast.makeText((Context) mActivity,
+ R.string.empty_album, Toast.LENGTH_LONG).show();
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ mActivity.getStateManager().finishState(AlbumSetPage.this);
+ }
+ }
+ }
+ }
+
+ private class MyDetailsSource implements DetailsWindow.DetailsSource {
+ private int mIndex;
+ public int size() {
+ return mAlbumSetDataAdapter.size();
+ }
+
+ // If requested index is out of active window, suggest a valid index.
+ // If there is no valid index available, return -1.
+ public int findIndex(int indexHint) {
+ if (mAlbumSetDataAdapter.isActive(indexHint)) {
+ mIndex = indexHint;
+ } else {
+ mIndex = mAlbumSetDataAdapter.getActiveStart();
+ if (!mAlbumSetDataAdapter.isActive(mIndex)) {
+ return -1;
+ }
+ }
+ return mIndex;
+ }
+
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
+ if (item != null) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
new file mode 100644
index 000000000..4586235f6
--- /dev/null
+++ b/src/com/android/gallery3d/app/Config.java
@@ -0,0 +1,140 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+final class Config {
+ public static class AlbumSetPage {
+ private static AlbumSetPage sInstance;
+
+ public final int slotWidth;
+ public final int slotHeight;
+ public final int displayItemSize;
+ public final int labelFontSize;
+ public final int labelOffsetY;
+ public final int labelMargin;
+
+ public static synchronized AlbumSetPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumSetPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumSetPage(Context context) {
+ Resources r = context.getResources();
+ slotWidth = r.getDimensionPixelSize(R.dimen.albumset_slot_width);
+ slotHeight = r.getDimensionPixelSize(R.dimen.albumset_slot_height);
+ displayItemSize = r.getDimensionPixelSize(R.dimen.albumset_display_item_size);
+ labelFontSize = r.getDimensionPixelSize(R.dimen.albumset_label_font_size);
+ labelOffsetY = r.getDimensionPixelSize(R.dimen.albumset_label_offset_y);
+ labelMargin = r.getDimensionPixelSize(R.dimen.albumset_label_margin);
+ }
+ }
+
+ public static class AlbumPage {
+ private static AlbumPage sInstance;
+
+ public final int slotWidth;
+ public final int slotHeight;
+ public final int displayItemSize;
+
+ public static synchronized AlbumPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumPage(Context context) {
+ Resources r = context.getResources();
+ slotWidth = r.getDimensionPixelSize(R.dimen.album_slot_width);
+ slotHeight = r.getDimensionPixelSize(R.dimen.album_slot_height);
+ displayItemSize = r.getDimensionPixelSize(R.dimen.album_display_item_size);
+ }
+ }
+
+ public static class ManageCachePage extends AlbumSetPage {
+ private static ManageCachePage sInstance;
+
+ public final int cacheBarHeight;
+ public final int cacheBarPinLeftMargin;
+ public final int cacheBarPinRightMargin;
+ public final int cacheBarButtonRightMargin;
+ public final int cacheBarFontSize;
+
+ public static synchronized ManageCachePage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new ManageCachePage(context);
+ }
+ return sInstance;
+ }
+
+ public ManageCachePage(Context context) {
+ super(context);
+ Resources r = context.getResources();
+ cacheBarHeight = r.getDimensionPixelSize(R.dimen.cache_bar_height);
+ cacheBarPinLeftMargin = r.getDimensionPixelSize(R.dimen.cache_bar_pin_left_margin);
+ cacheBarPinRightMargin = r.getDimensionPixelSize(
+ R.dimen.cache_bar_pin_right_margin);
+ cacheBarButtonRightMargin = r.getDimensionPixelSize(
+ R.dimen.cache_bar_button_right_margin);
+ cacheBarFontSize = r.getDimensionPixelSize(R.dimen.cache_bar_font_size);
+ }
+ }
+
+ public static class PhotoPage {
+ private static PhotoPage sInstance;
+
+ // These are all height values. See the comment in FilmStripView for
+ // the meaning of these values.
+ public final int filmstripTopMargin;
+ public final int filmstripMidMargin;
+ public final int filmstripBottomMargin;
+ public final int filmstripThumbSize;
+ public final int filmstripContentSize;
+ public final int filmstripGripSize;
+ public final int filmstripBarSize;
+
+ // These are width values.
+ public final int filmstripGripWidth;
+
+ public static synchronized PhotoPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new PhotoPage(context);
+ }
+ return sInstance;
+ }
+
+ public PhotoPage(Context context) {
+ Resources r = context.getResources();
+ filmstripTopMargin = r.getDimensionPixelSize(R.dimen.filmstrip_top_margin);
+ filmstripMidMargin = r.getDimensionPixelSize(R.dimen.filmstrip_mid_margin);
+ filmstripBottomMargin = r.getDimensionPixelSize(R.dimen.filmstrip_bottom_margin);
+ filmstripThumbSize = r.getDimensionPixelSize(R.dimen.filmstrip_thumb_size);
+ filmstripContentSize = r.getDimensionPixelSize(R.dimen.filmstrip_content_size);
+ filmstripGripSize = r.getDimensionPixelSize(R.dimen.filmstrip_grip_size);
+ filmstripBarSize = r.getDimensionPixelSize(R.dimen.filmstrip_bar_size);
+ filmstripGripWidth = r.getDimensionPixelSize(R.dimen.filmstrip_grip_width);
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
new file mode 100644
index 000000000..6c0a0c7eb
--- /dev/null
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -0,0 +1,850 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.BitmapTileProvider;
+import com.android.gallery3d.ui.CropView;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.InterruptableOutputStream;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.ProgressDialog;
+import android.app.WallpaperManager;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropImage extends AbstractGalleryActivity {
+ private static final String TAG = "CropImage";
+ public static final String ACTION_CROP = "com.android.camera.action.CROP";
+
+ private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
+ private static final int MAX_FILE_INDEX = 1000;
+ private static final int TILE_SIZE = 512;
+ private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
+
+ private static final int MSG_LARGE_BITMAP = 1;
+ private static final int MSG_BITMAP = 2;
+ private static final int MSG_SAVE_COMPLETE = 3;
+
+ private static final int MAX_BACKUP_IMAGE_SIZE = 320;
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+
+ public static final String KEY_RETURN_DATA = "return-data";
+ public static final String KEY_CROPPED_RECT = "cropped-rect";
+ public static final String KEY_ASPECT_X = "aspectX";
+ public static final String KEY_ASPECT_Y = "aspectY";
+ public static final String KEY_SPOTLIGHT_X = "spotlightX";
+ public static final String KEY_SPOTLIGHT_Y = "spotlightY";
+ public static final String KEY_OUTPUT_X = "outputX";
+ public static final String KEY_OUTPUT_Y = "outputY";
+ public static final String KEY_SCALE = "scale";
+ public static final String KEY_DATA = "data";
+ public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
+ public static final String KEY_OUTPUT_FORMAT = "outputFormat";
+ public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
+ public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
+
+ private static final String KEY_STATE = "state";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_LOADED = 1;
+ private static final int STATE_SAVING = 2;
+
+ public static final String DOWNLOAD_STRING = "download";
+ public static final File DOWNLOAD_BUCKET = new File(
+ Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
+
+ public static final String CROP_ACTION = "com.android.camera.action.CROP";
+
+ private int mState = STATE_INIT;
+
+ private CropView mCropView;
+
+ private boolean mDoFaceDetection = true;
+
+ private Handler mMainHandler;
+
+ // We keep the following members so that we can free them
+
+ // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces.
+ // mCropView is responsible for rotating it to the way that it is viewed by users.
+ private Bitmap mBitmap;
+ private BitmapTileProvider mBitmapTileProvider;
+ private BitmapRegionDecoder mRegionDecoder;
+ private Bitmap mBitmapInIntent;
+ private boolean mUseRegionDecoder = false;
+
+ private ProgressDialog mProgressDialog;
+ private Future<BitmapRegionDecoder> mLoadTask;
+ private Future<Bitmap> mLoadBitmapTask;
+ private Future<Intent> mSaveTask;
+
+ private MediaItem mMediaItem;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ // Initialize UI
+ setContentView(R.layout.cropimage);
+ mCropView = new CropView(this);
+ getGLRoot().setContentPane(mCropView);
+
+ mMainHandler = new SynchronizedHandler(getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_LARGE_BITMAP: {
+ mProgressDialog.dismiss();
+ onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
+ break;
+ }
+ case MSG_BITMAP: {
+ mProgressDialog.dismiss();
+ onBitmapAvailable((Bitmap) message.obj);
+ break;
+ }
+ case MSG_SAVE_COMPLETE: {
+ mProgressDialog.dismiss();
+ setResult(RESULT_OK, (Intent) message.obj);
+ finish();
+ break;
+ }
+ }
+ }
+ };
+
+ setCropParameters();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle saveState) {
+ saveState.putInt(KEY_STATE, mState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.crop, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.cancel: {
+ setResult(RESULT_CANCELED);
+ finish();
+ break;
+ }
+ case R.id.save: {
+ onSaveClicked();
+ break;
+ }
+ }
+ return true;
+ }
+
+ private class SaveOutput implements Job<Intent> {
+ private RectF mCropRect;
+
+ public SaveOutput(RectF cropRect) {
+ mCropRect = cropRect;
+ }
+
+ public Intent run(JobContext jc) {
+ RectF cropRect = mCropRect;
+ Bundle extra = getIntent().getExtras();
+
+ Rect rect = new Rect(
+ Math.round(cropRect.left), Math.round(cropRect.top),
+ Math.round(cropRect.right), Math.round(cropRect.bottom));
+
+ Intent result = new Intent();
+ result.putExtra(KEY_CROPPED_RECT, rect);
+ Bitmap cropped = null;
+ boolean outputted = false;
+ if (extra != null) {
+ Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (uri != null) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ cropped = getCroppedImage(rect);
+ if (!saveBitmapToUri(jc, cropped, uri)) return null;
+ }
+ if (extra.getBoolean(KEY_RETURN_DATA, false)) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ result.putExtra(KEY_DATA, cropped);
+ }
+ if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ if (!setAsWallpaper(jc, cropped)) return null;
+ }
+ }
+ if (!outputted) {
+ if (jc.isCancelled()) return null;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ Uri data = saveToMediaProvider(jc, cropped);
+ if (data != null) result.setData(data);
+ }
+ return result;
+ }
+ }
+
+ public static String determineCompressFormat(MediaObject obj) {
+ String compressFormat = "JPEG";
+ if (obj instanceof MediaItem) {
+ String mime = ((MediaItem) obj).getMimeType();
+ if (mime.contains("png") || mime.contains("gif")) {
+ // Set the compress format to PNG for png and gif images
+ // because they may contain alpha values.
+ compressFormat = "PNG";
+ }
+ }
+ return compressFormat;
+ }
+
+ private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
+ try {
+ WallpaperManager.getInstance(this).setBitmap(wallpaper);
+ } catch (IOException e) {
+ Log.w(TAG, "fail to set wall paper", e);
+ }
+ return true;
+ }
+
+ private File saveMedia(
+ JobContext jc, Bitmap cropped, File directory, String filename) {
+ // Try file-1.jpg, file-2.jpg, ... until we find a filename
+ // which does not exist yet.
+ File candidate = null;
+ String fileExtension = getFileExtension();
+ for (int i = 1; i < MAX_FILE_INDEX; ++i) {
+ candidate = new File(directory, filename + "-" + i + "."
+ + fileExtension);
+ try {
+ if (candidate.createNewFile()) break;
+ } catch (IOException e) {
+ Log.e(TAG, "fail to create new file: "
+ + candidate.getAbsolutePath(), e);
+ return null;
+ }
+ }
+ if (!candidate.exists() || !candidate.isFile()) {
+ throw new RuntimeException("cannot create file: " + filename);
+ }
+
+ candidate.setReadable(true, false);
+ candidate.setWritable(true, false);
+
+ try {
+ FileOutputStream fos = new FileOutputStream(candidate);
+ try {
+ saveBitmapToOutputStream(jc, cropped,
+ convertExtensionToCompressFormat(fileExtension), fos);
+ } finally {
+ fos.close();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "fail to save image: "
+ + candidate.getAbsolutePath(), e);
+ candidate.delete();
+ return null;
+ }
+
+ if (jc.isCancelled()) {
+ candidate.delete();
+ return null;
+ }
+
+ return candidate;
+ }
+
+ private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
+ if (PicasaSource.isPicasaImage(mMediaItem)) {
+ return savePicasaImage(jc, cropped);
+ } else if (mMediaItem instanceof LocalImage) {
+ return saveLocalImage(jc, cropped);
+ } else {
+ Log.w(TAG, "no output for crop image " + mMediaItem);
+ return null;
+ }
+ }
+
+ private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
+ if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
+ throw new RuntimeException("cannot create download folder");
+ }
+
+ String filename = PicasaSource.getImageTitle(mMediaItem);
+ int pos = filename.lastIndexOf('.');
+ if (pos >= 0) filename = filename.substring(0, pos);
+ File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
+ if (output == null) return null;
+
+ long now = System.currentTimeMillis() / 1000;
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
+ values.put(Images.Media.DISPLAY_NAME, output.getName());
+ values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
+ values.put(Images.Media.DATE_MODIFIED, now);
+ values.put(Images.Media.DATE_ADDED, now);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, output.getAbsolutePath());
+ values.put(Images.Media.SIZE, output.length());
+
+ double latitude = PicasaSource.getLatitude(mMediaItem);
+ double longitude = PicasaSource.getLongitude(mMediaItem);
+ if (GalleryUtils.isValidLocation(latitude, longitude)) {
+ values.put(Images.Media.LATITUDE, latitude);
+ values.put(Images.Media.LONGITUDE, longitude);
+ }
+ return getContentResolver().insert(
+ Images.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
+ LocalImage localImage = (LocalImage) mMediaItem;
+
+ File oldPath = new File(localImage.filePath);
+ File directory = new File(oldPath.getParent());
+
+ String filename = oldPath.getName();
+ int pos = filename.lastIndexOf('.');
+ if (pos >= 0) filename = filename.substring(0, pos);
+ File output = saveMedia(jc, cropped, directory, filename);
+ if (output == null) return null;
+
+ long now = System.currentTimeMillis() / 1000;
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, localImage.caption);
+ values.put(Images.Media.DISPLAY_NAME, output.getName());
+ values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
+ values.put(Images.Media.DATE_MODIFIED, now);
+ values.put(Images.Media.DATE_ADDED, now);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, output.getAbsolutePath());
+ values.put(Images.Media.SIZE, output.length());
+
+ if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
+ values.put(Images.Media.LATITUDE, localImage.latitude);
+ values.put(Images.Media.LONGITUDE, localImage.longitude);
+ }
+ return getContentResolver().insert(
+ Images.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ private boolean saveBitmapToOutputStream(
+ JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
+ // We wrap the OutputStream so that it can be interrupted.
+ final InterruptableOutputStream ios = new InterruptableOutputStream(os);
+ jc.setCancelListener(new CancelListener() {
+ public void onCancel() {
+ ios.interrupt();
+ }
+ });
+ try {
+ bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
+ if (!jc.isCancelled()) return false;
+ } finally {
+ jc.setCancelListener(null);
+ Utils.closeSilently(os);
+ }
+ return false;
+ }
+
+ private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
+ try {
+ return saveBitmapToOutputStream(jc, bitmap,
+ convertExtensionToCompressFormat(getFileExtension()),
+ getContentResolver().openOutputStream(uri));
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot write output", e);
+ }
+ return true;
+ }
+
+ private CompressFormat convertExtensionToCompressFormat(String extension) {
+ return extension.equals("png")
+ ? CompressFormat.PNG
+ : CompressFormat.JPEG;
+ }
+
+ private String getFileExtension() {
+ String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
+ String outputFormat = (requestFormat == null)
+ ? determineCompressFormat(mMediaItem)
+ : requestFormat;
+
+ outputFormat = outputFormat.toLowerCase();
+ return (outputFormat.equals("png") || outputFormat.equals("gif"))
+ ? "png" // We don't support gif compression.
+ : "jpg";
+ }
+
+ private void onSaveClicked() {
+ Bundle extra = getIntent().getExtras();
+ RectF cropRect = mCropView.getCropRectangle();
+ if (cropRect == null) return;
+ mState = STATE_SAVING;
+ int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
+ ? R.string.wallpaper
+ : R.string.saving_image;
+ mProgressDialog = ProgressDialog.show(
+ this, null, getString(messageId), true, false);
+ mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
+ new FutureListener<Intent>() {
+ public void onFutureDone(Future<Intent> future) {
+ mSaveTask = null;
+ if (future.get() == null) return;
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_SAVE_COMPLETE, future.get()));
+ }
+ });
+ }
+
+ private Bitmap getCroppedImage(Rect rect) {
+ Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
+
+ Bundle extras = getIntent().getExtras();
+ // (outputX, outputY) = the width and height of the returning bitmap.
+ int outputX = rect.width();
+ int outputY = rect.height();
+ if (extras != null) {
+ outputX = extras.getInt(KEY_OUTPUT_X, outputX);
+ outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
+ }
+
+ if (outputX * outputY > MAX_PIXEL_COUNT) {
+ float scale = (float) Math.sqrt(
+ (double) MAX_PIXEL_COUNT / outputX / outputY);
+ Log.w(TAG, "scale down the cropped image: " + scale);
+ outputX = Math.round(scale * outputX);
+ outputY = Math.round(scale * outputY);
+ }
+
+ // (rect.width() * scaleX, rect.height() * scaleY) =
+ // the size of drawing area in output bitmap
+ float scaleX = 1;
+ float scaleY = 1;
+ Rect dest = new Rect(0, 0, outputX, outputY);
+ if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
+ scaleX = (float) outputX / rect.width();
+ scaleY = (float) outputY / rect.height();
+ if (extras == null || !extras.getBoolean(
+ KEY_SCALE_UP_IF_NEEDED, false)) {
+ if (scaleX > 1f) scaleX = 1;
+ if (scaleY > 1f) scaleY = 1;
+ }
+ }
+
+ // Keep the content in the center (or crop the content)
+ int rectWidth = Math.round(rect.width() * scaleX);
+ int rectHeight = Math.round(rect.height() * scaleY);
+ dest.set(Math.round((outputX - rectWidth) / 2f),
+ Math.round((outputY - rectHeight) / 2f),
+ Math.round((outputX + rectWidth) / 2f),
+ Math.round((outputY + rectHeight) / 2f));
+
+ if (mBitmapInIntent != null) {
+ Bitmap source = mBitmapInIntent;
+ Bitmap result = Bitmap.createBitmap(
+ outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(source, rect, dest, null);
+ return result;
+ }
+
+ int rotation = mMediaItem.getRotation();
+ rotateRectangle(rect, mCropView.getImageWidth(),
+ mCropView.getImageHeight(), 360 - rotation);
+ rotateRectangle(dest, outputX, outputY, 360 - rotation);
+ if (mUseRegionDecoder) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int sample = BitmapUtils.computeSampleSizeLarger(
+ Math.max(scaleX, scaleY));
+ options.inSampleSize = sample;
+ if ((rect.width() / sample) == dest.width()
+ && (rect.height() / sample) == dest.height()
+ && rotation == 0) {
+ // To prevent concurrent access in GLThread
+ synchronized (mRegionDecoder) {
+ return mRegionDecoder.decodeRegion(rect, options);
+ }
+ }
+ Bitmap result = Bitmap.createBitmap(
+ outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ rotateCanvas(canvas, outputX, outputY, rotation);
+ drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
+ return result;
+ } else {
+ Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ rotateCanvas(canvas, outputX, outputY, rotation);
+ canvas.drawBitmap(mBitmap,
+ rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
+ return result;
+ }
+ }
+
+ private static void rotateCanvas(
+ Canvas canvas, int width, int height, int rotation) {
+ canvas.translate(width / 2, height / 2);
+ canvas.rotate(rotation);
+ if (((rotation / 90) & 0x01) == 0) {
+ canvas.translate(-width / 2, -height / 2);
+ } else {
+ canvas.translate(-height / 2, -width / 2);
+ }
+ }
+
+ private static void rotateRectangle(
+ Rect rect, int width, int height, int rotation) {
+ if (rotation == 0 || rotation == 360) return;
+
+ int w = rect.width();
+ int h = rect.height();
+ switch (rotation) {
+ case 90: {
+ rect.top = rect.left;
+ rect.left = height - rect.bottom;
+ rect.right = rect.left + h;
+ rect.bottom = rect.top + w;
+ return;
+ }
+ case 180: {
+ rect.left = width - rect.right;
+ rect.top = height - rect.bottom;
+ rect.right = rect.left + w;
+ rect.bottom = rect.top + h;
+ return;
+ }
+ case 270: {
+ rect.left = rect.top;
+ rect.top = width - rect.right;
+ rect.right = rect.left + h;
+ rect.bottom = rect.top + w;
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+
+ private void drawInTiles(Canvas canvas,
+ BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
+ int tileSize = TILE_SIZE * sample;
+ Rect tileRect = new Rect();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inSampleSize = sample;
+ canvas.translate(dest.left, dest.top);
+ canvas.scale((float) sample * dest.width() / rect.width(),
+ (float) sample * dest.height() / rect.height());
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ for (int tx = rect.left, x = 0;
+ tx < rect.right; tx += tileSize, x += TILE_SIZE) {
+ for (int ty = rect.top, y = 0;
+ ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
+ tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
+ if (tileRect.intersect(rect)) {
+ Bitmap bitmap;
+
+ // To prevent concurrent access in GLThread
+ synchronized (decoder) {
+ bitmap = decoder.decodeRegion(tileRect, options);
+ }
+ canvas.drawBitmap(bitmap, x, y, paint);
+ bitmap.recycle();
+ }
+ }
+ }
+ }
+
+ private void onBitmapRegionDecoderAvailable(
+ BitmapRegionDecoder regionDecoder) {
+
+ if (regionDecoder == null) {
+ Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ mRegionDecoder = regionDecoder;
+ mUseRegionDecoder = true;
+ mState = STATE_LOADED;
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int width = regionDecoder.getWidth();
+ int height = regionDecoder.getHeight();
+ options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
+ BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
+ mBitmap = regionDecoder.decodeRegion(
+ new Rect(0, 0, width, height), options);
+ mCropView.setDataModel(new TileImageViewAdapter(
+ mBitmap, regionDecoder), mMediaItem.getRotation());
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(mBitmap);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ }
+
+ private void onBitmapAvailable(Bitmap bitmap) {
+ if (bitmap == null) {
+ Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ mUseRegionDecoder = false;
+ mState = STATE_LOADED;
+
+ mBitmap = bitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
+ mMediaItem.getRotation());
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(bitmap);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ }
+
+ private void setCropParameters() {
+ Bundle extras = getIntent().getExtras();
+ if (extras == null)
+ return;
+ int aspectX = extras.getInt(KEY_ASPECT_X, 0);
+ int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
+ if (aspectX != 0 && aspectY != 0) {
+ mCropView.setAspectRatio((float) aspectX / aspectY);
+ }
+
+ float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
+ float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
+ if (spotlightX != 0 && spotlightY != 0) {
+ mCropView.setSpotlightRatio(spotlightX, spotlightY);
+ }
+ }
+
+ private void initializeData() {
+ Bundle extras = getIntent().getExtras();
+
+ if (extras != null) {
+ if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
+ mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
+ }
+
+ mBitmapInIntent = extras.getParcelable(KEY_DATA);
+
+ if (mBitmapInIntent != null) {
+ mBitmapTileProvider =
+ new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
+ mCropView.setDataModel(mBitmapTileProvider, 0);
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(mBitmapInIntent);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ mState = STATE_LOADED;
+ return;
+ }
+ }
+
+ mProgressDialog = ProgressDialog.show(
+ this, null, getString(R.string.loading_image), true, false);
+
+ mMediaItem = getMediaItemFromIntentData();
+ if (mMediaItem == null) return;
+
+ boolean supportedByBitmapRegionDecoder =
+ (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
+ if (supportedByBitmapRegionDecoder) {
+ mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
+ new FutureListener<BitmapRegionDecoder>() {
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mLoadTask = null;
+ BitmapRegionDecoder decoder = future.get();
+ if (future.isCancelled()) {
+ if (decoder != null) decoder.recycle();
+ return;
+ }
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_LARGE_BITMAP, decoder));
+ }
+ });
+ } else {
+ mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
+ new FutureListener<Bitmap>() {
+ public void onFutureDone(Future<Bitmap> future) {
+ mLoadBitmapTask = null;
+ Bitmap bitmap = future.get();
+ if (future.isCancelled()) {
+ if (bitmap != null) bitmap.recycle();
+ return;
+ }
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_BITMAP, bitmap));
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mState == STATE_INIT) initializeData();
+ if (mState == STATE_SAVING) onSaveClicked();
+
+ // TODO: consider to do it in GLView system
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ mCropView.resume();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ Future<BitmapRegionDecoder> loadTask = mLoadTask;
+ if (loadTask != null && !loadTask.isDone()) {
+ // load in progress, try to cancel it
+ loadTask.cancel();
+ loadTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+
+ Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
+ if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
+ // load in progress, try to cancel it
+ loadBitmapTask.cancel();
+ loadBitmapTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+
+ Future<Intent> saveTask = mSaveTask;
+ if (saveTask != null && !saveTask.isDone()) {
+ // save in progress, try to cancel it
+ saveTask.cancel();
+ saveTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ mCropView.pause();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private MediaItem getMediaItemFromIntentData() {
+ Uri uri = getIntent().getData();
+ DataManager manager = getDataManager();
+ if (uri == null) {
+ Log.w(TAG, "no data given");
+ return null;
+ }
+ Path path = manager.findPathByUri(uri);
+ if (path == null) {
+ Log.w(TAG, "cannot get path for: " + uri);
+ return null;
+ }
+ return (MediaItem) manager.getMediaObject(path);
+ }
+
+ private class LoadDataTask implements Job<BitmapRegionDecoder> {
+ MediaItem mItem;
+
+ public LoadDataTask(MediaItem item) {
+ mItem = item;
+ }
+
+ public BitmapRegionDecoder run(JobContext jc) {
+ return mItem == null ? null : mItem.requestLargeImage().run(jc);
+ }
+ }
+
+ private class LoadBitmapDataTask implements Job<Bitmap> {
+ MediaItem mItem;
+
+ public LoadBitmapDataTask(MediaItem item) {
+ mItem = item;
+ }
+ public Bitmap run(JobContext jc) {
+ return mItem == null
+ ? null
+ : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
new file mode 100644
index 000000000..ebfc52158
--- /dev/null
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -0,0 +1,68 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class DialogPicker extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.dialog_picker);
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ findViewById(R.id.cancel).setOnClickListener(this);
+
+ int typeBits = GalleryUtils.determineTypeBits(this, getIntent());
+ setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+ data.putBoolean(Gallery.KEY_GET_CONTENT, true);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(typeBits));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().getTopState().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
new file mode 100644
index 000000000..1c3aa60bb
--- /dev/null
+++ b/src/com/android/gallery3d/app/EyePosition.java
@@ -0,0 +1,218 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+public class EyePosition {
+ private static final String TAG = "EyePosition";
+
+ public interface EyePositionListener {
+ public void onEyePositionChanged(float x, float y, float z);
+ }
+
+ private static final float GYROSCOPE_THRESHOLD = 0.15f;
+ private static final float GYROSCOPE_LIMIT = 10f;
+ private static final int GYROSCOPE_SETTLE_DOWN = 15;
+ private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f;
+
+ private static final double USER_ANGEL = Math.toRadians(10);
+ private static final float USER_ANGEL_COS = (float) Math.cos(USER_ANGEL);
+ private static final float USER_ANGEL_SIN = (float) Math.sin(USER_ANGEL);
+ private static final float MAX_VIEW_RANGE = (float) 0.5;
+ private static final int NOT_STARTED = -1;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private Context mContext;
+ private EyePositionListener mListener;
+ private Display mDisplay;
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private final float mUserDistance; // in pixel
+ private final float mLimit;
+ private long mStartTime = NOT_STARTED;
+ private Sensor mSensor;
+ private PositionListener mPositionListener = new PositionListener();
+
+ private int mGyroscopeCountdown = 0;
+
+ public EyePosition(Context context, EyePositionListener listener) {
+ mContext = context;
+ mListener = listener;
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ mLimit = mUserDistance * MAX_VIEW_RANGE;
+
+ WindowManager wManager = (WindowManager) mContext
+ .getSystemService(Context.WINDOW_SERVICE);
+ mDisplay = wManager.getDefaultDisplay();
+
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ if (mSensor == null) {
+ Log.w(TAG, "no gyroscope, use accelerometer instead");
+ mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+ if (mSensor == null) {
+ Log.w(TAG, "no sensor available");
+ }
+ }
+
+ public void resetPosition() {
+ mStartTime = NOT_STARTED;
+ mX = mY = 0;
+ mZ = -mUserDistance;
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ /*
+ * We assume the user is at the following position
+ *
+ * /|\ user's eye
+ * | /
+ * -G(gravity) | /
+ * |_/
+ * / |/_____\ -Y (-y direction of device)
+ * user angel
+ */
+ private void onAccelerometerChanged(float gx, float gy, float gz) {
+
+ float x = gx, y = gy, z = gz;
+
+ switch (mDisplay.getRotation()) {
+ case Surface.ROTATION_90: x = -gy; y= gx; break;
+ case Surface.ROTATION_180: x = -gx; y = -gy; break;
+ case Surface.ROTATION_270: x = gy; y = -gx; break;
+ }
+
+ float temp = x * x + y * y + z * z;
+ float t = -y /temp;
+
+ float tx = t * x;
+ float ty = -1 + t * y;
+ float tz = t * z;
+
+ float length = (float) Math.sqrt(tx * tx + ty * ty + tz * tz);
+ float glength = (float) Math.sqrt(temp);
+
+ mX = Utils.clamp((x * USER_ANGEL_COS / glength
+ + tx * USER_ANGEL_SIN / length) * mUserDistance,
+ -mLimit, mLimit);
+ mY = -Utils.clamp((y * USER_ANGEL_COS / glength
+ + ty * USER_ANGEL_SIN / length) * mUserDistance,
+ -mLimit, mLimit);
+ mZ = (float) -Math.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private void onGyroscopeChanged(float gx, float gy, float gz) {
+ long now = SystemClock.elapsedRealtime();
+ float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy);
+ if (distance < GYROSCOPE_THRESHOLD
+ || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) {
+ --mGyroscopeCountdown;
+ mStartTime = now;
+ float limit = mUserDistance / 20f;
+ if (mX > limit || mX < -limit || mY > limit || mY < -limit) {
+ mX *= GYROSCOPE_RESTORE_FACTOR;
+ mY *= GYROSCOPE_RESTORE_FACTOR;
+ mZ = (float) -Math.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+ return;
+ }
+
+ float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ);
+ mStartTime = now;
+
+ float x = -gy, y = -gx;
+ switch (mDisplay.getRotation()) {
+ case Surface.ROTATION_90: x = -gx; y= gy; break;
+ case Surface.ROTATION_180: x = gy; y = gx; break;
+ case Surface.ROTATION_270: x = gx; y = -gy; break;
+ }
+
+ mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)),
+ -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+ mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)),
+ -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+
+ mZ = (float) -Math.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private class PositionListener implements SensorEventListener {
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ public void onSensorChanged(SensorEvent event) {
+ switch (event.sensor.getType()) {
+ case Sensor.TYPE_GYROSCOPE: {
+ onGyroscopeChanged(
+ event.values[0], event.values[1], event.values[2]);
+ break;
+ }
+ case Sensor.TYPE_ACCELEROMETER: {
+ onAccelerometerChanged(
+ event.values[0], event.values[1], event.values[2]);
+ }
+ }
+ }
+ }
+
+ public void pause() {
+ if (mSensor != null) {
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ sManager.unregisterListener(mPositionListener);
+ }
+ }
+
+ public void resume() {
+ if (mSensor != null) {
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ sManager.registerListener(mPositionListener,
+ mSensor, SensorManager.SENSOR_DELAY_GAME);
+ }
+
+ mStartTime = NOT_STARTED;
+ mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN;
+ mX = mY = 0;
+ mZ = -mUserDistance;
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+}
diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
new file mode 100644
index 000000000..9b8ea2d62
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -0,0 +1,296 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+
+// This class handles filtering and clustering.
+//
+// We allow at most only one filter operation at a time (Currently it
+// doesn't make sense to use more than one). Also each clustering operation
+// can be applied at most once. In addition, there is one more constraint
+// ("fixed set constraint") described below.
+//
+// A clustered album (not including album set) and its base sets are fixed.
+// For example,
+//
+// /cluster/{base_set}/time/7
+//
+// This set and all sets inside base_set (recursively) are fixed because
+// 1. We can not change this set to use another clustering condition (like
+// changing "time" to "location").
+// 2. Neither can we change any set in the base_set.
+// The reason is in both cases the 7th set may not exist in the new clustering.
+// ---------------------
+// newPath operation: create a new path based on a source path and put an extra
+// condition on top of it:
+//
+// T = newFilterPath(S, filterType);
+// T = newClusterPath(S, clusterType);
+//
+// Similar functions can be used to replace the current condition (if there is one).
+//
+// T = switchFilterPath(S, filterType);
+// T = switchClusterPath(S, clusterType);
+//
+// For all fixed set in the path defined above, if some clusterType and
+// filterType are already used, they cannot not be used as parameter for these
+// functions. setupMenuItems() makes sure those types cannot be selected.
+//
+public class FilterUtils {
+ private static final String TAG = "FilterUtils";
+
+ public static final int CLUSTER_BY_ALBUM = 1;
+ public static final int CLUSTER_BY_TIME = 2;
+ public static final int CLUSTER_BY_LOCATION = 4;
+ public static final int CLUSTER_BY_TAG = 8;
+ public static final int CLUSTER_BY_SIZE = 16;
+ public static final int CLUSTER_BY_FACE = 32;
+
+ public static final int FILTER_IMAGE_ONLY = 1;
+ public static final int FILTER_VIDEO_ONLY = 2;
+ public static final int FILTER_ALL = 4;
+
+ // These are indices of the return values of getAppliedFilters().
+ // The _F suffix means "fixed".
+ private static final int CLUSTER_TYPE = 0;
+ private static final int FILTER_TYPE = 1;
+ private static final int CLUSTER_TYPE_F = 2;
+ private static final int FILTER_TYPE_F = 3;
+ private static final int CLUSTER_CURRENT_TYPE = 4;
+ private static final int FILTER_CURRENT_TYPE = 5;
+
+ public static void setupMenuItems(GalleryActionBar model, Path path, boolean inAlbum) {
+ int[] result = new int[6];
+ getAppliedFilters(path, result);
+ int ctype = result[CLUSTER_TYPE];
+ int ftype = result[FILTER_TYPE];
+ int ftypef = result[FILTER_TYPE_F];
+ int ccurrent = result[CLUSTER_CURRENT_TYPE];
+ int fcurrent = result[FILTER_CURRENT_TYPE];
+
+ setMenuItemApplied(model, CLUSTER_BY_TIME,
+ (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_LOCATION,
+ (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_TAG,
+ (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_FACE,
+ (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0);
+
+ model.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0);
+
+ setMenuItemApplied(model, R.id.action_cluster_album, ctype == 0,
+ ccurrent == 0);
+
+ // A filtering is available if it's not applied, and the old filtering
+ // (if any) is not fixed.
+ setMenuItemAppliedEnabled(model, R.string.show_images_only,
+ (ftype & FILTER_IMAGE_ONLY) != 0,
+ (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_IMAGE_ONLY) != 0);
+ setMenuItemAppliedEnabled(model, R.string.show_videos_only,
+ (ftype & FILTER_VIDEO_ONLY) != 0,
+ (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_VIDEO_ONLY) != 0);
+ setMenuItemAppliedEnabled(model, R.string.show_all,
+ ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0);
+ }
+
+ // Gets the filters applied in the path.
+ private static void getAppliedFilters(Path path, int[] result) {
+ getAppliedFilters(path, result, false);
+ }
+
+ private static void getAppliedFilters(Path path, int[] result, boolean underCluster) {
+ String[] segments = path.split();
+ // Recurse into sub media sets.
+ for (int i = 0; i < segments.length; i++) {
+ if (segments[i].startsWith("{")) {
+ String[] sets = Path.splitSequence(segments[i]);
+ for (int j = 0; j < sets.length; j++) {
+ Path sub = Path.fromString(sets[j]);
+ getAppliedFilters(sub, result, underCluster);
+ }
+ }
+ }
+
+ // update current selection
+ if (segments[0].equals("cluster")) {
+ // if this is a clustered album, set underCluster to true.
+ if (segments.length == 4) {
+ underCluster = true;
+ }
+
+ int ctype = toClusterType(segments[2]);
+ result[CLUSTER_TYPE] |= ctype;
+ result[CLUSTER_CURRENT_TYPE] = ctype;
+ if (underCluster) {
+ result[CLUSTER_TYPE_F] |= ctype;
+ }
+ }
+ }
+
+ private static int toClusterType(String s) {
+ if (s.equals("time")) {
+ return CLUSTER_BY_TIME;
+ } else if (s.equals("location")) {
+ return CLUSTER_BY_LOCATION;
+ } else if (s.equals("tag")) {
+ return CLUSTER_BY_TAG;
+ } else if (s.equals("size")) {
+ return CLUSTER_BY_SIZE;
+ } else if (s.equals("face")) {
+ return CLUSTER_BY_FACE;
+ }
+ return 0;
+ }
+
+ private static void setMenuItemApplied(
+ GalleryActionBar model, int id, boolean applied, boolean updateTitle) {
+ model.setClusterItemEnabled(id, !applied);
+ }
+
+ private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) {
+ model.setClusterItemEnabled(id, enabled);
+ }
+
+ // Add a specified filter to the path.
+ public static String newFilterPath(String base, int filterType) {
+ int mediaType;
+ switch (filterType) {
+ case FILTER_IMAGE_ONLY:
+ mediaType = MediaObject.MEDIA_TYPE_IMAGE;
+ break;
+ case FILTER_VIDEO_ONLY:
+ mediaType = MediaObject.MEDIA_TYPE_VIDEO;
+ break;
+ default: /* FILTER_ALL */
+ return base;
+ }
+
+ return "/filter/mediatype/" + mediaType + "/{" + base + "}";
+ }
+
+ // Add a specified clustering to the path.
+ public static String newClusterPath(String base, int clusterType) {
+ String kind;
+ switch (clusterType) {
+ case CLUSTER_BY_TIME:
+ kind = "time";
+ break;
+ case CLUSTER_BY_LOCATION:
+ kind = "location";
+ break;
+ case CLUSTER_BY_TAG:
+ kind = "tag";
+ break;
+ case CLUSTER_BY_SIZE:
+ kind = "size";
+ break;
+ case CLUSTER_BY_FACE:
+ kind = "face";
+ break;
+ default: /* CLUSTER_BY_ALBUM */
+ return base;
+ }
+
+ return "/cluster/{" + base + "}/" + kind;
+ }
+
+ // Change the topmost filter to the specified type.
+ public static String switchFilterPath(String base, int filterType) {
+ return newFilterPath(removeOneFilterFromPath(base), filterType);
+ }
+
+ // Change the topmost clustering to the specified type.
+ public static String switchClusterPath(String base, int clusterType) {
+ return newClusterPath(removeOneClusterFromPath(base), clusterType);
+ }
+
+ // Remove the topmost clustering (if any) from the path.
+ private static String removeOneClusterFromPath(String base) {
+ boolean[] done = new boolean[1];
+ return removeOneClusterFromPath(base, done);
+ }
+
+ private static String removeOneClusterFromPath(String base, boolean[] done) {
+ if (done[0]) return base;
+
+ String[] segments = Path.split(base);
+ if (segments[0].equals("cluster")) {
+ done[0] = true;
+ return Path.splitSequence(segments[1])[0];
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < segments.length; i++) {
+ sb.append("/");
+ if (segments[i].startsWith("{")) {
+ sb.append("{");
+ String[] sets = Path.splitSequence(segments[i]);
+ for (int j = 0; j < sets.length; j++) {
+ if (j > 0) {
+ sb.append(",");
+ }
+ sb.append(removeOneClusterFromPath(sets[j], done));
+ }
+ sb.append("}");
+ } else {
+ sb.append(segments[i]);
+ }
+ }
+ return sb.toString();
+ }
+
+ // Remove the topmost filter (if any) from the path.
+ private static String removeOneFilterFromPath(String base) {
+ boolean[] done = new boolean[1];
+ return removeOneFilterFromPath(base, done);
+ }
+
+ private static String removeOneFilterFromPath(String base, boolean[] done) {
+ if (done[0]) return base;
+
+ String[] segments = Path.split(base);
+ if (segments[0].equals("filter") && segments[1].equals("mediatype")) {
+ done[0] = true;
+ return Path.splitSequence(segments[3])[0];
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < segments.length; i++) {
+ sb.append("/");
+ if (segments[i].startsWith("{")) {
+ sb.append("{");
+ String[] sets = Path.splitSequence(segments[i]);
+ for (int j = 0; j < sets.length; j++) {
+ if (j > 0) {
+ sb.append(",");
+ }
+ sb.append(removeOneFilterFromPath(sets[j], done));
+ }
+ sb.append("}");
+ } else {
+ sb.append(segments[i]);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
new file mode 100644
index 000000000..2c5263b03
--- /dev/null
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2009 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.Toast;
+
+public final class Gallery extends AbstractGalleryActivity {
+ public static final String EXTRA_SLIDESHOW = "slideshow";
+ public static final String EXTRA_CROP = "crop";
+
+ public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW";
+ public static final String KEY_GET_CONTENT = "get-content";
+ public static final String KEY_GET_ALBUM = "get-album";
+ public static final String KEY_TYPE_BITS = "type-bits";
+ public static final String KEY_MEDIA_TYPES = "mediaTypes";
+
+ private static final String TAG = "Gallery";
+ private GalleryActionBar mActionBar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setContentView(R.layout.main);
+ mActionBar = new GalleryActionBar(this);
+
+ if (savedInstanceState != null) {
+ getStateManager().restoreFromState(savedInstanceState);
+ } else {
+ initializeByIntent();
+ }
+ }
+
+ private void initializeByIntent() {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+
+ if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) {
+ startGetContent(intent);
+ } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) {
+ // We do NOT really support the PICK intent. Handle it as
+ // the GET_CONTENT. However, we need to translate the type
+ // in the intent here.
+ Log.w(TAG, "action PICK is not supported");
+ String type = Utils.ensureNotNull(intent.getType());
+ if (type.startsWith("vnd.android.cursor.dir/")) {
+ if (type.endsWith("/image")) intent.setType("image/*");
+ if (type.endsWith("/video")) intent.setType("video/*");
+ }
+ startGetContent(intent);
+ } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action)
+ || ACTION_REVIEW.equalsIgnoreCase(action)){
+ startViewAction(intent);
+ } else {
+ startDefaultPage();
+ }
+ }
+
+ public void startDefaultPage() {
+ Bundle data = new Bundle();
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ private void startGetContent(Intent intent) {
+ Bundle data = intent.getExtras() != null
+ ? new Bundle(intent.getExtras())
+ : new Bundle();
+ data.putBoolean(KEY_GET_CONTENT, true);
+ int typeBits = GalleryUtils.determineTypeBits(this, intent);
+ data.putInt(KEY_TYPE_BITS, typeBits);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(typeBits));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ private String getContentType(Intent intent) {
+ String type = intent.getType();
+ if (type != null) return type;
+
+ Uri uri = intent.getData();
+ try {
+ return getContentResolver().getType(uri);
+ } catch (Throwable t) {
+ Log.w(TAG, "get type fail", t);
+ return null;
+ }
+ }
+
+ private void startViewAction(Intent intent) {
+ Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
+ if (slideshow) {
+ getActionBar().hide();
+ DataManager manager = getDataManager();
+ Path path = manager.findPathByUri(intent.getData());
+ if (path == null || manager.getMediaObject(path)
+ instanceof MediaItem) {
+ path = Path.fromString(
+ manager.getTopSetPath(DataManager.INCLUDE_IMAGE));
+ }
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH, path.toString());
+ data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true);
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ getStateManager().startState(SlideshowPage.class, data);
+ } else {
+ Bundle data = new Bundle();
+ DataManager dm = getDataManager();
+ Uri uri = intent.getData();
+ String contentType = getContentType(intent);
+ if (contentType == null) {
+ Toast.makeText(this,
+ R.string.no_such_item, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (contentType.startsWith(
+ ContentResolver.CURSOR_DIR_BASE_TYPE)) {
+ int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0);
+ if (mediaType != 0) {
+ uri = uri.buildUpon().appendQueryParameter(
+ KEY_MEDIA_TYPES, String.valueOf(mediaType))
+ .build();
+ }
+ Path albumPath = dm.findPathByUri(uri);
+ if (albumPath != null) {
+ MediaSet mediaSet = (MediaSet) dm.getMediaObject(albumPath);
+ data.putString(AlbumPage.KEY_MEDIA_PATH, albumPath.toString());
+ getStateManager().startState(AlbumPage.class, data);
+ } else {
+ startDefaultPage();
+ }
+ } else {
+ Path itemPath = dm.findPathByUri(uri);
+ Path albumPath = dm.getDefaultSetOf(itemPath);
+ if (albumPath != null) {
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ albumPath.toString());
+ }
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+ getStateManager().startState(PhotoPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return getStateManager().createOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ return getStateManager().itemSelected(item);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().destroy();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Utils.assertTrue(getStateManager().getStateCount() > 0);
+ super.onResume();
+ }
+
+ @Override
+ public GalleryActionBar getGalleryActionBar() {
+ return mActionBar;
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
new file mode 100644
index 000000000..b9b59ee39
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -0,0 +1,218 @@
+/*
+ * 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.app;
+
+import java.util.ArrayList;
+
+import com.android.gallery3d.R;
+
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ShareActionProvider;
+
+public class GalleryActionBar implements ActionBar.TabListener {
+ private static final String TAG = "GalleryActionBar";
+
+ public interface ClusterRunner {
+ public void doCluster(int id);
+ }
+
+ private static class ActionItem {
+ public int action;
+ public boolean enabled;
+ public boolean visible;
+ public int tabTitle;
+ public int dialogTitle;
+ public int clusterBy;
+
+ public ActionItem(int action, boolean applied, boolean enabled, int title,
+ int clusterBy) {
+ this(action, applied, enabled, title, title, clusterBy);
+ }
+
+ public ActionItem(int action, boolean applied, boolean enabled, int tabTitle,
+ int dialogTitle, int clusterBy) {
+ this.action = action;
+ this.enabled = enabled;
+ this.tabTitle = tabTitle;
+ this.dialogTitle = dialogTitle;
+ this.clusterBy = clusterBy;
+ this.visible = true;
+ }
+ }
+
+ private static final ActionItem[] sClusterItems = new ActionItem[] {
+ new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums,
+ R.string.group_by_album),
+ new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false,
+ R.string.locations, R.string.location, R.string.group_by_location),
+ new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times,
+ R.string.time, R.string.group_by_time),
+ new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people,
+ R.string.group_by_faces),
+ new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags,
+ R.string.group_by_tags)
+ };
+
+ private ClusterRunner mClusterRunner;
+ private CharSequence[] mTitles;
+ private ArrayList<Integer> mActions;
+ private Context mContext;
+ private ActionBar mActionBar;
+ // We need this because ActionBar.getSelectedTab() doesn't work when
+ // ActionBar is hidden.
+ private Tab mCurrentTab;
+
+ public GalleryActionBar(Activity activity) {
+ mActionBar = activity.getActionBar();
+ mContext = activity;
+
+ for (ActionItem item : sClusterItems) {
+ mActionBar.addTab(mActionBar.newTab().setText(item.tabTitle).
+ setTag(item).setTabListener(this));
+ }
+ }
+
+ public static int getHeight(Activity activity) {
+ ActionBar actionBar = activity.getActionBar();
+ return actionBar != null ? actionBar.getHeight() : 0;
+ }
+
+ private void createDialogData() {
+ ArrayList<CharSequence> titles = new ArrayList<CharSequence>();
+ mActions = new ArrayList<Integer>();
+ for (ActionItem item : sClusterItems) {
+ if (item.enabled && item.visible) {
+ titles.add(mContext.getString(item.dialogTitle));
+ mActions.add(item.action);
+ }
+ }
+ mTitles = new CharSequence[titles.size()];
+ titles.toArray(mTitles);
+ }
+
+ public void setClusterItemEnabled(int id, boolean enabled) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == id) {
+ item.enabled = enabled;
+ return;
+ }
+ }
+ }
+
+ public void setClusterItemVisibility(int id, boolean visible) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == id) {
+ item.visible = visible;
+ return;
+ }
+ }
+ }
+
+ public int getClusterTypeAction() {
+ if (mCurrentTab != null) {
+ ActionItem item = (ActionItem) mCurrentTab.getTag();
+ return item.action;
+ }
+ // By default, it's group-by-album
+ return FilterUtils.CLUSTER_BY_ALBUM;
+ }
+
+ public static String getClusterByTypeString(Context context, int type) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == type) {
+ return context.getString(item.clusterBy);
+ }
+ }
+ return null;
+ }
+
+ public static ShareActionProvider initializeShareActionProvider(Menu menu) {
+ MenuItem item = menu.findItem(R.id.action_share);
+ ShareActionProvider shareActionProvider = null;
+ if (item != null) {
+ shareActionProvider = (ShareActionProvider) item.getActionProvider();
+ shareActionProvider.setShareHistoryFileName(
+ ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
+ }
+ return shareActionProvider;
+ }
+
+ public void showClusterTabs(ClusterRunner runner) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ mClusterRunner = runner;
+ }
+
+ public void hideClusterTabs() {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ mClusterRunner = null;
+ }
+
+ public void showClusterDialog(final ClusterRunner clusterRunner) {
+ createDialogData();
+ final ArrayList<Integer> actions = mActions;
+ new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems(
+ mTitles, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ clusterRunner.doCluster(actions.get(which).intValue());
+ }
+ }).create().show();
+ }
+
+ public void setTitle(String title) {
+ if (mActionBar != null) mActionBar.setTitle(title);
+ }
+
+ public void setTitle(int titleId) {
+ if (mActionBar != null) mActionBar.setTitle(titleId);
+ }
+
+ public void setSubtitle(String title) {
+ if (mActionBar != null) mActionBar.setSubtitle(title);
+ }
+
+ public void setNavigationMode(int mode) {
+ if (mActionBar != null) mActionBar.setNavigationMode(mode);
+ }
+
+ public int getHeight() {
+ return mActionBar == null ? 0 : mActionBar.getHeight();
+ }
+
+ @Override
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ if (mCurrentTab == tab) return;
+ mCurrentTab = tab;
+ ActionItem item = (ActionItem) tab.getTag();
+ if (mClusterRunner != null) mClusterRunner.doCluster(item.action);
+ }
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java
new file mode 100644
index 000000000..02f2f72f3
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActivity.java
@@ -0,0 +1,28 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.PositionRepository;
+
+public interface GalleryActivity extends GalleryContext {
+ public StateManager getStateManager();
+ public GLRoot getGLRoot();
+ public PositionRepository getPositionRepository();
+ public GalleryApp getGalleryApplication();
+ public GalleryActionBar getGalleryActionBar();
+}
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
new file mode 100644
index 000000000..b3a305e53
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryApp.java
@@ -0,0 +1,39 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+public interface GalleryApp {
+ public DataManager getDataManager();
+ public ImageCacheService getImageCacheService();
+ public DownloadCache getDownloadCache();
+ public ThreadPool getThreadPool();
+
+ public Context getAndroidContext();
+ public Looper getMainLooper();
+ public ContentResolver getContentResolver();
+ public Resources getResources();
+}
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
new file mode 100644
index 000000000..a11d92017
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.widget.WidgetUtils;
+
+import android.app.Application;
+import android.content.Context;
+
+import java.io.File;
+
+public class GalleryAppImpl extends Application implements GalleryApp {
+
+ private static final String DOWNLOAD_FOLDER = "download";
+ private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M
+
+ private ImageCacheService mImageCacheService;
+ private DataManager mDataManager;
+ private ThreadPool mThreadPool;
+ private DownloadCache mDownloadCache;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ GalleryUtils.initialize(this);
+ WidgetUtils.initialize(this);
+ PicasaSource.initialize(this);
+ }
+
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ public synchronized DataManager getDataManager() {
+ if (mDataManager == null) {
+ mDataManager = new DataManager(this);
+ mDataManager.initializeSourceMap();
+ }
+ return mDataManager;
+ }
+
+ public synchronized ImageCacheService getImageCacheService() {
+ if (mImageCacheService == null) {
+ mImageCacheService = new ImageCacheService(getAndroidContext());
+ }
+ return mImageCacheService;
+ }
+
+ public synchronized ThreadPool getThreadPool() {
+ if (mThreadPool == null) {
+ mThreadPool = new ThreadPool();
+ }
+ return mThreadPool;
+ }
+
+ public synchronized DownloadCache getDownloadCache() {
+ if (mDownloadCache == null) {
+ File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER);
+
+ if (!cacheDir.isDirectory()) cacheDir.mkdirs();
+
+ if (!cacheDir.isDirectory()) {
+ throw new RuntimeException(
+ "fail to create: " + cacheDir.getAbsolutePath());
+ }
+ mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY);
+ }
+ return mDownloadCache;
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
new file mode 100644
index 000000000..022b4a704
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -0,0 +1,38 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+public interface GalleryContext {
+ public ImageCacheService getImageCacheService();
+ public DataManager getDataManager();
+
+ public Context getAndroidContext();
+
+ public Looper getMainLooper();
+ public Resources getResources();
+ public ContentResolver getContentResolver();
+ public ThreadPool getThreadPool();
+}
diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java
new file mode 100644
index 000000000..ecbd798d2
--- /dev/null
+++ b/src/com/android/gallery3d/app/LoadingListener.java
@@ -0,0 +1,22 @@
+/*
+ * 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.app;
+
+public interface LoadingListener {
+ public void onLoadingStarted();
+ public void onLoadingFinished();
+}
diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java
new file mode 100644
index 000000000..07a8ea588
--- /dev/null
+++ b/src/com/android/gallery3d/app/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.app;
+
+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/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
new file mode 100644
index 000000000..a0190db77
--- /dev/null
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -0,0 +1,271 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.CacheBarView;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionDrawer;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class ManageCachePage extends ActivityState implements
+ SelectionManager.SelectionListener, CacheBarView.Listener,
+ MenuExecutor.ProgressListener, EyePosition.EyePositionListener {
+ public static final String KEY_MEDIA_PATH = "media-path";
+ private static final String TAG = "ManageCachePage";
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+ private static final int DATA_CACHE_SIZE = 256;
+
+ private StaticBackground mStaticBackground;
+ private AlbumSetView mAlbumSetView;
+
+ private MediaSet mMediaSet;
+
+ protected SelectionManager mSelectionManager;
+ protected SelectionDrawer mSelectionDrawer;
+ private AlbumSetDataAdapter mAlbumSetDataAdapter;
+ private float mUserDistance; // in pixel
+
+ private CacheBarView mCacheBar;
+
+ private EyePosition mEyePosition;
+
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private int mAlbumCountToMakeAvailableOffline;
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+ mEyePosition.resetPosition();
+
+ Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity);
+
+ ActionBar actionBar = ((Activity) mActivity).getActionBar();
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top - config.cacheBarHeight;
+
+ mAlbumSetView.layout(0, slotViewTop, right - left, slotViewBottom);
+ mCacheBar.layout(0, bottom - top - config.cacheBarHeight,
+ right - left, bottom - top);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ public void onEyePositionChanged(float x, float y, float z) {
+ mRootPane.lockRendering();
+ mX = x;
+ mY = y;
+ mZ = z;
+ mRootPane.unlockRendering();
+ mRootPane.invalidate();
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+
+ // ignore selection action if the target set does not support cache
+ // operation (like a local album).
+ if ((targetSet.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ showToastForLocalAlbum();
+ return;
+ }
+
+ Path path = targetSet.getPath();
+ boolean isFullyCached =
+ (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL);
+ boolean isSelected = mSelectionManager.isItemSelected(path);
+
+ if (!isFullyCached) {
+ // We only count the media sets that will be made available offline
+ // in this session.
+ if (isSelected) {
+ --mAlbumCountToMakeAvailableOffline;
+ } else {
+ ++mAlbumCountToMakeAvailableOffline;
+ }
+ }
+
+ long sizeOfTarget = targetSet.getCacheSize();
+ if (isFullyCached ^ isSelected) {
+ mCacheBar.increaseTargetCacheSize(-sizeOfTarget);
+ } else {
+ mCacheBar.increaseTargetCacheSize(sizeOfTarget);
+ }
+
+ mSelectionManager.toggle(path);
+ mAlbumSetView.invalidate();
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ initializeViews();
+ initializeData(data);
+ mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mCacheBar.pause();
+ mEyePosition.pause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mAlbumSetView.resume();
+ mCacheBar.resume();
+ mEyePosition.resume();
+ }
+
+ private void initializeData(Bundle data) {
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+
+ // We will always be in selection mode in this page.
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+
+ mAlbumSetDataAdapter = new AlbumSetDataAdapter(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+ mStaticBackground = new StaticBackground(mActivity.getAndroidContext());
+ mRootPane.addComponent(mStaticBackground);
+
+ mSelectionDrawer = new ManageCacheDrawer(
+ (Context) mActivity, mSelectionManager);
+ Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity);
+ mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer,
+ config.slotWidth, config.slotHeight,
+ config.displayItemSize, config.labelFontSize,
+ config.labelOffsetY, config.labelMargin);
+ mAlbumSetView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ ManageCachePage.this.onSingleTapUp(slotIndex);
+ }
+ });
+ mRootPane.addComponent(mAlbumSetView);
+
+ mCacheBar = new CacheBarView(mActivity, R.drawable.manage_bar,
+ config.cacheBarHeight,
+ config.cacheBarPinLeftMargin,
+ config.cacheBarPinRightMargin,
+ config.cacheBarButtonRightMargin,
+ config.cacheBarFontSize);
+
+ mCacheBar.setListener(this);
+ mRootPane.addComponent(mCacheBar);
+
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ public void onDoneClicked() {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ if (ids.size() == 0) {
+ onBackPressed();
+ return;
+ }
+ showToast();
+
+ MenuExecutor menuExecutor = new MenuExecutor(mActivity,
+ mSelectionManager);
+ menuExecutor.startAction(R.id.action_toggle_full_caching,
+ R.string.process_caching_requests, this);
+ }
+
+ private void showToast() {
+ if (mAlbumCountToMakeAvailableOffline > 0) {
+ Activity activity = (Activity) mActivity;
+ Toast.makeText(activity, activity.getResources().getQuantityString(
+ R.plurals.make_albums_available_offline,
+ mAlbumCountToMakeAvailableOffline),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void showToastForLocalAlbum() {
+ Activity activity = (Activity) mActivity;
+ Toast.makeText(activity, activity.getResources().getString(
+ R.string.try_to_set_local_album_available_offline),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ public void onProgressComplete(int result) {
+ onBackPressed();
+ }
+
+ public void onProgressUpdate(int index) {
+ }
+
+ public void onSelectionModeChange(int mode) {
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
new file mode 100644
index 000000000..fea364e85
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2007 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.app;
+
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+/**
+ * This activity plays a video from a specified URI.
+ */
+public class MovieActivity extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MovieActivity";
+
+ private MoviePlayer mPlayer;
+ private boolean mFinishOnCompletion;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ setContentView(R.layout.movie_view);
+ View rootView = findViewById(R.id.root);
+ Intent intent = getIntent();
+ setVideoTitle(intent);
+ mPlayer = new MoviePlayer(rootView, this, intent.getData()) {
+ @Override
+ public void onCompletion() {
+ if (mFinishOnCompletion) {
+ finish();
+ }
+ }
+ };
+ if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+ int orientation = intent.getIntExtra(
+ MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ if (orientation != getRequestedOrientation()) {
+ setRequestedOrientation(orientation);
+ }
+ }
+ mFinishOnCompletion = intent.getBooleanExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+ Window win = getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
+ win.setAttributes(winParams);
+
+ }
+
+ private void setVideoTitle(Intent intent) {
+ String title = intent.getStringExtra(Intent.EXTRA_TITLE);
+ if (title == null) {
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(intent.getData(),
+ new String[] {VideoColumns.TITLE}, null, null, null);
+ if (cursor != null && cursor.moveToNext()) {
+ title = cursor.getString(0);
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot get title from: " + intent.getDataString(), t);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+ if (title != null) getActionBar().setTitle(title);
+ }
+
+ @Override
+ public void onStart() {
+ ((AudioManager) getSystemService(AUDIO_SERVICE))
+ .requestAudioFocus(null, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ ((AudioManager) getSystemService(AUDIO_SERVICE))
+ .abandonAudioFocus(null);
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ mPlayer.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ mPlayer.onResume();
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ mPlayer.onDestroy();
+ super.onDestroy();
+ }
+}
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 000000000..423994485
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2009 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+ MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoviePlayer";
+
+ // Copied from MediaPlaybackService in the Music Player app.
+ private static final String SERVICECMD = "com.android.music.musicservicecommand";
+ private static final String CMDNAME = "command";
+ private static final String CMDPAUSE = "pause";
+
+ private Context mContext;
+ private final VideoView mVideoView;
+ private final View mProgressView;
+ private final Bookmarker mBookmarker;
+ private final Uri mUri;
+ private final Handler mHandler = new Handler();
+ private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+ private final ActionBar mActionBar;
+
+ private boolean mHasPaused;
+
+ private final Runnable mPlayingChecker = new Runnable() {
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mProgressView.setVisibility(View.GONE);
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri) {
+ mContext = movieActivity.getApplicationContext();
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+ mProgressView = rootView.findViewById(R.id.progress_indicator);
+ mBookmarker = new Bookmarker(movieActivity);
+ mActionBar = movieActivity.getActionBar();
+ mUri = videoUri;
+
+ // For streams that we expect to be slow to start up, show a
+ // progress spinner until playback starts.
+ String scheme = mUri.getScheme();
+ if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mProgressView.setVisibility(View.GONE);
+ }
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+
+ MediaController mediaController = new MediaController(movieActivity) {
+ @Override
+ public void show() {
+ super.show();
+ mActionBar.show();
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ mActionBar.hide();
+ }
+ };
+ mVideoView.setMediaController(mediaController);
+ mediaController.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ movieActivity.onBackPressed();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+ mAudioBecomingNoisyReceiver.register();
+
+ // make the video view handle keys for seeking and pausing
+ mVideoView.requestFocus();
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ movieActivity.sendBroadcast(i);
+
+ final Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ showResumeDialog(movieActivity, bookmark);
+ } else {
+ mVideoView.start();
+ }
+ }
+
+ private void showResumeDialog(Context context, final int bookmark) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.resume_playing_title);
+ builder.setMessage(String.format(
+ context.getString(R.string.resume_playing_message),
+ GalleryUtils.formatDuration(context, bookmark / 1000)));
+ builder.setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ onCompletion();
+ }
+ });
+ builder.setPositiveButton(
+ R.string.resume_playing_resume, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ mVideoView.start();
+ }
+ });
+ builder.setNegativeButton(
+ R.string.resume_playing_restart, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.start();
+ }
+ });
+ builder.show();
+ }
+
+ public void onPause() {
+ mHandler.removeCallbacksAndMessages(null);
+ mBookmarker.setBookmark(mUri, mVideoView.getCurrentPosition(),
+ mVideoView.getDuration());
+ mVideoView.suspend();
+ mHasPaused = true;
+ }
+
+ public void onResume() {
+ if (mHasPaused) {
+ Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ mVideoView.seekTo(bookmark);
+ }
+ }
+ mVideoView.resume();
+ }
+
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ mProgressView.setVisibility(View.GONE);
+ return false;
+ }
+
+ public void onCompletion(MediaPlayer mp) {
+ onCompletion();
+ }
+
+ public void onCompletion() {
+ }
+
+ private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
+
+ public void register() {
+ mContext.registerReceiver(this,
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ }
+
+ public void unregister() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mVideoView.isPlaying()) {
+ mVideoView.pause();
+ }
+ }
+ }
+}
+
+class Bookmarker {
+ private static final String TAG = "Bookmarker";
+
+ private static final String BOOKMARK_CACHE_FILE = "bookmark";
+ private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
+ private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
+ private static final int BOOKMARK_CACHE_VERSION = 1;
+
+ private static final int HALF_MINUTE = 30 * 1000;
+ private static final int TWO_MINUTES = 4 * HALF_MINUTE;
+
+ private final Context mContext;
+
+ public Bookmarker(Context context) {
+ mContext = context;
+ }
+
+ public void setBookmark(Uri uri, int bookmark, int duration) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeUTF(uri.toString());
+ dos.writeInt(bookmark);
+ dos.writeInt(duration);
+ dos.flush();
+ cache.insert(uri.hashCode(), bos.toByteArray());
+ } catch (Throwable t) {
+ Log.w(TAG, "setBookmark failed", t);
+ }
+ }
+
+ public Integer getBookmark(Uri uri) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ byte[] data = cache.lookup(uri.hashCode());
+ if (data == null) return null;
+
+ DataInputStream dis = new DataInputStream(
+ new ByteArrayInputStream(data));
+
+ String uriString = dis.readUTF(dis);
+ int bookmark = dis.readInt();
+ int duration = dis.readInt();
+
+ if (!uriString.equals(uri.toString())) {
+ return null;
+ }
+
+ if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
+ || (bookmark > (duration - HALF_MINUTE))) {
+ return null;
+ }
+ return Integer.valueOf(bookmark);
+ } catch (Throwable t) {
+ Log.w(TAG, "getBookmark failed", t);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
new file mode 100644
index 000000000..cb202a31c
--- /dev/null
+++ b/src/com/android/gallery3d/app/PackagesMonitor.java
@@ -0,0 +1,50 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+public class PackagesMonitor extends BroadcastReceiver {
+ public static final String KEY_PACKAGES_VERSION = "packages-version";
+
+ public synchronized static int getPackagesVersion(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ return prefs.getInt(KEY_PACKAGES_VERSION, 1);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ int version = prefs.getInt(KEY_PACKAGES_VERSION, 1);
+ prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit();
+
+ String action = intent.getAction();
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ PicasaSource.onPackageAdded(context, packageName);
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ PicasaSource.onPackageRemoved(context, packageName);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 000000000..c05c89a0d
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,794 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.PhotoView.ImageData;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class PhotoDataAdapter implements PhotoPage.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoDataAdapter";
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ private static final int MIN_LOAD_COUNT = 8;
+ private static final int DATA_CACHE_SIZE = 32;
+ private static final int IMAGE_CACHE_SIZE = 5;
+
+ private static final int BIT_SCREEN_NAIL = 1;
+ private static final int BIT_FULL_IMAGE = 2;
+
+ private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber();
+
+ // sImageFetchSeq is the fetching sequence for images.
+ // We want to fetch the current screennail first (offset = 0), the next
+ // screennail (offset = +1), then the previous screennail (offset = -1) etc.
+ // After all the screennail are fetched, we fetch the full images (only some
+ // of them because of we don't want to use too much memory).
+ private static ImageFetch[] sImageFetchSeq;
+
+ private static class ImageFetch {
+ int indexOffset;
+ int imageBit;
+ public ImageFetch(int offset, int bit) {
+ indexOffset = offset;
+ imageBit = bit;
+ }
+ }
+
+ static {
+ int k = 0;
+ sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3];
+ sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL);
+
+ for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+ sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL);
+ sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL);
+ }
+
+ sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE);
+ sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE);
+ sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE);
+ }
+
+ private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter();
+
+ // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image).
+ //
+ // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE
+ // entries. The valid index range are [mContentStart, mContentEnd). We keep
+ // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use
+ // (i % DATA_CACHE_SIZE) as index to the array.
+ //
+ // The valid MediaItem window size (mContentEnd - mContentStart) may be
+ // smaller than DATA_CACHE_SIZE because we only update the window and reload
+ // the MediaItems when there are significant changes to the window position
+ // (>= MIN_LOAD_COUNT).
+ private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE];
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ /*
+ * The ImageCache is a version-to-ImageEntry map. It only holds
+ * the ImageEntries in the range of [mActiveStart, mActiveEnd).
+ * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.
+ * Besides, the [mActiveStart, mActiveEnd) range must be contained
+ * within the[mContentStart, mContentEnd) range.
+ */
+ private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>();
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ // mCurrentIndex is the "center" image the user is viewing. The change of
+ // mCurrentIndex triggers the data loading and image loading.
+ private int mCurrentIndex;
+
+ // mChanges keeps the version number (of MediaItem) about the previous,
+ // current, and next image. If the version number changes, we invalidate
+ // the model. This is used after a database reload or mCurrentIndex changes.
+ private final long mChanges[] = new long[3];
+
+ private final Handler mMainHandler;
+ private final ThreadPool mThreadPool;
+
+ private final PhotoView mPhotoView;
+ private final MediaSet mSource;
+ private ReloadTask mReloadTask;
+
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+ private int mSize = 0;
+ private Path mItemPath;
+ private boolean mIsActive;
+
+ public interface DataListener extends LoadingListener {
+ public void onPhotoChanged(int index, Path item);
+ }
+
+ private DataListener mDataListener;
+
+ private final SourceListener mSourceListener = new SourceListener();
+
+ // The path of the current viewing item will be stored in mItemPath.
+ // If mItemPath is not null, mCurrentIndex is only a hint for where we
+ // can find the item. If mItemPath is null, then we use the mCurrentIndex to
+ // find the image being viewed.
+ public PhotoDataAdapter(GalleryActivity activity,
+ PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) {
+ mSource = Utils.checkNotNull(mediaSet);
+ mPhotoView = Utils.checkNotNull(view);
+ mItemPath = Utils.checkNotNull(itemPath);
+ mCurrentIndex = indexHint;
+ mThreadPool = activity.getThreadPool();
+
+ Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START: {
+ if (mDataListener != null) mDataListener.onLoadingStarted();
+ return;
+ }
+ case MSG_LOAD_FINISH: {
+ if (mDataListener != null) mDataListener.onLoadingFinished();
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+ };
+
+ updateSlidingWindow();
+ }
+
+ private long getVersion(int index) {
+ if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE;
+ if (index >= mContentStart && index < mContentEnd) {
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ if (item != null) return item.getDataVersion();
+ }
+ return MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void fireModelInvalidated() {
+ for (int i = -1; i <= 1; ++i) {
+ long current = getVersion(mCurrentIndex + i);
+ long change = mChanges[i + 1];
+ if (current != change) {
+ mPhotoView.notifyImageInvalidated(i);
+ mChanges[i + 1] = current;
+ }
+ }
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ private void updateScreenNail(long version, Future<Bitmap> future) {
+ ImageEntry entry = mImageCache.get(version);
+ if (entry == null || entry.screenNailTask == null) {
+ Bitmap screenNail = future.get();
+ if (screenNail != null) screenNail.recycle();
+ return;
+ }
+ entry.screenNailTask = null;
+ entry.screenNail = future.get();
+
+ if (entry.screenNail == null) {
+ entry.failToLoad = true;
+ } else {
+ for (int i = -1; i <=1; ++i) {
+ if (version == getVersion(mCurrentIndex + i)) {
+ if (i == 0) updateTileProvider(entry);
+ mPhotoView.notifyImageInvalidated(i);
+ }
+ }
+ }
+ updateImageRequests();
+ }
+
+ private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
+ ImageEntry entry = mImageCache.get(version);
+ if (entry == null || entry.fullImageTask == null) {
+ BitmapRegionDecoder fullImage = future.get();
+ if (fullImage != null) fullImage.recycle();
+ return;
+ }
+ entry.fullImageTask = null;
+ entry.fullImage = future.get();
+ if (entry.fullImage != null) {
+ if (version == getVersion(mCurrentIndex)) {
+ updateTileProvider(entry);
+ mPhotoView.notifyImageInvalidated(0);
+ }
+ }
+ updateImageRequests();
+ }
+
+ public void resume() {
+ mIsActive = true;
+ mSource.addContentListener(mSourceListener);
+ updateImageCache();
+ updateImageRequests();
+
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+
+ mPhotoView.notifyModelInvalidated();
+ }
+
+ public void pause() {
+ mIsActive = false;
+
+ mReloadTask.terminate();
+ mReloadTask = null;
+
+ mSource.removeContentListener(mSourceListener);
+
+ for (ImageEntry entry : mImageCache.values()) {
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ }
+ mImageCache.clear();
+ mTileProvider.clear();
+ }
+
+ private ImageData getImage(int index) {
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ ImageEntry entry = mImageCache.get(getVersion(index));
+ Bitmap screennail = entry == null ? null : entry.screenNail;
+ if (screennail != null) {
+ return new ImageData(screennail, entry.rotation);
+ } else {
+ return new ImageData(null, 0);
+ }
+ }
+
+ public ImageData getPreviousImage() {
+ return getImage(mCurrentIndex - 1);
+ }
+
+ public ImageData getNextImage() {
+ return getImage(mCurrentIndex + 1);
+ }
+
+ private void updateCurrentIndex(int index) {
+ mCurrentIndex = index;
+ updateSlidingWindow();
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ mItemPath = item == null ? null : item.getPath();
+
+ updateImageCache();
+ updateImageRequests();
+ updateTileProvider();
+ mPhotoView.notifyOnNewImage();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(index, mItemPath);
+ }
+ fireModelInvalidated();
+ }
+
+ public void next() {
+ updateCurrentIndex(mCurrentIndex + 1);
+ }
+
+ public void previous() {
+ updateCurrentIndex(mCurrentIndex - 1);
+ }
+
+ public void jumpTo(int index) {
+ if (mCurrentIndex == index) return;
+ updateCurrentIndex(index);
+ }
+
+ public Bitmap getBackupImage() {
+ return mTileProvider.getBackupImage();
+ }
+
+ public int getImageHeight() {
+ return mTileProvider.getImageHeight();
+ }
+
+ public int getImageWidth() {
+ return mTileProvider.getImageWidth();
+ }
+
+ public int getImageRotation() {
+ ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+ return entry == null ? 0 : entry.rotation;
+ }
+
+ public int getLevelCount() {
+ return mTileProvider.getLevelCount();
+ }
+
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ return mTileProvider.getTile(level, x, y, tileSize);
+ }
+
+ public boolean isFailedToLoad() {
+ return mTileProvider.isFailedToLoad();
+ }
+
+ public boolean isEmpty() {
+ return mSize == 0;
+ }
+
+ public int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ public MediaItem getCurrentMediaItem() {
+ return mData[mCurrentIndex % DATA_CACHE_SIZE];
+ }
+
+ public void setCurrentPhoto(Path path, int indexHint) {
+ if (mItemPath == path) return;
+ mItemPath = path;
+ mCurrentIndex = indexHint;
+ updateSlidingWindow();
+ updateImageCache();
+ fireModelInvalidated();
+
+ // We need to reload content if the path doesn't match.
+ MediaItem item = getCurrentMediaItem();
+ if (item != null && item.getPath() != path) {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private void updateTileProvider() {
+ ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+ if (entry == null) { // in loading
+ mTileProvider.clear();
+ } else {
+ updateTileProvider(entry);
+ }
+ }
+
+ private void updateTileProvider(ImageEntry entry) {
+ Bitmap screenNail = entry.screenNail;
+ BitmapRegionDecoder fullImage = entry.fullImage;
+ if (screenNail != null) {
+ if (fullImage != null) {
+ mTileProvider.setBackupImage(screenNail,
+ fullImage.getWidth(), fullImage.getHeight());
+ mTileProvider.setRegionDecoder(fullImage);
+ } else {
+ int width = screenNail.getWidth();
+ int height = screenNail.getHeight();
+ mTileProvider.setBackupImage(screenNail, width, height);
+ }
+ } else {
+ mTileProvider.clear();
+ if (entry.failToLoad) mTileProvider.setFailedToLoad();
+ }
+ }
+
+ private void updateSlidingWindow() {
+ // 1. Update the image window
+ int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
+ 0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
+ int end = Math.min(mSize, start + IMAGE_CACHE_SIZE);
+
+ if (mActiveStart == start && mActiveEnd == end) return;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // 2. Update the data window
+ start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
+ 0, Math.max(0, mSize - DATA_CACHE_SIZE));
+ end = Math.min(mSize, start + DATA_CACHE_SIZE);
+ if (mContentStart > mActiveStart || mContentEnd < mActiveEnd
+ || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) {
+ for (int i = mContentStart; i < mContentEnd; ++i) {
+ if (i < start || i >= end) {
+ mData[i % DATA_CACHE_SIZE] = null;
+ }
+ }
+ mContentStart = start;
+ mContentEnd = end;
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private void updateImageRequests() {
+ if (!mIsActive) return;
+
+ int currentIndex = mCurrentIndex;
+ MediaItem item = mData[currentIndex % DATA_CACHE_SIZE];
+ if (item == null || item.getPath() != mItemPath) {
+ // current item mismatch - don't request image
+ return;
+ }
+
+ // 1. Find the most wanted request and start it (if not already started).
+ Future<?> task = null;
+ for (int i = 0; i < sImageFetchSeq.length; i++) {
+ int offset = sImageFetchSeq[i].indexOffset;
+ int bit = sImageFetchSeq[i].imageBit;
+ task = startTaskIfNeeded(currentIndex + offset, bit);
+ if (task != null) break;
+ }
+
+ // 2. Cancel everything else.
+ for (ImageEntry entry : mImageCache.values()) {
+ if (entry.screenNailTask != null && entry.screenNailTask != task) {
+ entry.screenNailTask.cancel();
+ entry.screenNailTask = null;
+ entry.requestedBits &= ~BIT_SCREEN_NAIL;
+ }
+ if (entry.fullImageTask != null && entry.fullImageTask != task) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ entry.requestedBits &= ~BIT_FULL_IMAGE;
+ }
+ }
+ }
+
+ // Returns the task if we started the task or the task is already started.
+ private Future<?> startTaskIfNeeded(int index, int which) {
+ if (index < mActiveStart || index >= mActiveEnd) return null;
+
+ ImageEntry entry = mImageCache.get(getVersion(index));
+ if (entry == null) return null;
+
+ if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
+ return entry.screenNailTask;
+ } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
+ return entry.fullImageTask;
+ }
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ Utils.assertTrue(item != null);
+
+ if (which == BIT_SCREEN_NAIL
+ && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
+ entry.requestedBits |= BIT_SCREEN_NAIL;
+ entry.screenNailTask = mThreadPool.submit(
+ item.requestImage(MediaItem.TYPE_THUMBNAIL),
+ new ScreenNailListener(item.getDataVersion()));
+ // request screen nail
+ return entry.screenNailTask;
+ }
+ if (which == BIT_FULL_IMAGE
+ && (entry.requestedBits & BIT_FULL_IMAGE) == 0
+ && (item.getSupportedOperations()
+ & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+ entry.requestedBits |= BIT_FULL_IMAGE;
+ entry.fullImageTask = mThreadPool.submit(
+ item.requestLargeImage(),
+ new FullImageListener(item.getDataVersion()));
+ // request full image
+ return entry.fullImageTask;
+ }
+ return null;
+ }
+
+ private void updateImageCache() {
+ HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
+ for (int i = mActiveStart; i < mActiveEnd; ++i) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ long version = item == null
+ ? MediaObject.INVALID_DATA_VERSION
+ : item.getDataVersion();
+ if (version == MediaObject.INVALID_DATA_VERSION) continue;
+ ImageEntry entry = mImageCache.get(version);
+ toBeRemoved.remove(version);
+ if (entry != null) {
+ if (Math.abs(i - mCurrentIndex) > 1) {
+ if (entry.fullImageTask != null) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ }
+ entry.fullImage = null;
+ entry.requestedBits &= ~BIT_FULL_IMAGE;
+ }
+ } else {
+ entry = new ImageEntry();
+ entry.rotation = item.getRotation();
+ mImageCache.put(version, entry);
+ }
+ }
+
+ // Clear the data and requests for ImageEntries outside the new window.
+ for (Long version : toBeRemoved) {
+ ImageEntry entry = mImageCache.remove(version);
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ }
+ }
+
+ private class FullImageListener
+ implements Runnable, FutureListener<BitmapRegionDecoder> {
+ private final long mVersion;
+ private Future<BitmapRegionDecoder> mFuture;
+
+ public FullImageListener(long version) {
+ mVersion = version;
+ }
+
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ public void run() {
+ updateFullImage(mVersion, mFuture);
+ }
+ }
+
+ private class ScreenNailListener
+ implements Runnable, FutureListener<Bitmap> {
+ private final long mVersion;
+ private Future<Bitmap> mFuture;
+
+ public ScreenNailListener(long version) {
+ mVersion = version;
+ }
+
+ public void onFutureDone(Future<Bitmap> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ public void run() {
+ updateScreenNail(mVersion, mFuture);
+ }
+ }
+
+ private static class ImageEntry {
+ public int requestedBits = 0;
+ public int rotation;
+ public BitmapRegionDecoder fullImage;
+ public Bitmap screenNail;
+ public Future<Bitmap> screenNailTask;
+ public Future<BitmapRegionDecoder> fullImageTask;
+ public boolean failToLoad = false;
+ }
+
+ private class SourceListener implements ContentListener {
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public boolean reloadContent;
+ public Path target;
+ public int indexHint;
+ public int contentStart;
+ public int contentEnd;
+
+ public int size;
+ public ArrayList<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ private boolean needContentReload() {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ if (mData[i % DATA_CACHE_SIZE] == null) return true;
+ }
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ return current == null || current.getPath() != mItemPath;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ UpdateInfo info = new UpdateInfo();
+ info.version = mSourceVersion;
+ info.reloadContent = needContentReload();
+ info.target = mItemPath;
+ info.indexHint = mCurrentIndex;
+ info.contentStart = mContentStart;
+ info.contentEnd = mContentEnd;
+ info.size = mSize;
+ return info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+ UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo updateInfo) {
+ mUpdateInfo = updateInfo;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+
+ if (info.size != mSize) {
+ mSize = info.size;
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ if (info.indexHint == MediaSet.INDEX_NOT_FOUND) {
+ // The image has been deleted, clear mItemPath, the
+ // mCurrentIndex will be updated in the updateCurrentItem().
+ mItemPath = null;
+ updateCurrentItem();
+ } else {
+ mCurrentIndex = info.indexHint;
+ }
+
+ updateSlidingWindow();
+
+ if (info.items != null) {
+ int start = Math.max(info.contentStart, mContentStart);
+ int end = Math.min(info.contentStart + info.items.size(), mContentEnd);
+ int dataIndex = start % DATA_CACHE_SIZE;
+ for (int i = start; i < end; ++i) {
+ mData[dataIndex] = info.items.get(i - info.contentStart);
+ if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
+ }
+ }
+ if (mItemPath == null) {
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+ }
+ updateImageCache();
+ updateTileProvider();
+ updateImageRequests();
+ fireModelInvalidated();
+ return null;
+ }
+
+ private void updateCurrentItem() {
+ if (mSize == 0) return;
+ if (mCurrentIndex >= mSize) {
+ mCurrentIndex = mSize - 1;
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT);
+ } else {
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT);
+ }
+ }
+ }
+
+ private class ReloadTask extends Thread {
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+
+ private boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ while (mActive) {
+ synchronized (this) {
+ if (!mDirty && mActive) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ UpdateInfo info = executeAndWait(new GetUpdateInfo());
+ synchronized (DataManager.LOCK) {
+ updateLoading(true);
+ long version = mSource.reload();
+ if (info.version != version) {
+ info.reloadContent = true;
+ info.size = mSource.getMediaItemCount();
+ }
+ if (!info.reloadContent) continue;
+ info.items = mSource.getMediaItem(info.contentStart, info.contentEnd);
+ MediaItem item = findCurrentMediaItem(info);
+ if (item == null || item.getPath() != info.target) {
+ info.indexHint = findIndexOfTarget(info);
+ }
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+
+ private MediaItem findCurrentMediaItem(UpdateInfo info) {
+ ArrayList<MediaItem> items = info.items;
+ int index = info.indexHint - info.contentStart;
+ return index < 0 || index >= items.size() ? null : items.get(index);
+ }
+
+ private int findIndexOfTarget(UpdateInfo info) {
+ if (info.target == null) return info.indexHint;
+ ArrayList<MediaItem> items = info.items;
+
+ // First, try to find the item in the data just loaded
+ if (items != null) {
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ if (items.get(i).getPath() == info.target) return i + info.contentStart;
+ }
+ }
+
+ // Not found, find it in mSource.
+ return mSource.getIndexOfItem(info.target, info.indexHint);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
new file mode 100644
index 000000000..f28eb221d
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -0,0 +1,581 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MtpDevice;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.DetailsWindow.DetailsSource;
+import com.android.gallery3d.ui.FilmStripView;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ImportCompleteListener;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.UserInteractionListener;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.WindowManager;
+import android.widget.ShareActionProvider;
+import android.widget.Toast;
+
+public class PhotoPage extends ActivityState
+ implements PhotoView.PhotoTapListener, FilmStripView.Listener,
+ UserInteractionListener {
+ private static final String TAG = "PhotoPage";
+
+ private static final int MSG_HIDE_BARS = 1;
+ private static final int HIDE_BARS_TIMEOUT = 3500;
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_CROP = 2;
+ private static final int REQUEST_CROP_PICASA = 3;
+
+ public static final String KEY_MEDIA_SET_PATH = "media-set-path";
+ public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
+ public static final String KEY_INDEX_HINT = "index-hint";
+
+ private GalleryApp mApplication;
+ private SelectionManager mSelectionManager;
+
+ private PhotoView mPhotoView;
+ private PhotoPage.Model mModel;
+ private FilmStripView mFilmStripView;
+ private DetailsWindow mDetailsWindow;
+ private boolean mShowDetails;
+
+ // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
+ // E.g., viewing a photo in gmail attachment
+ private MediaSet mMediaSet;
+ private Menu mMenu;
+
+ private Intent mResultIntent = new Intent();
+ private int mCurrentIndex = 0;
+ private Handler mHandler;
+ private boolean mShowBars;
+ private ActionBar mActionBar;
+ private MyMenuVisibilityListener mMenuVisibilityListener;
+ private boolean mIsMenuVisible;
+ private boolean mIsInteracting;
+ private MediaItem mCurrentPhoto = null;
+ private MenuExecutor mMenuExecutor;
+ private boolean mIsActive;
+ private ShareActionProvider mShareActionProvider;
+
+ public static interface Model extends PhotoView.Model {
+ public void resume();
+ public void pause();
+ public boolean isEmpty();
+ public MediaItem getCurrentMediaItem();
+ public int getCurrentIndex();
+ public void setCurrentPhoto(Path path, int indexHint);
+ }
+
+ private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ mIsMenuVisible = isVisible;
+ refreshHidingMessage();
+ }
+ }
+
+ private GLView mRootPane = new GLView() {
+
+ @Override
+ protected void renderBackground(GLCanvas view) {
+ view.clearBuffer();
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mPhotoView.layout(0, 0, right - left, bottom - top);
+ PositionRepository.getInstance(mActivity).setOffset(0, 0);
+ int filmStripHeight = 0;
+ if (mFilmStripView != null) {
+ mFilmStripView.measure(
+ MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ filmStripHeight = mFilmStripView.getMeasuredHeight();
+ mFilmStripView.layout(0, bottom - top - filmStripHeight,
+ right - left, bottom - top);
+ }
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int viewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ mDetailsWindow.layout(
+ 0, viewTop, width, bottom - top - filmStripHeight);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mActionBar = ((Activity) mActivity).getActionBar();
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+
+ mPhotoView = new PhotoView(mActivity);
+ mPhotoView.setPhotoTapListener(this);
+ mRootPane.addComponent(mPhotoView);
+ mApplication = (GalleryApp)((Activity) mActivity).getApplication();
+
+ String setPathString = data.getString(KEY_MEDIA_SET_PATH);
+ Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
+
+ if (setPathString != null) {
+ mMediaSet = mActivity.getDataManager().getMediaSet(setPathString);
+ mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
+ mMediaSet = (MediaSet)
+ mActivity.getDataManager().getMediaObject(setPathString);
+ if (mMediaSet == null) {
+ Log.w(TAG, "failed to restore " + setPathString);
+ }
+ PhotoDataAdapter pda = new PhotoDataAdapter(
+ mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
+ mModel = pda;
+ mPhotoView.setModel(mModel);
+
+ Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity);
+
+ mFilmStripView = new FilmStripView(mActivity, mMediaSet,
+ config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin,
+ config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize,
+ config.filmstripGripSize, config.filmstripGripWidth);
+ mRootPane.addComponent(mFilmStripView);
+ mFilmStripView.setListener(this);
+ mFilmStripView.setUserInteractionListener(this);
+ mFilmStripView.setFocusIndex(mCurrentIndex);
+ mFilmStripView.setStartIndex(mCurrentIndex);
+
+ mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
+ setStateResult(Activity.RESULT_OK, mResultIntent);
+
+ pda.setDataListener(new PhotoDataAdapter.DataListener() {
+
+ public void onPhotoChanged(int index, Path item) {
+ mFilmStripView.setFocusIndex(index);
+ mCurrentIndex = index;
+ mResultIntent.putExtra(KEY_INDEX_HINT, index);
+ if (item != null) {
+ mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
+ MediaItem photo = mModel.getCurrentMediaItem();
+ if (photo != null) updateCurrentPhoto(photo);
+ } else {
+ mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
+ }
+ setStateResult(Activity.RESULT_OK, mResultIntent);
+ }
+
+ @Override
+ public void onLoadingFinished() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ if (!mModel.isEmpty()) {
+ MediaItem photo = mModel.getCurrentMediaItem();
+ if (photo != null) updateCurrentPhoto(photo);
+ } else if (mIsActive) {
+ mActivity.getStateManager().finishState(PhotoPage.this);
+ }
+ }
+
+
+ @Override
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+ });
+ } else {
+ // Get default media set by the URI
+ MediaItem mediaItem = (MediaItem)
+ mActivity.getDataManager().getMediaObject(itemPath);
+ mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
+ mPhotoView.setModel(mModel);
+ updateCurrentPhoto(mediaItem);
+ }
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_HIDE_BARS: {
+ hideBars();
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+
+ // start the opening animation
+ mPhotoView.setOpenedItem(itemPath);
+ }
+
+ private void updateCurrentPhoto(MediaItem photo) {
+ if (mCurrentPhoto == photo) return;
+ mCurrentPhoto = photo;
+ if (mCurrentPhoto == null) return;
+ updateMenuOperations();
+ if (mShowDetails) {
+ mDetailsWindow.reloadDetails(mModel.getCurrentIndex());
+ }
+ String title = photo.getName();
+ if (title != null) mActionBar.setTitle(title);
+ mPhotoView.showVideoPlayIcon(photo.getMediaType()
+ == MediaObject.MEDIA_TYPE_VIDEO);
+
+ // If we have an ActionBar then we update the share intent
+ if (mShareActionProvider != null) {
+ Path path = photo.getPath();
+ DataManager manager = mActivity.getDataManager();
+ int type = manager.getMediaType(path);
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(MenuExecutor.getMimeType(type));
+ intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
+ mShareActionProvider.setShareIntent(intent);
+ }
+ }
+
+ private void updateMenuOperations() {
+ if (mCurrentPhoto == null || mMenu == null) return;
+ int supportedOperations = mCurrentPhoto.getSupportedOperations();
+ if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
+ supportedOperations &= ~MediaObject.SUPPORT_EDIT;
+ }
+ MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
+ }
+
+ private void showBars() {
+ if (mShowBars) return;
+ mShowBars = true;
+ mActionBar.show();
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ if (mFilmStripView != null) {
+ mFilmStripView.show();
+ }
+ }
+
+ private void hideBars() {
+ if (!mShowBars) return;
+ mShowBars = false;
+ mActionBar.hide();
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ if (mFilmStripView != null) {
+ mFilmStripView.hide();
+ }
+ }
+
+ private void refreshHidingMessage() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ if (!mIsMenuVisible && !mIsInteracting) {
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
+ }
+ }
+
+ public void onUserInteraction() {
+ showBars();
+ refreshHidingMessage();
+ }
+
+ public void onUserInteractionTap() {
+ if (mShowBars) {
+ hideBars();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ } else {
+ showBars();
+ refreshHidingMessage();
+ }
+ }
+
+ public void onUserInteractionBegin() {
+ showBars();
+ mIsInteracting = true;
+ refreshHidingMessage();
+ }
+
+ public void onUserInteractionEnd() {
+ mIsInteracting = false;
+ refreshHidingMessage();
+ }
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ PositionRepository repository = PositionRepository.getInstance(mActivity);
+ repository.clear();
+ if (mCurrentPhoto != null) {
+ Position position = new Position();
+ position.x = mRootPane.getWidth() / 2;
+ position.y = mRootPane.getHeight() / 2;
+ position.z = -1000;
+ repository.putPosition(
+ Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())),
+ position);
+ }
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
+ inflater.inflate(R.menu.photo, menu);
+ menu.findItem(R.id.action_slideshow).setVisible(
+ mMediaSet != null && !(mMediaSet instanceof MtpDevice));
+ mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
+ mMenu = menu;
+ mShowBars = true;
+ updateMenuOperations();
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ MediaItem current = mModel.getCurrentMediaItem();
+
+ if (current == null) {
+ // item is not ready, ignore
+ return true;
+ }
+
+ int currentIndex = mModel.getCurrentIndex();
+ Path path = current.getPath();
+
+ DataManager manager = mActivity.getDataManager();
+ int action = item.getItemId();
+ switch (action) {
+ case R.id.action_slideshow: {
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH,
+ mMediaSet.getPath().toString());
+ data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ mActivity.getStateManager().startStateForResult(
+ SlideshowPage.class, REQUEST_SLIDESHOW, data);
+ return true;
+ }
+ case R.id.action_crop: {
+ Activity activity = (Activity) mActivity;
+ Intent intent = new Intent(CropImage.CROP_ACTION);
+ intent.setClass(activity, CropImage.class);
+ intent.setData(manager.getContentUri(path));
+ activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
+ ? REQUEST_CROP_PICASA
+ : REQUEST_CROP);
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails(currentIndex);
+ }
+ return true;
+ }
+ case R.id.action_setas:
+ case R.id.action_confirm_delete:
+ case R.id.action_rotate_ccw:
+ case R.id.action_rotate_cw:
+ case R.id.action_show_on_map:
+ case R.id.action_edit:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item, null);
+ return true;
+ case R.id.action_import:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item,
+ new ImportCompleteListener(mActivity));
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsWindow.hide();
+ }
+
+ private void showDetails(int index) {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mDetailsWindow.reloadDetails(index);
+ mDetailsWindow.show();
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ MediaItem item = mModel.getCurrentMediaItem();
+ if (item == null) {
+ // item is not ready, ignore
+ return;
+ }
+
+ boolean playVideo =
+ (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
+
+ if (playVideo) {
+ // determine if the point is at center (1/6) of the photo view.
+ // (The position of the "play" icon is at center (1/6) of the photo)
+ int w = mPhotoView.getWidth();
+ int h = mPhotoView.getHeight();
+ playVideo = (Math.abs(x - w / 2) * 12 <= w)
+ && (Math.abs(y - h / 2) * 12 <= h);
+ }
+
+ if (playVideo) {
+ playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
+ } else {
+ onUserInteractionTap();
+ }
+ }
+
+ public static void playVideo(Activity activity, Uri uri, String title) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "video/*");
+ intent.putExtra(Intent.EXTRA_TITLE, title);
+ activity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, activity.getString(R.string.video_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Called by FileStripView
+ public void onSlotSelected(int slotIndex) {
+ ((PhotoDataAdapter) mModel).jumpTo(slotIndex);
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CROP:
+ if (resultCode == Activity.RESULT_OK) {
+ if (data == null) break;
+ Path path = mApplication
+ .getDataManager().findPathByUri(data.getData());
+ if (path != null) {
+ mModel.setCurrentPhoto(path, mCurrentIndex);
+ }
+ }
+ break;
+ case REQUEST_CROP_PICASA: {
+ int message = resultCode == Activity.RESULT_OK
+ ? R.string.crop_saved
+ : R.string.crop_not_saved;
+ Toast.makeText(mActivity.getAndroidContext(),
+ message, Toast.LENGTH_SHORT).show();
+ break;
+ }
+ case REQUEST_SLIDESHOW: {
+ if (data == null) break;
+ String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
+ int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+ if (path != null) {
+ mModel.setCurrentPhoto(Path.fromString(path), index);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ if (mFilmStripView != null) {
+ mFilmStripView.pause();
+ }
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ mPhotoView.pause();
+ mModel.pause();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mModel.resume();
+ mPhotoView.resume();
+ if (mFilmStripView != null) {
+ mFilmStripView.resume();
+ }
+ if (mMenuVisibilityListener == null) {
+ mMenuVisibilityListener = new MyMenuVisibilityListener();
+ }
+ mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
+ onUserInteraction();
+ }
+
+ private class MyDetailsSource implements DetailsSource {
+ public MediaDetails getDetails() {
+ return mModel.getCurrentMediaItem().getDetails();
+ }
+ public int size() {
+ return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
+ }
+ public int findIndex(int indexHint) {
+ return indexHint;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
new file mode 100644
index 000000000..11e0013cc
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.PhotoView.ImageData;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+
+public class SinglePhotoDataAdapter extends TileImageViewAdapter
+ implements PhotoPage.Model {
+
+ private static final String TAG = "SinglePhotoDataAdapter";
+ private static final int SIZE_BACKUP = 640;
+ private static final int MSG_UPDATE_IMAGE = 1;
+
+ private MediaItem mItem;
+ private boolean mHasFullImage;
+ private Future<?> mTask;
+ private BitmapRegionDecoder mDecoder;
+ private Bitmap mBackup;
+ private Handler mHandler;
+
+ private PhotoView mPhotoView;
+ private ThreadPool mThreadPool;
+
+ public SinglePhotoDataAdapter(
+ GalleryActivity activity, PhotoView view, MediaItem item) {
+ mItem = Utils.checkNotNull(item);
+ mHasFullImage = (item.getSupportedOperations() &
+ MediaItem.SUPPORT_FULL_IMAGE) != 0;
+ mPhotoView = Utils.checkNotNull(view);
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_IMAGE);
+ if (mHasFullImage) {
+ onDecodeLargeComplete((Future<BitmapRegionDecoder>)
+ message.obj);
+ } else {
+ onDecodeThumbComplete((Future<Bitmap>) message.obj);
+ }
+ }
+ };
+ mThreadPool = activity.getThreadPool();
+ }
+
+ private FutureListener<BitmapRegionDecoder> mLargeListener =
+ new FutureListener<BitmapRegionDecoder>() {
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ private FutureListener<Bitmap> mThumbListener =
+ new FutureListener<Bitmap>() {
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public int getImageRotation() {
+ return mItem.getRotation();
+ }
+
+ private void onDecodeLargeComplete(Future<BitmapRegionDecoder> future) {
+ try {
+ mDecoder = future.get();
+ if (mDecoder == null) return;
+ int width = mDecoder.getWidth();
+ int height = mDecoder.getHeight();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ width, height, SIZE_BACKUP);
+ mBackup = mDecoder.decodeRegion(
+ new Rect(0, 0, width, height), options);
+ setBackupImage(mBackup, width, height);
+ setRegionDecoder(mDecoder);
+ mPhotoView.notifyImageInvalidated(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode large", t);
+ }
+ }
+
+ private void onDecodeThumbComplete(Future<Bitmap> future) {
+ try {
+ mBackup = future.get();
+ if (mBackup == null) return;
+ setBackupImage(mBackup, mBackup.getWidth(), mBackup.getHeight());
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.notifyImageInvalidated(0); // the current image
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode thumb", t);
+ }
+ }
+
+ public void resume() {
+ if (mTask == null) {
+ if (mHasFullImage) {
+ mTask = mThreadPool.submit(
+ mItem.requestLargeImage(), mLargeListener);
+ } else {
+ mTask = mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_THUMBNAIL),
+ mThumbListener);
+ }
+ }
+ }
+
+ public void pause() {
+ Future<?> task = mTask;
+ task.cancel();
+ task.waitDone();
+ if (task.get() == null) {
+ mTask = null;
+ }
+ }
+
+ public ImageData getNextImage() {
+ return null;
+ }
+
+ public ImageData getPreviousImage() {
+ return null;
+ }
+
+ public void next() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void previous() {
+ throw new UnsupportedOperationException();
+ }
+
+ public MediaItem getCurrentMediaItem() {
+ return mItem;
+ }
+
+ public int getCurrentIndex() {
+ return 0;
+ }
+
+ public void setCurrentPhoto(Path path, int indexHint) {
+ // ignore
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
new file mode 100644
index 000000000..6f9b98e8e
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
@@ -0,0 +1,187 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.app.SlideshowPage.Slide;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.graphics.Bitmap;
+
+import java.util.LinkedList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SlideshowDataAdapter implements SlideshowPage.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlideshowDataAdapter";
+
+ private static final int IMAGE_QUEUE_CAPACITY = 3;
+
+ public interface SlideshowSource {
+ public void addContentListener(ContentListener listener);
+ public void removeContentListener(ContentListener listener);
+ public long reload();
+ public MediaItem getMediaItem(int index);
+ }
+
+ private final SlideshowSource mSource;
+
+ private int mLoadIndex = 0;
+ private int mNextOutput = 0;
+ private boolean mIsActive = false;
+ private boolean mNeedReset;
+ private boolean mDataReady;
+
+ private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>();
+
+ private Future<Void> mReloadTask;
+ private final ThreadPool mThreadPool;
+
+ private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+ private final AtomicBoolean mNeedReload = new AtomicBoolean(false);
+ private final SourceListener mSourceListener = new SourceListener();
+
+ public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index) {
+ mSource = source;
+ mLoadIndex = index;
+ mNextOutput = index;
+ mThreadPool = context.getThreadPool();
+ }
+
+ public MediaItem loadItem() {
+ if (mNeedReload.compareAndSet(true, false)) {
+ long v = mSource.reload();
+ if (v != mDataVersion) {
+ mDataVersion = v;
+ mNeedReset = true;
+ return null;
+ }
+ }
+ return mSource.getMediaItem(mLoadIndex);
+ }
+
+ private class ReloadTask implements Job<Void> {
+ public Void run(JobContext jc) {
+ while (true) {
+ synchronized (SlideshowDataAdapter.this) {
+ while (mIsActive && (!mDataReady
+ || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) {
+ try {
+ SlideshowDataAdapter.this.wait();
+ } catch (InterruptedException ex) {
+ // ignored.
+ }
+ continue;
+ }
+ }
+ if (!mIsActive) return null;
+ mNeedReset = false;
+
+ MediaItem item = loadItem();
+
+ if (mNeedReset) {
+ synchronized (SlideshowDataAdapter.this) {
+ mImageQueue.clear();
+ mLoadIndex = mNextOutput;
+ }
+ continue;
+ }
+
+ if (item == null) {
+ synchronized (SlideshowDataAdapter.this) {
+ if (!mNeedReload.get()) mDataReady = false;
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ continue;
+ }
+
+ Bitmap bitmap = item
+ .requestImage(MediaItem.TYPE_THUMBNAIL)
+ .run(jc);
+
+ if (bitmap != null) {
+ synchronized (SlideshowDataAdapter.this) {
+ mImageQueue.addLast(
+ new Slide(item, mLoadIndex, bitmap));
+ if (mImageQueue.size() == 1) {
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ }
+ }
+ ++mLoadIndex;
+ }
+ }
+ }
+
+ private class SourceListener implements ContentListener {
+ public void onContentDirty() {
+ synchronized (SlideshowDataAdapter.this) {
+ mNeedReload.set(true);
+ mDataReady = true;
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ }
+ }
+
+ private synchronized Slide innerNextBitmap() {
+ while (mIsActive && mDataReady && mImageQueue.isEmpty()) {
+ try {
+ wait();
+ } catch (InterruptedException t) {
+ throw new AssertionError();
+ }
+ }
+ if (mImageQueue.isEmpty()) return null;
+ mNextOutput++;
+ this.notifyAll();
+ return mImageQueue.removeFirst();
+ }
+
+ public Future<Slide> nextSlide(FutureListener<Slide> listener) {
+ return mThreadPool.submit(new Job<Slide>() {
+ public Slide run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ return innerNextBitmap();
+ }
+ }, listener);
+ }
+
+ public void pause() {
+ synchronized (this) {
+ mIsActive = false;
+ notifyAll();
+ }
+ mSource.removeContentListener(mSourceListener);
+ mReloadTask.cancel();
+ mReloadTask.waitDone();
+ mReloadTask = null;
+ }
+
+ public synchronized void resume() {
+ mIsActive = true;
+ mSource.addContentListener(mSourceListener);
+ mNeedReload.set(true);
+ mDataReady = true;
+ mReloadTask = mThreadPool.submit(new ReloadTask());
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java
new file mode 100644
index 000000000..f4abe86ab
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDream.java
@@ -0,0 +1,28 @@
+package com.android.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.support.v13.dreams.BasicDream;
+import android.graphics.Canvas;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ViewFlipper;
+
+public class SlideshowDream extends BasicDream {
+ @Override
+ public void onCreate(Bundle bndl) {
+ super.onCreate(bndl);
+ Intent i = new Intent(
+ Intent.ACTION_VIEW,
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+// Uri.fromFile(Environment.getExternalStoragePublicDirectory(
+// Environment.DIRECTORY_PICTURES)))
+ .putExtra(Gallery.EXTRA_SLIDESHOW, true)
+ .setFlags(getIntent().getFlags());
+ startActivity(i);
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
new file mode 100644
index 000000000..cdf9308ec
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -0,0 +1,338 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SlideshowView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+public class SlideshowPage extends ActivityState {
+ private static final String TAG = "SlideshowPage";
+
+ public static final String KEY_SET_PATH = "media-set-path";
+ public static final String KEY_ITEM_PATH = "media-item-path";
+ public static final String KEY_PHOTO_INDEX = "photo-index";
+ public static final String KEY_RANDOM_ORDER = "random-order";
+ public static final String KEY_REPEAT = "repeat";
+
+ private static final long SLIDESHOW_DELAY = 3000; // 3 seconds
+
+ private static final int MSG_LOAD_NEXT_BITMAP = 1;
+ private static final int MSG_SHOW_PENDING_BITMAP = 2;
+
+ public static interface Model {
+ public void pause();
+ public void resume();
+ public Future<Slide> nextSlide(FutureListener<Slide> listener);
+ }
+
+ public static class Slide {
+ public Bitmap bitmap;
+ public MediaItem item;
+ public int index;
+
+ public Slide(MediaItem item, int index, Bitmap bitmap) {
+ this.bitmap = bitmap;
+ this.item = item;
+ this.index = index;
+ }
+ }
+
+ private Handler mHandler;
+ private Model mModel;
+ private SlideshowView mSlideshowView;
+
+ private Slide mPendingSlide = null;
+ private boolean mIsActive = false;
+ private WakeLock mWakeLock;
+ private Intent mResultIntent = new Intent();
+
+ private GLView mRootPane = new GLView() {
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mSlideshowView.layout(0, 0, right - left, bottom - top);
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ onBackPressed();
+ }
+ return true;
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ canvas.clearBuffer();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+
+ PowerManager pm = (PowerManager) mActivity.getAndroidContext()
+ .getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
+ | PowerManager.ON_AFTER_RELEASE, TAG);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SHOW_PENDING_BITMAP:
+ showPendingBitmap();
+ break;
+ case MSG_LOAD_NEXT_BITMAP:
+ loadNextBitmap();
+ break;
+ default: throw new AssertionError();
+ }
+ }
+ };
+ initializeViews();
+ initializeData(data);
+ }
+
+ private void loadNextBitmap() {
+ mModel.nextSlide(new FutureListener<Slide>() {
+ public void onFutureDone(Future<Slide> future) {
+ mPendingSlide = future.get();
+ mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP);
+ }
+ });
+ }
+
+ private void showPendingBitmap() {
+ // mPendingBitmap could be null, if
+ // 1.) there is no more items
+ // 2.) mModel is paused
+ Slide slide = mPendingSlide;
+ if (slide == null) {
+ if (mIsActive) {
+ mActivity.getStateManager().finishState(SlideshowPage.this);
+ }
+ return;
+ }
+
+ mSlideshowView.next(slide.bitmap, slide.item.getRotation());
+
+ setStateResult(Activity.RESULT_OK, mResultIntent
+ .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString())
+ .putExtra(KEY_PHOTO_INDEX, slide.index));
+ mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP,
+ SLIDESHOW_DELAY);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mWakeLock.release();
+ mIsActive = false;
+ mModel.pause();
+ mSlideshowView.release();
+
+ mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
+ mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mWakeLock.acquire();
+ mIsActive = true;
+ mModel.resume();
+
+ if (mPendingSlide != null) {
+ showPendingBitmap();
+ } else {
+ loadNextBitmap();
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(KEY_SET_PATH);
+ boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
+ MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+
+ if (random) {
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(
+ mActivity, new ShuffleSource(mediaSet, repeat), 0);
+ setStateResult(Activity.RESULT_OK,
+ mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
+ } else {
+ int index = data.getInt(KEY_PHOTO_INDEX);
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity,
+ new SequentialSource(mediaSet, repeat), index);
+ setStateResult(Activity.RESULT_OK,
+ mResultIntent.putExtra(KEY_PHOTO_INDEX, index));
+ }
+ }
+
+ private void initializeViews() {
+ mSlideshowView = new SlideshowView();
+ mRootPane.addComponent(mSlideshowView);
+ setContentPane(mRootPane);
+ }
+
+ private static MediaItem findMediaItem(MediaSet mediaSet, int index) {
+ for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) {
+ MediaSet subset = mediaSet.getSubMediaSet(i);
+ int count = subset.getTotalMediaItemCount();
+ if (index < count) {
+ return findMediaItem(subset, index);
+ }
+ index -= count;
+ }
+ ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1);
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource {
+ private static final int RETRY_COUNT = 5;
+ private final MediaSet mMediaSet;
+ private final Random mRandom = new Random();
+ private int mOrder[] = new int[0];
+ private boolean mRepeat;
+ private long mSourceVersion = MediaSet.INVALID_DATA_VERSION;
+ private int mLastIndex = -1;
+
+ public ShuffleSource(MediaSet mediaSet, boolean repeat) {
+ mMediaSet = Utils.checkNotNull(mediaSet);
+ mRepeat = repeat;
+ }
+
+ public MediaItem getMediaItem(int index) {
+ if (!mRepeat && index >= mOrder.length) return null;
+ mLastIndex = mOrder[index % mOrder.length];
+ MediaItem item = findMediaItem(mMediaSet, mLastIndex);
+ for (int i = 0; i < RETRY_COUNT && item == null; ++i) {
+ Log.w(TAG, "fail to find image: " + mLastIndex);
+ mLastIndex = mRandom.nextInt(mOrder.length);
+ item = findMediaItem(mMediaSet, mLastIndex);
+ }
+ return item;
+ }
+
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mSourceVersion) {
+ mSourceVersion = version;
+ int count = mMediaSet.getTotalMediaItemCount();
+ if (count != mOrder.length) generateOrderArray(count);
+ }
+ return version;
+ }
+
+ private void generateOrderArray(int totalCount) {
+ if (mOrder.length != totalCount) {
+ mOrder = new int[totalCount];
+ for (int i = 0; i < totalCount; ++i) {
+ mOrder[i] = i;
+ }
+ }
+ for (int i = totalCount - 1; i > 0; --i) {
+ Utils.swap(mOrder, i, mRandom.nextInt(i + 1));
+ }
+ if (mOrder[0] == mLastIndex && totalCount > 1) {
+ Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1);
+ }
+ }
+
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ public void removeContentListener(ContentListener listener) {
+ mMediaSet.removeContentListener(listener);
+ }
+ }
+
+ private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
+ private static final int DATA_SIZE = 32;
+
+ private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
+ private int mDataStart = 0;
+ private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+ private final MediaSet mMediaSet;
+ private final boolean mRepeat;
+
+ public SequentialSource(MediaSet mediaSet, boolean repeat) {
+ mMediaSet = mediaSet;
+ mRepeat = repeat;
+ }
+
+ public MediaItem getMediaItem(int index) {
+ int dataEnd = mDataStart + mData.size();
+
+ if (mRepeat) {
+ index = index % mMediaSet.getMediaItemCount();
+ }
+ if (index < mDataStart || index >= dataEnd) {
+ mData = mMediaSet.getMediaItem(index, DATA_SIZE);
+ mDataStart = index;
+ dataEnd = index + mData.size();
+ }
+
+ return (index < mDataStart || index >= dataEnd)
+ ? null
+ : mData.get(index - mDataStart);
+ }
+
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mDataVersion) {
+ mDataVersion = version;
+ mData.clear();
+ }
+ return mDataVersion;
+ }
+
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ public void removeContentListener(ContentListener listener) {
+ mMediaSet.removeContentListener(listener);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
new file mode 100644
index 000000000..b551f693a
--- /dev/null
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -0,0 +1,277 @@
+/*
+ * 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.app;
+
+import com.android.gallery3d.common.Utils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import java.util.Stack;
+
+public class StateManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "StateManager";
+ private boolean mIsResumed = false;
+
+ private static final String KEY_MAIN = "activity-state";
+ private static final String KEY_DATA = "data";
+ private static final String KEY_STATE = "bundle";
+ private static final String KEY_CLASS = "class";
+
+ private GalleryActivity mContext;
+ private Stack<StateEntry> mStack = new Stack<StateEntry>();
+ private ActivityState.ResultEntry mResult;
+
+ public StateManager(GalleryActivity context) {
+ mContext = context;
+ }
+
+ public void startState(Class<? extends ActivityState> klass,
+ Bundle data) {
+ Log.v(TAG, "startState " + klass);
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ if (!mStack.isEmpty()) {
+ ActivityState top = getTopState();
+ if (mIsResumed) top.onPause();
+ }
+ state.initialize(mContext, data);
+
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public void startStateForResult(Class<? extends ActivityState> klass,
+ int requestCode, Bundle data) {
+ Log.v(TAG, "startStateForResult " + klass + ", " + requestCode);
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ state.initialize(mContext, data);
+ state.mResult = new ActivityState.ResultEntry();
+ state.mResult.requestCode = requestCode;
+
+ if (!mStack.isEmpty()) {
+ ActivityState as = getTopState();
+ as.mReceivedResults = state.mResult;
+ if (mIsResumed) as.onPause();
+ } else {
+ mResult = state.mResult;
+ }
+
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public boolean createOptionsMenu(Menu menu) {
+ if (!mStack.isEmpty()) {
+ ((Activity) mContext).setProgressBarIndeterminateVisibility(false);
+ return getTopState().onCreateActionBar(menu);
+ } else {
+ return false;
+ }
+ }
+
+ public void resume() {
+ if (mIsResumed) return;
+ mIsResumed = true;
+ if (!mStack.isEmpty()) getTopState().resume();
+ }
+
+ public void pause() {
+ if (!mIsResumed) return;
+ mIsResumed = false;
+ if (!mStack.isEmpty()) getTopState().onPause();
+ }
+
+ public void notifyActivityResult(int requestCode, int resultCode, Intent data) {
+ getTopState().onStateResult(requestCode, resultCode, data);
+ }
+
+ public int getStateCount() {
+ return mStack.size();
+ }
+
+ public boolean itemSelected(MenuItem item) {
+ if (!mStack.isEmpty()) {
+ if (mStack.size() > 1 && item.getItemId() == android.R.id.home) {
+ getTopState().onBackPressed();
+ return true;
+ } else {
+ return getTopState().onItemSelected(item);
+ }
+ }
+ return false;
+ }
+
+ public void onBackPressed() {
+ if (!mStack.isEmpty()) {
+ getTopState().onBackPressed();
+ }
+ }
+
+ void finishState(ActivityState state) {
+ Log.v(TAG, "finishState " + state.getClass());
+ if (state != mStack.peek().activityState) {
+ throw new IllegalArgumentException("The stateview to be finished"
+ + " is not at the top of the stack: " + state + ", "
+ + mStack.peek().activityState);
+ }
+
+ // Remove the top state.
+ mStack.pop();
+ if (mIsResumed) state.onPause();
+ mContext.getGLRoot().setContentPane(null);
+ state.onDestroy();
+
+ if (mStack.isEmpty()) {
+ Log.v(TAG, "no more state, finish activity");
+ Activity activity = (Activity) mContext.getAndroidContext();
+ if (mResult != null) {
+ activity.setResult(mResult.resultCode, mResult.resultData);
+ }
+ activity.finish();
+
+ // The finish() request is rejected (only happens under Monkey),
+ // so we start the default page instead.
+ if (!activity.isFinishing()) {
+ Log.v(TAG, "finish() failed, start default page");
+ ((Gallery) mContext).startDefaultPage();
+ }
+ } else {
+ // Restore the immediately previous state
+ ActivityState top = mStack.peek().activityState;
+ if (mIsResumed) top.resume();
+ }
+ }
+
+ void switchState(ActivityState oldState,
+ Class<? extends ActivityState> klass, Bundle data) {
+ Log.v(TAG, "switchState " + oldState + ", " + klass);
+ if (oldState != mStack.peek().activityState) {
+ throw new IllegalArgumentException("The stateview to be finished"
+ + " is not at the top of the stack: " + oldState + ", "
+ + mStack.peek().activityState);
+ }
+ // Remove the top state.
+ mStack.pop();
+ if (mIsResumed) oldState.onPause();
+ oldState.onDestroy();
+
+ // Create new state.
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ state.initialize(mContext, data);
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public void destroy() {
+ Log.v(TAG, "destroy");
+ while (!mStack.isEmpty()) {
+ mStack.pop().activityState.onDestroy();
+ }
+ mStack.clear();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void restoreFromState(Bundle inState) {
+ Log.v(TAG, "restoreFromState");
+ Parcelable list[] = inState.getParcelableArray(KEY_MAIN);
+
+ for (Parcelable parcelable : list) {
+ Bundle bundle = (Bundle) parcelable;
+ Class<? extends ActivityState> klass =
+ (Class<? extends ActivityState>) bundle.getSerializable(KEY_CLASS);
+
+ Bundle data = bundle.getBundle(KEY_DATA);
+ Bundle state = bundle.getBundle(KEY_STATE);
+
+ ActivityState activityState;
+ try {
+ Log.v(TAG, "restoreFromState " + klass);
+ activityState = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ activityState.initialize(mContext, data);
+ activityState.onCreate(data, state);
+ mStack.push(new StateEntry(data, activityState));
+ }
+ }
+
+ public void saveState(Bundle outState) {
+ Log.v(TAG, "saveState");
+ Parcelable list[] = new Parcelable[mStack.size()];
+
+ int i = 0;
+ for (StateEntry entry : mStack) {
+ Bundle bundle = new Bundle();
+ bundle.putSerializable(KEY_CLASS, entry.activityState.getClass());
+ bundle.putBundle(KEY_DATA, entry.data);
+ Bundle state = new Bundle();
+ entry.activityState.onSaveState(state);
+ bundle.putBundle(KEY_STATE, state);
+ Log.v(TAG, "saveState " + entry.activityState.getClass());
+ list[i++] = bundle;
+ }
+ outState.putParcelableArray(KEY_MAIN, list);
+ }
+
+ public boolean hasStateClass(Class<? extends ActivityState> klass) {
+ for (StateEntry entry : mStack) {
+ if (klass.isInstance(entry.activityState)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public ActivityState getTopState() {
+ Utils.assertTrue(!mStack.isEmpty());
+ return mStack.peek().activityState;
+ }
+
+ private static class StateEntry {
+ public Bundle data;
+ public ActivityState activityState;
+
+ public StateEntry(Bundle data, ActivityState state) {
+ this.data = data;
+ this.activityState = state;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/UsbDeviceActivity.java b/src/com/android/gallery3d/app/UsbDeviceActivity.java
new file mode 100644
index 000000000..28bd667e3
--- /dev/null
+++ b/src/com/android/gallery3d/app/UsbDeviceActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.app;
+
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+/* This Activity does nothing but receive USB_DEVICE_ATTACHED events from the
+ * USB service and springboards to the main Gallery activity
+ */
+public final class UsbDeviceActivity extends Activity {
+
+ static final String TAG = "UsbDeviceActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ //
+ Intent intent = new Intent(this, Gallery.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "unable to start Gallery activity", e);
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
new file mode 100644
index 000000000..07a3d5313
--- /dev/null
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007 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.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Display;
+
+/**
+ * Wallpaper picker for the gallery application. This just redirects to the
+ * standard pick action.
+ */
+public class Wallpaper extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Wallpaper";
+
+ private static final String IMAGE_TYPE = "image/*";
+ private static final String KEY_STATE = "activity-state";
+ private static final String KEY_PICKED_ITEM = "picked-item";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_PHOTO_PICKED = 1;
+
+ private int mState = STATE_INIT;
+ private Uri mPickedItem;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ if (bundle != null) {
+ mState = bundle.getInt(KEY_STATE);
+ mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle saveState) {
+ saveState.putInt(KEY_STATE, mState);
+ if (mPickedItem != null) {
+ saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent intent = getIntent();
+ switch (mState) {
+ case STATE_INIT: {
+ mPickedItem = intent.getData();
+ if (mPickedItem == null) {
+ Intent request = new Intent(Intent.ACTION_GET_CONTENT)
+ .setClass(this, DialogPicker.class)
+ .setType(IMAGE_TYPE);
+ startActivityForResult(request, STATE_PHOTO_PICKED);
+ return;
+ }
+ mState = STATE_PHOTO_PICKED;
+ // fall-through
+ }
+ case STATE_PHOTO_PICKED: {
+ int width = getWallpaperDesiredMinimumWidth();
+ int height = getWallpaperDesiredMinimumHeight();
+ Display display = getWindowManager().getDefaultDisplay();
+ float spotlightX = (float) display.getWidth() / width;
+ float spotlightY = (float) display.getHeight() / height;
+ Intent request = new Intent(CropImage.ACTION_CROP)
+ .setDataAndType(mPickedItem, IMAGE_TYPE)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtra(CropImage.KEY_OUTPUT_X, width)
+ .putExtra(CropImage.KEY_OUTPUT_Y, height)
+ .putExtra(CropImage.KEY_ASPECT_X, width)
+ .putExtra(CropImage.KEY_ASPECT_Y, height)
+ .putExtra(CropImage.KEY_SPOTLIGHT_X, spotlightX)
+ .putExtra(CropImage.KEY_SPOTLIGHT_Y, spotlightY)
+ .putExtra(CropImage.KEY_SCALE, true)
+ .putExtra(CropImage.KEY_NO_FACE_DETECTION, true)
+ .putExtra(CropImage.KEY_SET_AS_WALLPAPER, true);
+ startActivity(request);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ setResult(resultCode);
+ finish();
+ return;
+ }
+ mState = requestCode;
+ if (mState == STATE_PHOTO_PICKED) {
+ mPickedItem = data.getData();
+ }
+
+ // onResume() would be called next
+ }
+}
diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java
new file mode 100644
index 000000000..e1e601dd6
--- /dev/null
+++ b/src/com/android/gallery3d/data/ChangeNotifier.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import android.net.Uri;
+
+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);
+ }
+
+ // Returns the dirty flag and clear it.
+ public boolean isDirty() {
+ return mContentDirty.compareAndSet(true, false);
+ }
+
+ public void fakeChange() {
+ onChange(false);
+ }
+
+ public void clearDirty() {
+ mContentDirty.set(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..32f902301
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbum.java
@@ -0,0 +1,129 @@
+/*
+ * 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 {
+ private static final String TAG = "ClusterAlbum";
+ private ArrayList<Path> mPaths = new ArrayList<Path>();
+ private String mName = "";
+ private DataManager mDataManager;
+ private MediaSet mClusterAlbumSet;
+
+ public ClusterAlbum(Path path, DataManager dataManager,
+ MediaSet clusterAlbumSet) {
+ super(path, nextVersionNumber());
+ mDataManager = dataManager;
+ mClusterAlbumSet = clusterAlbumSet;
+ mClusterAlbumSet.addContentListener(this);
+ }
+
+ 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() {
+ 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;
+ }
+
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO;
+ }
+
+ @Override
+ public void delete() {
+ ItemConsumer consumer = new ItemConsumer() {
+ 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..5b0569a67
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -0,0 +1,152 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class ClusterAlbumSet extends MediaSet implements ContentListener {
+ 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;
+ }
+
+ 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 = (ClusterAlbum) dataManager.peekMediaObject(
+ childPath);
+ if (album == null) {
+ album = new ClusterAlbum(childPath, dataManager, this);
+ }
+ album.setMediaItems(clustering.getCluster(i));
+ album.setName(childName);
+ mAlbums.add(album);
+ }
+ }
+
+ private void updateClustersContents() {
+ final HashSet<Path> existing = new HashSet<Path>();
+ mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ 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..542dda27f
--- /dev/null
+++ b/src/com/android/gallery3d/data/Clustering.java
@@ -0,0 +1,26 @@
+/*
+ * 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);
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
new file mode 100644
index 000000000..8ca2077a4
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -0,0 +1,87 @@
+/*
+ * 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.app.GalleryApp;
+
+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 {
+ private static final String TAG = "ComboAlbum";
+ private final MediaSet[] mSets;
+ private final 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 String getName() {
+ return mName;
+ }
+
+ @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;
+ }
+
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
new file mode 100644
index 000000000..aa196039d
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -0,0 +1,80 @@
+/*
+ * 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;
+
+// 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 {
+ 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 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;
+ }
+
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+}
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..f7dac5ebd
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -0,0 +1,333 @@
+/*
+ * 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;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSource.PathId;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+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 {
+ 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();
+
+ 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/{/mtp,/local/all,/picasa/all}";
+ private static final String TOP_IMAGE_SET_PATH =
+ "/combo/{/mtp,/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> {
+ 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 MtpSource(mApplication));
+ addSource(new ComboSource(mApplication));
+ addSource(new ClusterSource(mApplication));
+ addSource(new FilterSource(mApplication));
+ addSource(new UriSource(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) {
+ mSourceMap.put(source.getPrefix(), source);
+ }
+
+ public MediaObject peekMediaObject(Path path) {
+ return path.getObject();
+ }
+
+ public MediaSet peekMediaSet(Path path) {
+ return (MediaSet) path.getObject();
+ }
+
+ public MediaObject getMediaObject(Path path) {
+ 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;
+ }
+
+ MediaObject object = source.createMediaObject(path);
+ if (object == null) {
+ Log.w(TAG, "cannot create media object: " + path);
+ }
+ return object;
+ }
+
+ 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 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 MediaDetails getDetails(Path path) {
+ return getMediaObject(path).getDetails();
+ }
+
+ public void cache(Path path, int flag) {
+ getMediaObject(path).cache(flag);
+ }
+
+ public Path findPathByUri(Uri uri) {
+ if (uri == null) return null;
+ for (MediaSource source : mSourceMap.values()) {
+ Path path = source.findPathByUri(uri);
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
new file mode 100644
index 000000000..e7ae638c2
--- /dev/null
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -0,0 +1,173 @@
+/*
+ * 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.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+
+public class DecodeUtils {
+ private static final String TAG = "DecodeService";
+
+ private static class DecodeCanceller implements CancelListener {
+ Options mOptions;
+ public DecodeCanceller(Options options) {
+ mOptions = options;
+ }
+ public void onCancel() {
+ mOptions.requestCancelDecode();
+ }
+ }
+
+ public static Bitmap requestDecode(JobContext jc, final String filePath,
+ Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeFile(filePath, options));
+ }
+
+ public static Bitmap requestDecode(JobContext jc, byte[] bytes,
+ Options options) {
+ return requestDecode(jc, bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap requestDecode(JobContext jc, byte[] bytes, int offset,
+ int length, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(bytes, offset, length, options));
+ }
+
+ public static Bitmap requestDecode(JobContext jc, final String filePath,
+ Options options, int targetSize) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(filePath);
+ FileDescriptor fd = fis.getFD();
+ return requestDecode(jc, fd, options, targetSize);
+ } catch (Exception ex) {
+ Log.w(TAG, ex);
+ return null;
+ } finally {
+ Utils.closeSilently(fis);
+ }
+ }
+
+ public static Bitmap requestDecode(JobContext jc, FileDescriptor fd,
+ Options options, int targetSize) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ if (jc.isCancelled()) return null;
+
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ options.outWidth, options.outHeight, targetSize);
+ options.inJustDecodeBounds = false;
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeFileDescriptor(fd, null, options));
+ }
+
+ public static Bitmap requestDecode(JobContext jc,
+ FileDescriptor fileDescriptor, Rect paddings, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ return ensureGLCompatibleBitmap(BitmapFactory.decodeFileDescriptor
+ (fileDescriptor, paddings, 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 requestCreateBitmapRegionDecoder(
+ 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 requestCreateBitmapRegionDecoder(
+ JobContext jc, String filePath, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(filePath, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+ JobContext jc, FileDescriptor fd, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(fd, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+ JobContext jc, Uri uri, ContentResolver resolver,
+ boolean shareable) {
+ ParcelFileDescriptor pfd = null;
+ try {
+ pfd = resolver.openFileDescriptor(uri, "r");
+ return BitmapRegionDecoder.newInstance(
+ pfd.getFileDescriptor(), shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ } finally {
+ Utils.closeSilently(pfd);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
new file mode 100644
index 000000000..30ba668c3
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -0,0 +1,398 @@
+/*
+ * 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;
+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 android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.WeakHashMap;
+
+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;
+ private WeakHashMap<Object, Entry> mAssociateMap = new WeakHashMap<Object, Entry>();
+
+ 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 lookup(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;
+ }
+ }
+ 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);
+ }
+
+ public void associateWith(Object object) {
+ mAssociateMap.put(Utils.checkNotNull(object), this);
+ }
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+ }
+
+ 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() {
+ 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..9632db984
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadUtils.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 com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.ByteArrayOutputStream;
+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 byte[] requestDownload(JobContext jc, URL url) {
+ ByteArrayOutputStream baos = null;
+ try {
+ baos = new ByteArrayOutputStream();
+ if (!download(jc, url, baos)) {
+ return null;
+ }
+ return baos.toByteArray();
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ } finally {
+ Utils.closeSilently(baos);
+ }
+ }
+
+ 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() {
+ 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/Face.java b/src/com/android/gallery3d/data/Face.java
new file mode 100644
index 000000000..cc1a2d3dc
--- /dev/null
+++ b/src/com/android/gallery3d/data/Face.java
@@ -0,0 +1,56 @@
+/*
+ * 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.common.Utils;
+
+public class Face implements Comparable<Face> {
+ private String mName;
+ private String mPersonId;
+
+ public Face(String name, String personId) {
+ mName = name;
+ mPersonId = personId;
+ Utils.assertTrue(mName != null && mPersonId != null);
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getPersonId() {
+ return mPersonId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Face) {
+ Face face = (Face) obj;
+ return mPersonId.equals(face.mPersonId);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPersonId.hashCode();
+ }
+
+ public int compareTo(Face another) {
+ return mPersonId.compareTo(another.mPersonId);
+ }
+}
diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java
new file mode 100644
index 000000000..6ed73b560
--- /dev/null
+++ b/src/com/android/gallery3d/data/FaceClustering.java
@@ -0,0 +1,94 @@
+/*
+ * 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.R;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class FaceClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FaceClustering";
+
+ private ArrayList<ArrayList<Path>> mClusters;
+ private String[] mNames;
+ private String mUntaggedString;
+
+ public FaceClustering(Context context) {
+ mUntaggedString = context.getResources().getString(R.string.untagged);
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final TreeMap<Face, ArrayList<Path>> map =
+ new TreeMap<Face, ArrayList<Path>>();
+ final ArrayList<Path> untagged = new ArrayList<Path>();
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ public void consume(int index, MediaItem item) {
+ Path path = item.getPath();
+
+ Face[] faces = item.getFaces();
+ if (faces == null || faces.length == 0) {
+ untagged.add(path);
+ return;
+ }
+ for (int j = 0; j < faces.length; j++) {
+ Face key = faces[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<Face, ArrayList<Path>> entry : map.entrySet()) {
+ mNames[i++] = entry.getKey().getName();
+ 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/FilterSet.java b/src/com/android/gallery3d/data/FilterSet.java
new file mode 100644
index 000000000..9cb7e02ef
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterSet.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;
+
+// FilterSet filters a base MediaSet according to a condition. Currently the
+// condition is a matching media type. It can be extended to other conditions
+// if needed.
+public class FilterSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterSet";
+
+ 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 FilterSet(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() {
+ 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() {
+ 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/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java
new file mode 100644
index 000000000..d1a04c995
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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 FilterSource extends MediaSource {
+ private static final String TAG = "FilterSource";
+ private static final int FILTER_BY_MEDIATYPE = 0;
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+
+ public FilterSource(GalleryApp application) {
+ super("filter");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE);
+ }
+
+ // The name we accept is:
+ // /filter/mediatype/k/{set}
+ // where k is the media type we want.
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ int matchType = mMatcher.match(path);
+ int mediaType = mMatcher.getIntVar(0);
+ String setsName = mMatcher.getVar(1);
+ DataManager dataManager = mApplication.getDataManager();
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ switch (matchType) {
+ case FILTER_BY_MEDIATYPE:
+ return new FilterSet(path, dataManager, sets[0], mediaType);
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
new file mode 100644
index 000000000..104ff4839
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.java
@@ -0,0 +1,89 @@
+/*
+ * 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;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.ImageCacheService.ImageData;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+abstract class ImageCacheRequest implements Job<Bitmap> {
+ private static final String TAG = "ImageCacheRequest";
+
+ protected GalleryApp mApplication;
+ private Path mPath;
+ private int mType;
+ private int mTargetSize;
+
+ public ImageCacheRequest(GalleryApp application,
+ Path path, int type, int targetSize) {
+ mApplication = application;
+ mPath = path;
+ mType = type;
+ mTargetSize = targetSize;
+ }
+
+ public Bitmap run(JobContext jc) {
+ String debugTag = mPath + "," +
+ ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" :
+ (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?");
+ ImageCacheService cacheService = mApplication.getImageCacheService();
+
+ ImageData data = cacheService.getImageData(mPath, mType);
+ if (jc.isCancelled()) return null;
+
+ if (data != null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = DecodeUtils.requestDecode(jc, data.mData,
+ data.mOffset, data.mData.length - data.mOffset, options);
+ if (bitmap == null && !jc.isCancelled()) {
+ Log.w(TAG, "decode cached failed " + debugTag);
+ }
+ return bitmap;
+ } else {
+ 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.resizeDownAndCropCenter(bitmap,
+ mTargetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap,
+ mTargetSize, true);
+ }
+ if (jc.isCancelled()) return null;
+
+ byte[] array = BitmapUtils.compressBitmap(bitmap);
+ if (jc.isCancelled()) return null;
+
+ cacheService.putImageData(mPath, 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..3adce1332
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheService.java
@@ -0,0 +1,105 @@
+/*
+ * 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.BlobCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Context;
+
+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 = 3;
+
+ 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);
+ }
+
+ public static class ImageData {
+ public ImageData(byte[] data, int offset) {
+ mData = data;
+ mOffset = offset;
+ }
+ public byte[] mData;
+ public int mOffset;
+ }
+
+ public ImageData getImageData(Path path, int type) {
+ byte[] key = makeKey(path, type);
+ long cacheKey = Utils.crc64Long(key);
+ try {
+ byte[] value = null;
+ synchronized (mCache) {
+ value = mCache.lookup(cacheKey);
+ }
+ if (value == null) return null;
+ if (isSameKey(key, value)) {
+ int offset = key.length;
+ return new ImageData(value, offset);
+ }
+ } catch (IOException ex) {
+ // ignore.
+ }
+ return null;
+ }
+
+ public void putImageData(Path path, int type, byte[] value) {
+ byte[] key = makeKey(path, 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.
+ }
+ }
+ }
+
+ private static byte[] makeKey(Path path, int type) {
+ return GalleryUtils.getBytes(path.toString() + "+" + 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..5bd4398b4
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -0,0 +1,252 @@
+/*
+ * 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;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+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 mBucketName;
+ 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;
+ mBucketName = 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,
+ LocalAlbumSet.getBucketName(application.getContentResolver(),
+ bucketId));
+ }
+
+ @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) {
+ 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 mBucketName;
+ }
+
+ @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;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
new file mode 100644
index 000000000..60bef9a33
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -0,0 +1,263 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+
+// 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 {
+ 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 String TAG = "LocalAlbumSet";
+ private static final String EXTERNAL_MEDIA = "external";
+
+ // The indices should match the following 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;
+
+ private static final Uri mBaseUri = Files.getContentUri(EXTERNAL_MEDIA);
+ private static final Uri mWatchUriImage = Images.Media.EXTERNAL_CONTENT_URI;
+ private static final Uri mWatchUriVideo = Video.Media.EXTERNAL_CONTENT_URI;
+
+ // The order is import it must match to the index in MediaStore.
+ private static final String[] PROJECTION_BUCKET = {
+ ImageColumns.BUCKET_ID,
+ FileColumns.MEDIA_TYPE,
+ ImageColumns.BUCKET_DISPLAY_NAME };
+
+ private final GalleryApp mApplication;
+ private final int mType;
+ private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+ private final ChangeNotifier mNotifierImage;
+ private final ChangeNotifier mNotifierVideo;
+ private final String mName;
+
+ public LocalAlbumSet(Path path, GalleryApp application) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mType = getTypeFromPath(path);
+ mNotifierImage = new ChangeNotifier(this, mWatchUriImage, application);
+ mNotifierVideo = new ChangeNotifier(this, mWatchUriVideo, 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());
+ }
+ if ("all".equals(name[1])) return MEDIA_TYPE_ALL;
+ if ("image".equals(name[1])) return MEDIA_TYPE_IMAGE;
+ if ("video".equals(name[1])) return MEDIA_TYPE_VIDEO;
+ throw new IllegalArgumentException(path.toString());
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return mAlbums.get(index);
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mAlbums.size();
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ private BucketEntry[] loadBucketEntries(Cursor cursor) {
+ HashSet<BucketEntry> buffer = new HashSet<BucketEntry>();
+ int typeBits = 0;
+ if ((mType & MEDIA_TYPE_IMAGE) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
+ }
+ if ((mType & MEDIA_TYPE_VIDEO) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
+ }
+ try {
+ while (cursor.moveToNext()) {
+ if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
+ buffer.add(new BucketEntry(
+ cursor.getInt(INDEX_BUCKET_ID),
+ cursor.getString(INDEX_BUCKET_NAME)));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return buffer.toArray(new BucketEntry[buffer.size()]);
+ }
+
+
+ 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;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected ArrayList<MediaSet> loadSubMediaSets() {
+ // Note: it will be faster if we only select media_type and bucket_id.
+ // need to test the performance if that is worth
+
+ Uri uri = mBaseUri.buildUpon().
+ appendQueryParameter("distinct", "true").build();
+ GalleryUtils.assertNotInRenderThread();
+ Cursor cursor = mApplication.getContentResolver().query(
+ uri, PROJECTION_BUCKET, null, null, null);
+ if (cursor == null) {
+ Log.w(TAG, "cannot open local database: " + uri);
+ return new ArrayList<MediaSet>();
+ }
+ BucketEntry[] entries = loadBucketEntries(cursor);
+ int offset = 0;
+
+ int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
+ if (index != -1) {
+ Utils.swap(entries, index, offset++);
+ }
+ index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
+ if (index != -1) {
+ Utils.swap(entries, index, offset++);
+ }
+
+ Arrays.sort(entries, offset, entries.length, new Comparator<BucketEntry>() {
+ @Override
+ public int compare(BucketEntry a, BucketEntry b) {
+ int result = a.bucketName.compareTo(b.bucketName);
+ return result != 0
+ ? result
+ : Utils.compare(a.bucketId, b.bucketId);
+ }
+ });
+ ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
+ DataManager dataManager = mApplication.getDataManager();
+ for (BucketEntry entry : entries) {
+ albums.add(getLocalAlbum(dataManager,
+ mType, mPath, entry.bucketId, entry.bucketName));
+ }
+ for (int i = 0, n = albums.size(); i < n; ++i) {
+ albums.get(i).reload();
+ }
+ return albums;
+ }
+
+ private MediaSet getLocalAlbum(
+ DataManager manager, int type, Path parent, int id, String name) {
+ 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)});
+ }
+ throw new IllegalArgumentException(String.valueOf(type));
+ }
+
+ public static String getBucketName(ContentResolver resolver, int bucketId) {
+ Uri uri = mBaseUri.buildUpon()
+ .appendQueryParameter("limit", "1")
+ .build();
+
+ Cursor cursor = resolver.query(
+ uri, PROJECTION_BUCKET, "bucket_id = ?",
+ new String[]{String.valueOf(bucketId)}, null);
+
+ if (cursor == null) {
+ Log.w(TAG, "query fail: " + uri);
+ return "";
+ }
+ try {
+ return cursor.moveToNext()
+ ? cursor.getString(INDEX_BUCKET_NAME)
+ : "";
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public long reload() {
+ // "|" is used instead of "||" because we want to clear both flags.
+ if (mNotifierImage.isDirty() | mNotifierVideo.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ mAlbums = loadSubMediaSets();
+ }
+ return mDataVersion;
+ }
+
+ // For debug only. Fake there is a ContentObserver.onChange() event.
+ void fakeChange() {
+ mNotifierImage.fakeChange();
+ mNotifierVideo.fakeChange();
+ }
+
+ private static class BucketEntry {
+ public String bucketName;
+ public int bucketId;
+
+ 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/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
new file mode 100644
index 000000000..f3dedf037
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -0,0 +1,289 @@
+/*
+ * 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;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.UpdateHelper;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+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.media.ExifInterface;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+
+import java.io.File;
+import java.io.IOException;
+
+// LocalImage represents an image in the local storage.
+public class LocalImage extends LocalMediaItem {
+ private static final int THUMBNAIL_TARGET_SIZE = 640;
+ private static final int MICROTHUMBNAIL_TARGET_SIZE = 200;
+
+ 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_ID = 11;
+
+ 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
+ };
+
+ private final GalleryApp mApplication;
+
+ public int rotation;
+
+ 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);
+ filePath = cursor.getString(INDEX_DATA);
+ rotation = cursor.getInt(INDEX_ORIENTATION);
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE_ID);
+ }
+
+ @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_ID));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalImageRequest(mApplication, mPath, type, filePath);
+ }
+
+ public static class LocalImageRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalImageRequest(GalleryApp application, Path path, int type,
+ String localFilePath) {
+ super(application, path, type, getTargetSize(type));
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public Bitmap onDecodeOriginal(JobContext jc, int type) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return DecodeUtils.requestDecode(
+ jc, mLocalFilePath, options, getTargetSize(type));
+ }
+ }
+
+ static int getTargetSize(int type) {
+ switch (type) {
+ case TYPE_THUMBNAIL:
+ return THUMBNAIL_TARGET_SIZE;
+ case TYPE_MICROTHUMBNAIL:
+ return MICROTHUMBNAIL_TARGET_SIZE;
+ default:
+ throw new RuntimeException(
+ "should only request thumb/microthumb from cache");
+ }
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new LocalLargeImageRequest(filePath);
+ }
+
+ public static class LocalLargeImageRequest
+ implements Job<BitmapRegionDecoder> {
+ String mLocalFilePath;
+
+ public LocalLargeImageRequest(String localFilePath) {
+ mLocalFilePath = localFilePath;
+ }
+
+ public BitmapRegionDecoder run(JobContext jc) {
+ return DecodeUtils.requestCreateBitmapRegionDecoder(
+ jc, mLocalFilePath, false);
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ 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 delete() {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ mApplication.getContentResolver().delete(baseUri, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ private static String getExifOrientation(int orientation) {
+ switch (orientation) {
+ case 0:
+ return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
+ case 90:
+ return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
+ case 180:
+ return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
+ case 270:
+ return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
+ default:
+ throw new AssertionError("invalid: " + orientation);
+ }
+ }
+
+ @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")) {
+ try {
+ ExifInterface exif = new ExifInterface(filePath);
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ getExifOrientation(rotation));
+ exif.saveAttributes();
+ } catch (IOException e) {
+ Log.w(TAG, "cannot set exif data: " + filePath);
+ }
+
+ // We need to update the filesize as well
+ fileSize = new File(filePath).length();
+ values.put(Images.Media.SIZE, fileSize);
+ }
+
+ 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));
+ MediaDetails.extractExifInfo(details, filePath);
+ return details;
+ }
+
+ @Override
+ public int getRotation() {
+ return rotation;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java
new file mode 100644
index 000000000..a76fedf32
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -0,0 +1,103 @@
+/*
+ * 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.GalleryUtils;
+
+import android.database.Cursor;
+
+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 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(dateTakenInMs)));
+
+ 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;
+ }
+
+ 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..bb796d53a
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -0,0 +1,226 @@
+/*
+ * 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.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Comparator;
+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 String mName;
+ private FetchCache[] mFetcher;
+ private int mSupportedOperation;
+
+ // 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) {
+ super(path, INVALID_DATA_VERSION);
+ mComparator = comparator;
+ mSources = sources;
+ mName = sources.length == 0 ? "" : sources[0].getName();
+ for (MediaSet set : mSources) {
+ set.addContentListener(this);
+ }
+ }
+
+ 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]);
+ mName = mSources.length == 0 ? "" : mSources[0].getName();
+ }
+
+ 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 String getName() {
+ return mName;
+ }
+
+ @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..58ac22490
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalSource.java
@@ -0,0 +1,272 @@
+/*
+ * 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.Gallery;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+import android.content.ContentProviderClient;
+import android.content.ContentUris;
+import android.content.UriMatcher;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+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);
+ }
+
+ @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});
+ }
+ 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_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("/merge/{/local/image,/local/video}")
+ .getChild(id);
+ }
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri) {
+ 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);
+ }
+ }
+ } 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 LocalImage) {
+ return Path.fromString("/local/image/").getChild(
+ String.valueOf(((LocalImage) object).getBucketId()));
+ } else if (object instanceof LocalVideo) {
+ return Path.fromString("/local/video/").getChild(
+ String.valueOf(((LocalVideo) 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> {
+ 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..d1498e856
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -0,0 +1,213 @@
+/*
+ * 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.common.BitmapUtils;
+import com.android.gallery3d.util.UpdateHelper;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import java.io.File;
+
+// LocalVideo represents a video in the local storage.
+public class LocalVideo extends LocalMediaItem {
+
+ 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_ID = 11;
+
+ 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
+ };
+
+ private final GalleryApp mApplication;
+ private static Bitmap sOverlay;
+
+ 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);
+ filePath = cursor.getString(INDEX_DATA);
+ durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE_ID);
+ }
+
+ @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_ID));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalVideoRequest(mApplication, getPath(), type, filePath);
+ }
+
+ public static class LocalVideoRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalVideoRequest(GalleryApp application, Path path, int type,
+ String localFilePath) {
+ super(application, path, type, LocalImage.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;
+ }
+
+ @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 Uri.fromFile(new File(filePath));
+ }
+
+ @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;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java
new file mode 100644
index 000000000..3cb1399e5
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocationClustering.java
@@ -0,0 +1,304 @@
+/*
+ * 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.util.ReverseGeocoder;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+class LocationClustering extends Clustering {
+ 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 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);
+ }
+
+ @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() {
+ 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) {
+ 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 * (float) Math.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..1b56ac42e
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -0,0 +1,162 @@
+/*
+ * 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 android.media.ExifInterface;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.TreeMap;
+import java.util.Map.Entry;
+
+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 int getFlashReturn() {
+ return (mState & FLASH_RETURN_MASK) >> 1;
+ }
+
+ public int getFlashMode() {
+ return (mState & FLASH_MODE_MASK) >> 3;
+ }
+
+ public boolean isFlashPresent() {
+ return (mState & FLASH_FUNCTION_MASK) != 0;
+ }
+
+ public boolean isRedEyeModePresent() {
+ return (mState & FLASH_RED_EYE_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();
+ }
+
+ 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, ExifInterface exif, String tag,
+ int key) {
+ String value = exif.getAttribute(tag);
+ if (value != null) {
+ 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) {
+ try {
+ ExifInterface exif = new ExifInterface(filePath);
+ setExifData(details, exif, ExifInterface.TAG_FLASH, MediaDetails.INDEX_FLASH);
+ setExifData(details, exif, ExifInterface.TAG_IMAGE_WIDTH, MediaDetails.INDEX_WIDTH);
+ setExifData(details, exif, ExifInterface.TAG_IMAGE_LENGTH,
+ MediaDetails.INDEX_HEIGHT);
+ setExifData(details, exif, ExifInterface.TAG_MAKE, MediaDetails.INDEX_MAKE);
+ setExifData(details, exif, ExifInterface.TAG_MODEL, MediaDetails.INDEX_MODEL);
+ setExifData(details, exif, ExifInterface.TAG_APERTURE, MediaDetails.INDEX_APERTURE);
+ setExifData(details, exif, ExifInterface.TAG_ISO, MediaDetails.INDEX_ISO);
+ setExifData(details, exif, ExifInterface.TAG_WHITE_BALANCE,
+ MediaDetails.INDEX_WHITE_BALANCE);
+ setExifData(details, exif, ExifInterface.TAG_EXPOSURE_TIME,
+ MediaDetails.INDEX_EXPOSURE_TIME);
+
+ double data = exif.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0);
+ if (data != 0f) {
+ details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, data);
+ details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
+ }
+ } catch (IOException ex) {
+ // ignore it.
+ Log.w(TAG, "", ex);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
new file mode 100644
index 000000000..430d8327d
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -0,0 +1,75 @@
+/*
+ * 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.ThreadPool.Job;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+// 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 IMAGE_READY = 0;
+ public static final int IMAGE_WAIT = 1;
+ public static final int IMAGE_ERROR = -1;
+
+ // 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