summaryrefslogtreecommitdiffstats
path: root/src
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 /src
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
Diffstat (limited to 'src')
-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
179 files changed, 31671 insertions, 0 deletions
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 version) {
+ super(path, version);
+ }
+
+ public long getDateInMs() {
+ return 0;
+ }
+
+ public String getName() {
+ return null;
+ }
+
+ public void getLatLong(double[] latLong) {
+ latLong[0] = INVALID_LATLNG;
+ latLong[1] = INVALID_LATLNG;
+ }
+
+ public String[] getTags() {
+ return null;
+ }
+
+ public Face[] getFaces() {
+ return null;
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+
+ public long getSize() {
+ return 0;
+ }
+
+ public abstract String getMimeType();
+}
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
new file mode 100644
index 000000000..d0f1672fc
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.net.Uri;
+
+public abstract class MediaObject {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaObject";
+ public static final long INVALID_DATA_VERSION = -1;
+
+ // These are the bits returned from getSupportedOperations():
+ public static final int SUPPORT_DELETE = 1 << 0;
+ public static final int SUPPORT_ROTATE = 1 << 1;
+ public static final int SUPPORT_SHARE = 1 << 2;
+ public static final int SUPPORT_CROP = 1 << 3;
+ public static final int SUPPORT_SHOW_ON_MAP = 1 << 4;
+ public static final int SUPPORT_SETAS = 1 << 5;
+ public static final int SUPPORT_FULL_IMAGE = 1 << 6;
+ public static final int SUPPORT_PLAY = 1 << 7;
+ public static final int SUPPORT_CACHE = 1 << 8;
+ public static final int SUPPORT_EDIT = 1 << 9;
+ public static final int SUPPORT_INFO = 1 << 10;
+ public static final int SUPPORT_IMPORT = 1 << 11;
+ public static final int SUPPORT_ALL = 0xffffffff;
+
+ // These are the bits returned from getMediaType():
+ public static final int MEDIA_TYPE_UNKNOWN = 1;
+ public static final int MEDIA_TYPE_IMAGE = 2;
+ public static final int MEDIA_TYPE_VIDEO = 4;
+ public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO;
+
+ // These are flags for cache() and return values for getCacheFlag():
+ public static final int CACHE_FLAG_NO = 0;
+ public static final int CACHE_FLAG_SCREENNAIL = 1;
+ public static final int CACHE_FLAG_FULL = 2;
+
+ // These are return values for getCacheStatus():
+ public static final int CACHE_STATUS_NOT_CACHED = 0;
+ public static final int CACHE_STATUS_CACHING = 1;
+ public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2;
+ public static final int CACHE_STATUS_CACHED_FULL = 3;
+
+ private static long sVersionSerial = 0;
+
+ protected long mDataVersion;
+
+ protected final Path mPath;
+
+ public MediaObject(Path path, long version) {
+ path.setObject(this);
+ mPath = path;
+ mDataVersion = version;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public int getSupportedOperations() {
+ return 0;
+ }
+
+ public void delete() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void rotate(int degrees) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getContentUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getPlayUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getMediaType() {
+ return MEDIA_TYPE_UNKNOWN;
+ }
+
+ public boolean Import() {
+ throw new UnsupportedOperationException();
+ }
+
+ public MediaDetails getDetails() {
+ MediaDetails details = new MediaDetails();
+ return details;
+ }
+
+ public long getDataVersion() {
+ return mDataVersion;
+ }
+
+ public int getCacheFlag() {
+ return CACHE_FLAG_NO;
+ }
+
+ public int getCacheStatus() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long getCacheSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void cache(int flag) {
+ throw new UnsupportedOperationException();
+ }
+
+ public static synchronized long nextVersionNumber() {
+ return ++MediaObject.sVersionSerial;
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
new file mode 100644
index 000000000..99f00a0dd
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -0,0 +1,219 @@
+/*
+ * 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.Future;
+
+import java.util.ArrayList;
+import java.util.WeakHashMap;
+
+// MediaSet is a directory-like data structure.
+// It contains MediaItems and sub-MediaSets.
+//
+// The primary interface are:
+// getMediaItemCount(), getMediaItem() and
+// getSubMediaSetCount(), getSubMediaSet().
+//
+// getTotalMediaItemCount() returns the number of all MediaItems, including
+// those in sub-MediaSets.
+public abstract class MediaSet extends MediaObject {
+ public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
+ public static final int INDEX_NOT_FOUND = -1;
+
+ public MediaSet(Path path, long version) {
+ super(path, version);
+ }
+
+ public int getMediaItemCount() {
+ return 0;
+ }
+
+ // Returns the media items in the range [start, start + count).
+ //
+ // The number of media items returned may be less than the specified count
+ // if there are not enough media items available. The number of
+ // media items available may not be consistent with the return value of
+ // getMediaItemCount() because the contents of database may have already
+ // changed.
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ return new ArrayList<MediaItem>();
+ }
+
+ public int getSubMediaSetCount() {
+ return 0;
+ }
+
+ public MediaSet getSubMediaSet(int index) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ public boolean isLeafAlbum() {
+ return false;
+ }
+
+ public int getTotalMediaItemCount() {
+ int total = getMediaItemCount();
+ for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+ total += getSubMediaSet(i).getTotalMediaItemCount();
+ }
+ return total;
+ }
+
+ // TODO: we should have better implementation of sub classes
+ public int getIndexOfItem(Path path, int hint) {
+ // hint < 0 is handled below
+ // first, try to find it around the hint
+ int start = Math.max(0,
+ hint - MEDIAITEM_BATCH_FETCH_COUNT / 2);
+ ArrayList<MediaItem> list = getMediaItem(
+ start, MEDIAITEM_BATCH_FETCH_COUNT);
+ int index = getIndexOf(path, list);
+ if (index != INDEX_NOT_FOUND) return start + index;
+
+ // try to find it globally
+ start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0;
+ list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+ while (true) {
+ index = getIndexOf(path, list);
+ if (index != INDEX_NOT_FOUND) return start + index;
+ if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND;
+ start += MEDIAITEM_BATCH_FETCH_COUNT;
+ list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+ }
+ }
+
+ protected int getIndexOf(Path path, ArrayList<MediaItem> list) {
+ for (int i = 0, n = list.size(); i < n; ++i) {
+ if (list.get(i).mPath == path) return i;
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+ public abstract String getName();
+
+ private WeakHashMap<ContentListener, Object> mListeners =
+ new WeakHashMap<ContentListener, Object>();
+
+ // NOTE: The MediaSet only keeps a weak reference to the listener. The
+ // listener is automatically removed when there is no other reference to
+ // the listener.
+ public void addContentListener(ContentListener listener) {
+ if (mListeners.containsKey(listener)) {
+ throw new IllegalArgumentException();
+ }
+ mListeners.put(listener, null);
+ }
+
+ public void removeContentListener(ContentListener listener) {
+ if (!mListeners.containsKey(listener)) {
+ throw new IllegalArgumentException();
+ }
+ mListeners.remove(listener);
+ }
+
+ // This should be called by subclasses when the content is changed.
+ public void notifyContentChanged() {
+ for (ContentListener listener : mListeners.keySet()) {
+ listener.onContentDirty();
+ }
+ }
+
+ // Reload the content. Return the current data version. reload() should be called
+ // in the same thread as getMediaItem(int, int) and getSubMediaSet(int).
+ public abstract long reload();
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ details.addDetail(MediaDetails.INDEX_TITLE, getName());
+ return details;
+ }
+
+ // Enumerate all media items in this media set (including the ones in sub
+ // media sets), in an efficient order. ItemConsumer.consumer() will be
+ // called for each media item with its index.
+ public void enumerateMediaItems(ItemConsumer consumer) {
+ enumerateMediaItems(consumer, 0);
+ }
+
+ public void enumerateTotalMediaItems(ItemConsumer consumer) {
+ enumerateTotalMediaItems(consumer, 0);
+ }
+
+ public static interface ItemConsumer {
+ void consume(int index, MediaItem item);
+ }
+
+ // The default implementation uses getMediaItem() for enumerateMediaItems().
+ // Subclasses may override this and use more efficient implementations.
+ // Returns the number of items enumerated.
+ protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
+ int total = getMediaItemCount();
+ int start = 0;
+ while (start < total) {
+ int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start);
+ ArrayList<MediaItem> items = getMediaItem(start, count);
+ for (int i = 0, n = items.size(); i < n; i++) {
+ MediaItem item = items.get(i);
+ consumer.consume(startIndex + start + i, item);
+ }
+ start += count;
+ }
+ return total;
+ }
+
+ // Recursively enumerate all media items under this set.
+ // Returns the number of items enumerated.
+ protected int enumerateTotalMediaItems(
+ ItemConsumer consumer, int startIndex) {
+ int start = 0;
+ start += enumerateMediaItems(consumer, startIndex);
+ int m = getSubMediaSetCount();
+ for (int i = 0; i < m; i++) {
+ start += getSubMediaSet(i).enumerateTotalMediaItems(
+ consumer, startIndex + start);
+ }
+ return start;
+ }
+
+ public Future<Void> requestSync() {
+ return FUTURE_STUB;
+ }
+
+ private static final Future<Void> FUTURE_STUB = new Future<Void>() {
+ @Override
+ public void cancel() {}
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Void get() {
+ return null;
+ }
+
+ @Override
+ public void waitDone() {}
+ };
+}
diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
new file mode 100644
index 000000000..ae98e0fcc
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSource.java
@@ -0,0 +1,93 @@
+/*
+ * 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.data.MediaSet.ItemConsumer;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+public abstract class MediaSource {
+ private static final String TAG = "MediaSource";
+ private String mPrefix;
+
+ protected MediaSource(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public String getPrefix() {
+ return mPrefix;
+ }
+
+ public Path findPathByUri(Uri uri) {
+ return null;
+ }
+
+ public abstract MediaObject createMediaObject(Path path);
+
+ public void pause() {
+ }
+
+ public void resume() {
+ }
+
+ public Path getDefaultSetOf(Path item) {
+ return null;
+ }
+
+ public long getTotalUsedCacheSize() {
+ return 0;
+ }
+
+ public long getTotalTargetCacheSize() {
+ return 0;
+ }
+
+ public static class PathId {
+ public PathId(Path path, int id) {
+ this.path = path;
+ this.id = id;
+ }
+ public Path path;
+ public int id;
+ }
+
+ // Maps a list of Paths (all belong to this MediaSource) to MediaItems,
+ // and invoke consumer.consume() for each MediaItem with the given id.
+ //
+ // This default implementation uses getMediaObject for each Path. Subclasses
+ // may override this and provide more efficient implementation (like
+ // batching the database query).
+ public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+ int n = list.size();
+ for (int i = 0; i < n; i++) {
+ PathId pid = list.get(i);
+ MediaObject obj = pid.path.getObject();
+ if (obj == null) {
+ try {
+ obj = createMediaObject(pid.path);
+ } catch (Throwable th) {
+ Log.w(TAG, "cannot create media object: " + pid.path, th);
+ }
+ }
+ if (obj != null) {
+ consumer.consume(pid.id, (MediaItem) obj);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java
new file mode 100644
index 000000000..6991c1637
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpClient.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.mtp.MtpDevice;
+import android.mtp.MtpDeviceInfo;
+import android.mtp.MtpObjectInfo;
+import android.mtp.MtpStorageInfo;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class helps an application manage a list of connected MTP or PTP devices.
+ * It listens for MTP devices being attached and removed from the USB host bus
+ * and notifies the application when the MTP device list changes.
+ */
+public class MtpClient {
+
+ private static final String TAG = "MtpClient";
+
+ private static final String ACTION_USB_PERMISSION =
+ "android.mtp.MtpClient.action.USB_PERMISSION";
+
+ private final Context mContext;
+ private final UsbManager mUsbManager;
+ private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+ // mDevices contains all MtpDevices that have been seen by our client,
+ // so we can inform when the device has been detached.
+ // mDevices is also used for synchronization in this class.
+ private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
+ // List of MTP devices we should not try to open for which we are currently
+ // asking for permission to open.
+ private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>();
+ // List of MTP devices we should not try to open.
+ // We add devices to this list if the user canceled a permission request or we were
+ // unable to open the device.
+ private final ArrayList<String> mIgnoredDevices = new ArrayList<String>();
+
+ private final PendingIntent mPermissionIntent;
+
+ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ String deviceName = usbDevice.getDeviceName();
+
+ synchronized (mDevices) {
+ MtpDevice mtpDevice = mDevices.get(deviceName);
+
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+ if (mtpDevice != null) {
+ mDevices.remove(deviceName);
+ mRequestPermissionDevices.remove(deviceName);
+ mIgnoredDevices.remove(deviceName);
+ for (Listener listener : mListeners) {
+ listener.deviceRemoved(mtpDevice);
+ }
+ }
+ } else if (ACTION_USB_PERMISSION.equals(action)) {
+ mRequestPermissionDevices.remove(deviceName);
+ boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+ false);
+ Log.d(TAG, "ACTION_USB_PERMISSION: " + permission);
+ if (permission) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else {
+ // so we don't ask for permission again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * An interface for being notified when MTP or PTP devices are attached
+ * or removed. In the current implementation, only PTP devices are supported.
+ */
+ public interface Listener {
+ /**
+ * Called when a new device has been added
+ *
+ * @param device the new device that was added
+ */
+ public void deviceAdded(MtpDevice device);
+
+ /**
+ * Called when a new device has been removed
+ *
+ * @param device the device that was removed
+ */
+ public void deviceRemoved(MtpDevice device);
+ }
+
+ /**
+ * Tests to see if a {@link android.hardware.usb.UsbDevice}
+ * supports the PTP protocol (typically used by digital cameras)
+ *
+ * @param device the device to test
+ * @return true if the device is a PTP device.
+ */
+ static public boolean isCamera(UsbDevice device) {
+ int count = device.getInterfaceCount();
+ for (int i = 0; i < count; i++) {
+ UsbInterface intf = device.getInterface(i);
+ if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
+ intf.getInterfaceSubclass() == 1 &&
+ intf.getInterfaceProtocol() == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * MtpClient constructor
+ *
+ * @param context the {@link android.content.Context} to use for the MtpClient
+ */
+ public MtpClient(Context context) {
+ mContext = context;
+ mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
+ mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ filter.addAction(ACTION_USB_PERMISSION);
+ context.registerReceiver(mUsbReceiver, filter);
+ }
+
+ /**
+ * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP
+ * device and return an {@link android.mtp.MtpDevice} for it.
+ *
+ * @param device the device to open
+ * @return an MtpDevice for the device.
+ */
+ private MtpDevice openDeviceLocked(UsbDevice usbDevice) {
+ String deviceName = usbDevice.getDeviceName();
+
+ // don't try to open devices that we have decided to ignore
+ // or are currently asking permission for
+ if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName)
+ && !mRequestPermissionDevices.contains(deviceName)) {
+ if (!mUsbManager.hasPermission(usbDevice)) {
+ mUsbManager.requestPermission(usbDevice, mPermissionIntent);
+ mRequestPermissionDevices.add(deviceName);
+ } else {
+ UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice);
+ if (connection != null) {
+ MtpDevice mtpDevice = new MtpDevice(usbDevice);
+ if (mtpDevice.open(connection)) {
+ mDevices.put(usbDevice.getDeviceName(), mtpDevice);
+ return mtpDevice;
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Closes all resources related to the MtpClient object
+ */
+ public void close() {
+ mContext.unregisterReceiver(mUsbReceiver);
+ }
+
+ /**
+ * Registers a {@link android.mtp.MtpClient.Listener} interface to receive
+ * notifications when MTP or PTP devices are added or removed.
+ *
+ * @param listener the listener to register
+ */
+ public void addListener(Listener listener) {
+ synchronized (mDevices) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a {@link android.mtp.MtpClient.Listener} interface.
+ *
+ * @param listener the listener to unregister
+ */
+ public void removeListener(Listener listener) {
+ synchronized (mDevices) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+ * with the given name.
+ *
+ * @param deviceName the name of the USB device
+ * @return the MtpDevice, or null if it does not exist
+ */
+ public MtpDevice getDevice(String deviceName) {
+ synchronized (mDevices) {
+ return mDevices.get(deviceName);
+ }
+ }
+
+ /**
+ * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+ * with the given ID.
+ *
+ * @param id the ID of the USB device
+ * @return the MtpDevice, or null if it does not exist
+ */
+ public MtpDevice getDevice(int id) {
+ synchronized (mDevices) {
+ return mDevices.get(UsbDevice.getDeviceName(id));
+ }
+ }
+
+ /**
+ * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}.
+ *
+ * @return the list of MtpDevices
+ */
+ public List<MtpDevice> getDeviceList() {
+ synchronized (mDevices) {
+ // Query the USB manager since devices might have attached
+ // before we added our listener.
+ for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
+ if (mDevices.get(usbDevice.getDeviceName()) == null) {
+ openDeviceLocked(usbDevice);
+ }
+ }
+
+ return new ArrayList<MtpDevice>(mDevices.values());
+ }
+ }
+
+ /**
+ * Retrieves a list of all {@link android.mtp.MtpStorageInfo}
+ * for the MTP or PTP device with the given USB device name
+ *
+ * @param deviceName the name of the USB device
+ * @return the list of MtpStorageInfo
+ */
+ public List<MtpStorageInfo> getStorageList(String deviceName) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ int[] storageIds = device.getStorageIds();
+ if (storageIds == null) {
+ return null;
+ }
+
+ int length = storageIds.length;
+ ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length);
+ for (int i = 0; i < length; i++) {
+ MtpStorageInfo info = device.getStorageInfo(storageIds[i]);
+ if (info == null) {
+ Log.w(TAG, "getStorageInfo failed");
+ } else {
+ storageList.add(info);
+ }
+ }
+ return storageList;
+ }
+
+ /**
+ * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on
+ * the MTP or PTP device with the given USB device name with the given
+ * object handle
+ *
+ * @param deviceName the name of the USB device
+ * @param objectHandle handle of the object to query
+ * @return the MtpObjectInfo
+ */
+ public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getObjectInfo(objectHandle);
+ }
+
+ /**
+ * Deletes an object on the MTP or PTP device with the given USB device name.
+ *
+ * @param deviceName the name of the USB device
+ * @param objectHandle handle of the object to delete
+ * @return true if the deletion succeeds
+ */
+ public boolean deleteObject(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return false;
+ }
+ return device.deleteObject(objectHandle);
+ }
+
+ /**
+ * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects
+ * on the MTP or PTP device with the given USB device name and given storage ID
+ * and/or object handle.
+ * If the object handle is zero, then all objects in the root of the storage unit
+ * will be returned. Otherwise, all immediate children of the object will be returned.
+ * If the storage ID is also zero, then all objects on all storage units will be returned.
+ *
+ * @param deviceName the name of the USB device
+ * @param storageId the ID of the storage unit to query, or zero for all
+ * @param objectHandle the handle of the parent object to query, or zero for the storage root
+ * @return the list of MtpObjectInfo
+ */
+ public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ if (objectHandle == 0) {
+ // all objects in root of storage
+ objectHandle = 0xFFFFFFFF;
+ }
+ int[] handles = device.getObjectHandles(storageId, 0, objectHandle);
+ if (handles == null) {
+ return null;
+ }
+
+ int length = handles.length;
+ ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length);
+ for (int i = 0; i < length; i++) {
+ MtpObjectInfo info = device.getObjectInfo(handles[i]);
+ if (info == null) {
+ Log.w(TAG, "getObjectInfo failed");
+ } else {
+ objectList.add(info);
+ }
+ }
+ return objectList;
+ }
+
+ /**
+ * Returns the data for an object as a byte array.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @param objectSize the size of the object (this should match
+ * {@link android.mtp.MtpObjectInfo#getCompressedSize}
+ * @return the object's data, or null if reading fails
+ */
+ public byte[] getObject(String deviceName, int objectHandle, int objectSize) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getObject(objectHandle, objectSize);
+ }
+
+ /**
+ * Returns the thumbnail data for an object as a byte array.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @return the object's thumbnail, or null if reading fails
+ */
+ public byte[] getThumbnail(String deviceName, int objectHandle) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return null;
+ }
+ return device.getThumbnail(objectHandle);
+ }
+
+ /**
+ * Copies the data for an object to a file in external storage.
+ *
+ * @param deviceName the name of the USB device containing the object
+ * @param objectHandle handle of the object to read
+ * @param destPath path to destination for the file transfer.
+ * This path should be in the external storage as defined by
+ * {@link android.os.Environment#getExternalStorageDirectory}
+ * @return true if the file transfer succeeds
+ */
+ public boolean importFile(String deviceName, int objectHandle, String destPath) {
+ MtpDevice device = getDevice(deviceName);
+ if (device == null) {
+ return false;
+ }
+ return device.importFile(objectHandle, destPath);
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpContext.java b/src/com/android/gallery3d/data/MtpContext.java
new file mode 100644
index 000000000..652849445
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpContext.java
@@ -0,0 +1,141 @@
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.mtp.MtpObjectInfo;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MtpContext implements MtpClient.Listener {
+ private static final String TAG = "MtpContext";
+
+ public static final String NAME_IMPORTED_FOLDER = "Imported";
+
+ private ScannerClient mScannerClient;
+ private Context mContext;
+ private MtpClient mClient;
+
+ private static final class ScannerClient implements MediaScannerConnectionClient {
+ ArrayList<String> mPaths = new ArrayList<String>();
+ MediaScannerConnection mScannerConnection;
+ boolean mConnected;
+ Object mLock = new Object();
+
+ public ScannerClient(Context context) {
+ mScannerConnection = new MediaScannerConnection(context, this);
+ }
+
+ public void scanPath(String path) {
+ synchronized (mLock) {
+ if (mConnected) {
+ mScannerConnection.scanFile(path, null);
+ } else {
+ mPaths.add(path);
+ mScannerConnection.connect();
+ }
+ }
+ }
+
+ @Override
+ public void onMediaScannerConnected() {
+ synchronized (mLock) {
+ mConnected = true;
+ if (!mPaths.isEmpty()) {
+ for (String path : mPaths) {
+ mScannerConnection.scanFile(path, null);
+ }
+ mPaths.clear();
+ }
+ }
+ }
+
+ @Override
+ public void onScanCompleted(String path, Uri uri) {
+ }
+ }
+
+ public MtpContext(Context context) {
+ mContext = context;
+ mScannerClient = new ScannerClient(context);
+ mClient = new MtpClient(mContext);
+ }
+
+ public void pause() {
+ mClient.removeListener(this);
+ }
+
+ public void resume() {
+ mClient.addListener(this);
+ notifyDirty();
+ }
+
+ public void deviceAdded(android.mtp.MtpDevice device) {
+ notifyDirty();
+ showToast(R.string.camera_connected);
+ }
+
+ public void deviceRemoved(android.mtp.MtpDevice device) {
+ notifyDirty();
+ showToast(R.string.camera_disconnected);
+ }
+
+ private void notifyDirty() {
+ mContext.getContentResolver().notifyChange(Uri.parse("mtp://"), null);
+ }
+
+ private void showToast(final int msg) {
+ Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
+ }
+
+ public MtpClient getMtpClient() {
+ return mClient;
+ }
+
+ public boolean copyFile(String deviceName, MtpObjectInfo objInfo) {
+ if (GalleryUtils.hasSpaceForSize(objInfo.getCompressedSize())) {
+ File dest = Environment.getExternalStorageDirectory();
+ dest = new File(dest, NAME_IMPORTED_FOLDER);
+ dest.mkdirs();
+ String destPath = new File(dest, objInfo.getName()).getAbsolutePath();
+ int objectId = objInfo.getObjectHandle();
+ if (mClient.importFile(deviceName, objectId, destPath)) {
+ mScannerClient.scanPath(destPath);
+ return true;
+ }
+ } else {
+ Log.w(TAG, "No space to import " + objInfo.getName() +
+ " whose size = " + objInfo.getCompressedSize());
+ }
+ return false;
+ }
+
+ public boolean copyAlbum(String deviceName, String albumName,
+ List<MtpObjectInfo> children) {
+ File dest = Environment.getExternalStorageDirectory();
+ dest = new File(dest, albumName);
+ dest.mkdirs();
+ int success = 0;
+ for (MtpObjectInfo child : children) {
+ if (!GalleryUtils.hasSpaceForSize(child.getCompressedSize())) continue;
+
+ File importedFile = new File(dest, child.getName());
+ String path = importedFile.getAbsolutePath();
+ if (mClient.importFile(deviceName, child.getObjectHandle(), path)) {
+ mScannerClient.scanPath(path);
+ success++;
+ }
+ }
+ return success == children.size();
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpDevice.java b/src/com/android/gallery3d/data/MtpDevice.java
new file mode 100644
index 000000000..e654583c5
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpDevice.java
@@ -0,0 +1,174 @@
+/*
+ * 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.hardware.usb.UsbDevice;
+import android.mtp.MtpConstants;
+import android.mtp.MtpObjectInfo;
+import android.mtp.MtpStorageInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MtpDevice extends MediaSet {
+ private static final String TAG = "MtpDevice";
+
+ private final GalleryApp mApplication;
+ private final int mDeviceId;
+ private final String mDeviceName;
+ private final DataManager mDataManager;
+ private final MtpContext mMtpContext;
+ private final String mName;
+ private final ChangeNotifier mNotifier;
+ private final Path mItemPath;
+ private List<MtpObjectInfo> mJpegChildren;
+
+ public MtpDevice(Path path, GalleryApp application, int deviceId,
+ String name, MtpContext mtpContext) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mDeviceId = deviceId;
+ mDeviceName = UsbDevice.getDeviceName(deviceId);
+ mDataManager = application.getDataManager();
+ mMtpContext = mtpContext;
+ mName = name;
+ mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application);
+ mItemPath = Path.fromString("/mtp/item/" + String.valueOf(deviceId));
+ mJpegChildren = new ArrayList<MtpObjectInfo>();
+ }
+
+ public MtpDevice(Path path, GalleryApp application, int deviceId,
+ MtpContext mtpContext) {
+ this(path, application, deviceId,
+ MtpDeviceSet.getDeviceName(mtpContext, deviceId), mtpContext);
+ }
+
+ private List<MtpObjectInfo> loadItems() {
+ ArrayList<MtpObjectInfo> result = new ArrayList<MtpObjectInfo>();
+
+ List<MtpStorageInfo> storageList = mMtpContext.getMtpClient()
+ .getStorageList(mDeviceName);
+ if (storageList == null) return result;
+
+ for (MtpStorageInfo info : storageList) {
+ collectJpegChildren(info.getStorageId(), 0, result);
+ }
+
+ return result;
+ }
+
+ private void collectJpegChildren(int storageId, int objectId,
+ ArrayList<MtpObjectInfo> result) {
+ ArrayList<MtpObjectInfo> dirChildren = new ArrayList<MtpObjectInfo>();
+
+ queryChildren(storageId, objectId, result, dirChildren);
+
+ for (int i = 0, n = dirChildren.size(); i < n; i++) {
+ MtpObjectInfo info = dirChildren.get(i);
+ collectJpegChildren(storageId, info.getObjectHandle(), result);
+ }
+ }
+
+ private void queryChildren(int storageId, int objectId,
+ ArrayList<MtpObjectInfo> jpeg, ArrayList<MtpObjectInfo> dir) {
+ List<MtpObjectInfo> children = mMtpContext.getMtpClient().getObjectList(
+ mDeviceName, storageId, objectId);
+ if (children == null) return;
+
+ for (MtpObjectInfo obj : children) {
+ int format = obj.getFormat();
+ switch (format) {
+ case MtpConstants.FORMAT_JFIF:
+ case MtpConstants.FORMAT_EXIF_JPEG:
+ jpeg.add(obj);
+ break;
+ case MtpConstants.FORMAT_ASSOCIATION:
+ dir.add(obj);
+ break;
+ default:
+ Log.w(TAG, "other type: name = " + obj.getName()
+ + ", format = " + format);
+ }
+ }
+ }
+
+ public static MtpObjectInfo getObjectInfo(MtpContext mtpContext, int deviceId,
+ int objectId) {
+ String deviceName = UsbDevice.getDeviceName(deviceId);
+ return mtpContext.getMtpClient().getObjectInfo(deviceName, objectId);
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+ int begin = start;
+ int end = Math.min(start + count, mJpegChildren.size());
+
+ DataManager dataManager = mApplication.getDataManager();
+ for (int i = begin; i < end; i++) {
+ MtpObjectInfo child = mJpegChildren.get(i);
+ Path childPath = mItemPath.getChild(child.getObjectHandle());
+ MtpImage image = (MtpImage) dataManager.peekMediaObject(childPath);
+ if (image == null) {
+ image = new MtpImage(
+ childPath, mApplication, mDeviceId, child, mMtpContext);
+ } else {
+ image.updateContent(child);
+ }
+ result.add(image);
+ }
+ return result;
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return mJpegChildren.size();
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public long reload() {
+ if (mNotifier.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ mJpegChildren = loadItems();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_IMPORT;
+ }
+
+ @Override
+ public boolean Import() {
+ return mMtpContext.copyAlbum(mDeviceName, mName, mJpegChildren);
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpDeviceSet.java b/src/com/android/gallery3d/data/MtpDeviceSet.java
new file mode 100644
index 000000000..6521623d4
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpDeviceSet.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import android.mtp.MtpDeviceInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+// MtpDeviceSet -- MtpDevice -- MtpImage
+public class MtpDeviceSet extends MediaSet {
+ private static final String TAG = "MtpDeviceSet";
+
+ private GalleryApp mApplication;
+ private final ArrayList<MediaSet> mDeviceSet = new ArrayList<MediaSet>();
+ private final ChangeNotifier mNotifier;
+ private final MtpContext mMtpContext;
+ private final String mName;
+
+ public MtpDeviceSet(Path path, GalleryApp application, MtpContext mtpContext) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application);
+ mMtpContext = mtpContext;
+ mName = application.getResources().getString(R.string.set_label_mtp_devices);
+ }
+
+ private void loadDevices() {
+ DataManager dataManager = mApplication.getDataManager();
+ // Enumerate all devices
+ mDeviceSet.clear();
+ List<android.mtp.MtpDevice> devices = mMtpContext.getMtpClient().getDeviceList();
+ Log.v(TAG, "loadDevices: " + devices + ", size=" + devices.size());
+ for (android.mtp.MtpDevice mtpDevice : devices) {
+ int deviceId = mtpDevice.getDeviceId();
+ Path childPath = mPath.getChild(deviceId);
+ MtpDevice device = (MtpDevice) dataManager.peekMediaObject(childPath);
+ if (device == null) {
+ device = new MtpDevice(childPath, mApplication, deviceId, mMtpContext);
+ }
+ Log.d(TAG, "add device " + device);
+ mDeviceSet.add(device);
+ }
+
+ Collections.sort(mDeviceSet, MediaSetUtils.NAME_COMPARATOR);
+ for (int i = 0, n = mDeviceSet.size(); i < n; i++) {
+ mDeviceSet.get(i).reload();
+ }
+ }
+
+ public static String getDeviceName(MtpContext mtpContext, int deviceId) {
+ android.mtp.MtpDevice device = mtpContext.getMtpClient().getDevice(deviceId);
+ if (device == null) {
+ return "";
+ }
+ MtpDeviceInfo info = device.getDeviceInfo();
+ if (info == null) {
+ return "";
+ }
+ String manufacturer = info.getManufacturer().trim();
+ String model = info.getModel().trim();
+ return manufacturer + " " + model;
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return index < mDeviceSet.size() ? mDeviceSet.get(index) : null;
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mDeviceSet.size();
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public long reload() {
+ if (mNotifier.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ loadDevices();
+ }
+ return mDataVersion;
+ }
+}
diff --git a/src/com/android/gallery3d/data/MtpImage.java b/src/com/android/gallery3d/data/MtpImage.java
new file mode 100644
index 000000000..4766d88f8
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpImage.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.provider.GalleryProvider;
+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 android.graphics.BitmapRegionDecoder;
+import android.hardware.usb.UsbDevice;
+import android.mtp.MtpObjectInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class MtpImage extends MediaItem {
+ private static final String TAG = "MtpImage";
+
+ private final int mDeviceId;
+ private int mObjectId;
+ private int mObjectSize;
+ private long mDateTaken;
+ private String mFileName;
+ private final ThreadPool mThreadPool;
+ private final MtpContext mMtpContext;
+ private final MtpObjectInfo mObjInfo;
+ private final int mImageWidth;
+ private final int mImageHeight;
+
+ MtpImage(Path path, GalleryApp application, int deviceId,
+ MtpObjectInfo objInfo, MtpContext mtpContext) {
+ super(path, nextVersionNumber());
+ mDeviceId = deviceId;
+ mObjInfo = objInfo;
+ mObjectId = objInfo.getObjectHandle();
+ mObjectSize = objInfo.getCompressedSize();
+ mDateTaken = objInfo.getDateCreated();
+ mFileName = objInfo.getName();
+ mImageWidth = objInfo.getImagePixWidth();
+ mImageHeight = objInfo.getImagePixHeight();
+ mThreadPool = application.getThreadPool();
+ mMtpContext = mtpContext;
+ }
+
+ MtpImage(Path path, GalleryApp app, int deviceId, int objectId, MtpContext mtpContext) {
+ this(path, app, deviceId, MtpDevice.getObjectInfo(mtpContext, deviceId, objectId),
+ mtpContext);
+ }
+
+ @Override
+ public long getDateInMs() {
+ return mDateTaken;
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new Job<Bitmap>() {
+ public Bitmap run(JobContext jc) {
+ GetThumbnailBytes job = new GetThumbnailBytes();
+ byte[] thumbnail = mThreadPool.submit(job).get();
+ if (thumbnail == null) {
+ Log.w(TAG, "decoding thumbnail failed");
+ return null;
+ }
+ return DecodeUtils.requestDecode(jc, thumbnail, null);
+ }
+ };
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new Job<BitmapRegionDecoder>() {
+ public BitmapRegionDecoder run(JobContext jc) {
+ byte[] bytes = mMtpContext.getMtpClient().getObject(
+ UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize);
+ return DecodeUtils.requestCreateBitmapRegionDecoder(
+ jc, bytes, 0, bytes.length, false);
+ }
+ };
+ }
+
+ public byte[] getImageData() {
+ return mMtpContext.getMtpClient().getObject(
+ UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize);
+ }
+
+ @Override
+ public boolean Import() {
+ return mMtpContext.copyFile(UsbDevice.getDeviceName(mDeviceId), mObjInfo);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_FULL_IMAGE | SUPPORT_IMPORT;
+ }
+
+ private class GetThumbnailBytes implements Job<byte[]> {
+ public byte[] run(JobContext jc) {
+ return mMtpContext.getMtpClient().getThumbnail(
+ UsbDevice.getDeviceName(mDeviceId), mObjectId);
+ }
+ }
+
+ public void updateContent(MtpObjectInfo info) {
+ if (mObjectId != info.getObjectHandle() || mDateTaken != info.getDateCreated()) {
+ mObjectId = info.getObjectHandle();
+ mDateTaken = info.getDateCreated();
+ mDataVersion = nextVersionNumber();
+ }
+ }
+
+ @Override
+ public String getMimeType() {
+ // Currently only JPEG is supported in MTP.
+ return "image/jpeg";
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_IMAGE;
+ }
+
+ @Override
+ public long getSize() {
+ return mObjectSize;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ return GalleryProvider.BASE_URI.buildUpon()
+ .appendEncodedPath(mPath.toString().substring(1))
+ .build();
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ DateFormat formater = DateFormat.getDateTimeInstance();
+ details.addDetail(MediaDetails.INDEX_TITLE, mFileName);
+ details.addDetail(MediaDetails.INDEX_DATETIME, formater.format(new Date(mDateTaken)));
+ details.addDetail(MediaDetails.INDEX_WIDTH, mImageWidth);
+ details.addDetail(MediaDetails.INDEX_HEIGHT, mImageHeight);
+ details.addDetail(MediaDetails.INDEX_SIZE, Long.valueOf(mObjectSize));
+ return details;
+ }
+
+}
diff --git a/src/com/android/gallery3d/data/MtpSource.java b/src/com/android/gallery3d/data/MtpSource.java
new file mode 100644
index 000000000..683a40291
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpSource.java
@@ -0,0 +1,71 @@
+/*
+ * 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 MtpSource extends MediaSource {
+ private static final String TAG = "MtpSource";
+
+ private static final int MTP_DEVICESET = 0;
+ private static final int MTP_DEVICE = 1;
+ private static final int MTP_ITEM = 2;
+
+ GalleryApp mApplication;
+ PathMatcher mMatcher;
+ MtpContext mMtpContext;
+
+ public MtpSource(GalleryApp application) {
+ super("mtp");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/mtp", MTP_DEVICESET);
+ mMatcher.add("/mtp/*", MTP_DEVICE);
+ mMatcher.add("/mtp/item/*/*", MTP_ITEM);
+ mMtpContext = new MtpContext(mApplication.getAndroidContext());
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ switch (mMatcher.match(path)) {
+ case MTP_DEVICESET: {
+ return new MtpDeviceSet(path, mApplication, mMtpContext);
+ }
+ case MTP_DEVICE: {
+ int deviceId = mMatcher.getIntVar(0);
+ return new MtpDevice(path, mApplication, deviceId, mMtpContext);
+ }
+ case MTP_ITEM: {
+ int deviceId = mMatcher.getIntVar(0);
+ int objectId = mMatcher.getIntVar(1);
+ return new MtpImage(path, mApplication, deviceId, objectId, mMtpContext);
+ }
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+
+ @Override
+ public void pause() {
+ mMtpContext.pause();
+ }
+
+ @Override
+ public void resume() {
+ mMtpContext.resume();
+ }
+}
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
new file mode 100644
index 000000000..3de1c7c76
--- /dev/null
+++ b/src/com/android/gallery3d/data/Path.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IdentityCache;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class Path {
+ private static final String TAG = "Path";
+ private static Path sRoot = new Path(null, "ROOT");
+
+ private final Path mParent;
+ private final String mSegment;
+ private WeakReference<MediaObject> mObject;
+ private IdentityCache<String, Path> mChildren;
+
+ private Path(Path parent, String segment) {
+ mParent = parent;
+ mSegment = segment;
+ }
+
+ public Path getChild(String segment) {
+ synchronized (Path.class) {
+ if (mChildren == null) {
+ mChildren = new IdentityCache<String, Path>();
+ } else {
+ Path p = mChildren.get(segment);
+ if (p != null) return p;
+ }
+
+ Path p = new Path(this, segment);
+ mChildren.put(segment, p);
+ return p;
+ }
+ }
+
+ public Path getParent() {
+ synchronized (Path.class) {
+ return mParent;
+ }
+ }
+
+ public Path getChild(int segment) {
+ return getChild(String.valueOf(segment));
+ }
+
+ public Path getChild(long segment) {
+ return getChild(String.valueOf(segment));
+ }
+
+ public void setObject(MediaObject object) {
+ synchronized (Path.class) {
+ Utils.assertTrue(mObject == null || mObject.get() == null);
+ mObject = new WeakReference<MediaObject>(object);
+ }
+ }
+
+ public MediaObject getObject() {
+ synchronized (Path.class) {
+ return (mObject == null) ? null : mObject.get();
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized (Path.class) {
+ StringBuilder sb = new StringBuilder();
+ String[] segments = split();
+ for (int i = 0; i < segments.length; i++) {
+ sb.append("/");
+ sb.append(segments[i]);
+ }
+ return sb.toString();
+ }
+ }
+
+ public static Path fromString(String s) {
+ synchronized (Path.class) {
+ String[] segments = split(s);
+ Path current = sRoot;
+ for (int i = 0; i < segments.length; i++) {
+ current = current.getChild(segments[i]);
+ }
+ return current;
+ }
+ }
+
+ public String[] split() {
+ synchronized (Path.class) {
+ int n = 0;
+ for (Path p = this; p != sRoot; p = p.mParent) {
+ n++;
+ }
+ String[] segments = new String[n];
+ int i = n - 1;
+ for (Path p = this; p != sRoot; p = p.mParent) {
+ segments[i--] = p.mSegment;
+ }
+ return segments;
+ }
+ }
+
+ public static String[] split(String s) {
+ int n = s.length();
+ if (n == 0) return new String[0];
+ if (s.charAt(0) != '/') {
+ throw new RuntimeException("malformed path:" + s);
+ }
+ ArrayList<String> segments = new ArrayList<String>();
+ int i = 1;
+ while (i < n) {
+ int brace = 0;
+ int j;
+ for (j = i; j < n; j++) {
+ char c = s.charAt(j);
+ if (c == '{') ++brace;
+ else if (c == '}') --brace;
+ else if (brace == 0 && c == '/') break;
+ }
+ if (brace != 0) {
+ throw new RuntimeException("unbalanced brace in path:" + s);
+ }
+ segments.add(s.substring(i, j));
+ i = j + 1;
+ }
+ String[] result = new String[segments.size()];
+ segments.toArray(result);
+ return result;
+ }
+
+ // Splits a string to an array of strings.
+ // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}.
+ public static String[] splitSequence(String s) {
+ int n = s.length();
+ if (s.charAt(0) != '{' || s.charAt(n-1) != '}') {
+ throw new RuntimeException("bad sequence: " + s);
+ }
+ ArrayList<String> segments = new ArrayList<String>();
+ int i = 1;
+ while (i < n - 1) {
+ int brace = 0;
+ int j;
+ for (j = i; j < n - 1; j++) {
+ char c = s.charAt(j);
+ if (c == '{') ++brace;
+ else if (c == '}') --brace;
+ else if (brace == 0 && c == ',') break;
+ }
+ if (brace != 0) {
+ throw new RuntimeException("unbalanced brace in path:" + s);
+ }
+ segments.add(s.substring(i, j));
+ i = j + 1;
+ }
+ String[] result = new String[segments.size()];
+ segments.toArray(result);
+ return result;
+ }
+
+ public String getPrefix() {
+ synchronized (Path.class) {
+ Path current = this;
+ if (current == sRoot) return "";
+ while (current.mParent != sRoot) {
+ current = current.mParent;
+ }
+ return current.mSegment;
+ }
+ }
+
+ public String getSuffix() {
+ // We don't need lock because mSegment is final.
+ return mSegment;
+ }
+
+ public String getSuffix(int level) {
+ // We don't need lock because mSegment and mParent are final.
+ Path p = this;
+ while (level-- != 0) {
+ p = p.mParent;
+ }
+ return p.mSegment;
+ }
+
+ // Below are for testing/debugging only
+ static void clearAll() {
+ synchronized (Path.class) {
+ sRoot = new Path(null, "");
+ }
+ }
+
+ static void dumpAll() {
+ dumpAll(sRoot, "", "");
+ }
+
+ static void dumpAll(Path p, String prefix1, String prefix2) {
+ synchronized (Path.class) {
+ MediaObject obj = p.getObject();
+ Log.d(TAG, prefix1 + p.mSegment + ":"
+ + (obj == null ? "null" : obj.getClass().getSimpleName()));
+ if (p.mChildren != null) {
+ ArrayList<String> childrenKeys = p.mChildren.keys();
+ int i = 0, n = childrenKeys.size();
+ for (String key : childrenKeys) {
+ Path child = p.mChildren.get(key);
+ if (child == null) {
+ ++i;
+ continue;
+ }
+ Log.d(TAG, prefix2 + "|");
+ if (++i < n) {
+ dumpAll(child, prefix2 + "+-- ", prefix2 + "| ");
+ } else {
+ dumpAll(child, prefix2 + "+-- ", prefix2 + " ");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java
new file mode 100644
index 000000000..9c6b840d5
--- /dev/null
+++ b/src/com/android/gallery3d/data/PathMatcher.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class PathMatcher {
+ public static final int NOT_FOUND = -1;
+
+ private ArrayList<String> mVariables = new ArrayList<String>();
+ private Node mRoot = new Node();
+
+ public PathMatcher() {
+ mRoot = new Node();
+ }
+
+ public void add(String pattern, int kind) {
+ String[] segments = Path.split(pattern);
+ Node current = mRoot;
+ for (int i = 0; i < segments.length; i++) {
+ current = current.addChild(segments[i]);
+ }
+ current.setKind(kind);
+ }
+
+ public int match(Path path) {
+ String[] segments = path.split();
+ mVariables.clear();
+ Node current = mRoot;
+ for (int i = 0; i < segments.length; i++) {
+ Node next = current.getChild(segments[i]);
+ if (next == null) {
+ next = current.getChild("*");
+ if (next != null) {
+ mVariables.add(segments[i]);
+ } else {
+ return NOT_FOUND;
+ }
+ }
+ current = next;
+ }
+ return current.getKind();
+ }
+
+ public String getVar(int index) {
+ return mVariables.get(index);
+ }
+
+ public int getIntVar(int index) {
+ return Integer.parseInt(mVariables.get(index));
+ }
+
+ public long getLongVar(int index) {
+ return Long.parseLong(mVariables.get(index));
+ }
+
+ private static class Node {
+ private HashMap<String, Node> mMap;
+ private int mKind = NOT_FOUND;
+
+ Node addChild(String segment) {
+ if (mMap == null) {
+ mMap = new HashMap<String, Node>();
+ } else {
+ Node node = mMap.get(segment);
+ if (node != null) return node;
+ }
+
+ Node n = new Node();
+ mMap.put(segment, n);
+ return n;
+ }
+
+ Node getChild(String segment) {
+ if (mMap == null) return null;
+ return mMap.get(segment);
+ }
+
+ void setKind(int kind) {
+ mKind = kind;
+ }
+
+ int getKind() {
+ return mKind;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java
new file mode 100644
index 000000000..7e24b337b
--- /dev/null
+++ b/src/com/android/gallery3d/data/SizeClustering.java
@@ -0,0 +1,138 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+
+import java.util.ArrayList;
+
+public class SizeClustering extends Clustering {
+ private static final String TAG = "SizeClustering";
+
+ private Context mContext;
+ private ArrayList<Path>[] mClusters;
+ private String[] mNames;
+ private long mMinSizes[];
+
+ private static final long MEGA_BYTES = 1024L*1024;
+ private static final long GIGA_BYTES = 1024L*1024*1024;
+
+ private static final long[] SIZE_LEVELS = {
+ 0,
+ 1 * MEGA_BYTES,
+ 10 * MEGA_BYTES,
+ 100 * MEGA_BYTES,
+ 1 * GIGA_BYTES,
+ 2 * GIGA_BYTES,
+ 4 * GIGA_BYTES,
+ };
+
+ public SizeClustering(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final ArrayList<Path>[] group =
+ (ArrayList<Path>[]) new ArrayList[SIZE_LEVELS.length];
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ public void consume(int index, MediaItem item) {
+ // Find the cluster this item belongs to.
+ long size = item.getSize();
+ int i;
+ for (i = 0; i < SIZE_LEVELS.length - 1; i++) {
+ if (size < SIZE_LEVELS[i + 1]) {
+ break;
+ }
+ }
+
+ ArrayList<Path> list = group[i];
+ if (list == null) {
+ list = new ArrayList<Path>();
+ group[i] = list;
+ }
+ list.add(item.getPath());
+ }
+ });
+
+ int count = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (group[i] != null) {
+ count++;
+ }
+ }
+
+ mClusters = (ArrayList<Path>[]) new ArrayList[count];
+ mNames = new String[count];
+ mMinSizes = new long[count];
+
+ Resources res = mContext.getResources();
+ int k = 0;
+ // Go through group in the reverse order, so the group with the largest
+ // size will show first.
+ for (int i = group.length - 1; i >= 0; i--) {
+ if (group[i] == null) continue;
+
+ mClusters[k] = group[i];
+ if (i == 0) {
+ mNames[k] = String.format(
+ res.getString(R.string.size_below), getSizeString(i + 1));
+ } else if (i == group.length - 1) {
+ mNames[k] = String.format(
+ res.getString(R.string.size_above), getSizeString(i));
+ } else {
+ String minSize = getSizeString(i);
+ String maxSize = getSizeString(i + 1);
+ mNames[k] = String.format(
+ res.getString(R.string.size_between), minSize, maxSize);
+ }
+ mMinSizes[k] = SIZE_LEVELS[i];
+ k++;
+ }
+ }
+
+ private String getSizeString(int index) {
+ long bytes = SIZE_LEVELS[index];
+ if (bytes >= GIGA_BYTES) {
+ return (bytes / GIGA_BYTES) + "GB";
+ } else {
+ return (bytes / MEGA_BYTES) + "MB";
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.length;
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters[index];
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+
+ public long getMinSize(int index) {
+ return mMinSizes[index];
+ }
+}
diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java
new file mode 100644
index 000000000..c87305132
--- /dev/null
+++ b/src/com/android/gallery3d/data/TagClustering.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class TagClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "TagClustering";
+
+ private ArrayList<ArrayList<Path>> mClusters;
+ private String[] mNames;
+ private String mUntaggedString;
+
+ public TagClustering(Context context) {
+ mUntaggedString = context.getResources().getString(R.string.untagged);
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final TreeMap<String, ArrayList<Path>> map =
+ new TreeMap<String, ArrayList<Path>>();
+ final ArrayList<Path> untagged = new ArrayList<Path>();
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ public void consume(int index, MediaItem item) {
+ Path path = item.getPath();
+
+ String[] tags = item.getTags();
+ if (tags == null || tags.length == 0) {
+ untagged.add(path);
+ return;
+ }
+ for (int j = 0; j < tags.length; j++) {
+ String key = tags[j];
+ ArrayList<Path> list = map.get(key);
+ if (list == null) {
+ list = new ArrayList<Path>();
+ map.put(key, list);
+ }
+ list.add(path);
+ }
+ }
+ });
+
+ int m = map.size();
+ mClusters = new ArrayList<ArrayList<Path>>();
+ mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)];
+ int i = 0;
+ for (Map.Entry<String, ArrayList<Path>> entry : map.entrySet()) {
+ mNames[i++] = entry.getKey();
+ mClusters.add(entry.getValue());
+ }
+ if (untagged.size() > 0) {
+ mNames[i++] = mUntaggedString;
+ mClusters.add(untagged);
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.size();
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters.get(index);
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+}
diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java
new file mode 100644
index 000000000..1ccf14c13
--- /dev/null
+++ b/src/com/android/gallery3d/data/TimeClustering.java
@@ -0,0 +1,436 @@
+/*
+ * 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.GalleryUtils;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class TimeClustering extends Clustering {
+ private static final String TAG = "TimeClustering";
+
+ // If 2 items are greater than 25 miles apart, they will be in different
+ // clusters.
+ private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20;
+
+ // Do not want to split based on anything under 1 min.
+ private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
+
+ // Disregard a cluster split time of anything over 2 hours.
+ private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
+
+ // Try and get around 9 clusters (best-effort for the common case).
+ private static final int NUM_CLUSTERS_TARGETED = 9;
+
+ // Try and merge 2 clusters if they are both smaller than min cluster size.
+ // The min cluster size can range from 8 to 15.
+ private static final int MIN_MIN_CLUSTER_SIZE = 8;
+ private static final int MAX_MIN_CLUSTER_SIZE = 15;
+
+ // Try and split a cluster if it is bigger than max cluster size.
+ // The max cluster size can range from 20 to 50.
+ private static final int MIN_MAX_CLUSTER_SIZE = 20;
+ private static final int MAX_MAX_CLUSTER_SIZE = 50;
+
+ // Initially put 2 items in the same cluster as long as they are within
+ // 3 cluster frequencies of each other.
+ private static int CLUSTER_SPLIT_MULTIPLIER = 3;
+
+ // The minimum change factor in the time between items to consider a
+ // partition.
+ // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
+ private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
+
+ // Make the cluster split time of a large cluster half that of a regular
+ // cluster.
+ private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
+
+ private Context mContext;
+ private ArrayList<Cluster> mClusters;
+ private String[] mNames;
+ private Cluster mCurrCluster;
+
+ private long mClusterSplitTime =
+ (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
+ private long mLargeClusterSplitTime =
+ mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+ private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
+ private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
+
+
+ private static final Comparator<SmallItem> sDateComparator =
+ new DateComparator();
+
+ private static class DateComparator implements Comparator<SmallItem> {
+ public int compare(SmallItem item1, SmallItem item2) {
+ return -Utils.compare(item1.dateInMs, item2.dateInMs);
+ }
+ }
+
+ public TimeClustering(Context context) {
+ mContext = context;
+ mClusters = new ArrayList<Cluster>();
+ mCurrCluster = new Cluster();
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final int total = baseSet.getTotalMediaItemCount();
+ final SmallItem[] buf = new SmallItem[total];
+ final double[] latLng = new double[2];
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ public void consume(int index, MediaItem item) {
+ if (index < 0 || index >= total) return;
+ SmallItem s = new SmallItem();
+ s.path = item.getPath();
+ s.dateInMs = item.getDateInMs();
+ item.getLatLong(latLng);
+ s.lat = latLng[0];
+ s.lng = latLng[1];
+ buf[index] = s;
+ }
+ });
+
+ ArrayList<SmallItem> items = new ArrayList<SmallItem>(total);
+ for (int i = 0; i < total; i++) {
+ if (buf[i] != null) {
+ items.add(buf[i]);
+ }
+ }
+
+ Collections.sort(items, sDateComparator);
+
+ int n = items.size();
+ long minTime = 0;
+ long maxTime = 0;
+ for (int i = 0; i < n; i++) {
+ long t = items.get(i).dateInMs;
+ if (t == 0) continue;
+ if (minTime == 0) {
+ minTime = maxTime = t;
+ } else {
+ minTime = Math.min(minTime, t);
+ maxTime = Math.max(maxTime, t);
+ }
+ }
+
+ setTimeRange(maxTime - minTime, n);
+
+ for (int i = 0; i < n; i++) {
+ compute(items.get(i));
+ }
+
+ compute(null);
+
+ int m = mClusters.size();
+ mNames = new String[m];
+ for (int i = 0; i < m; i++) {
+ mNames[i] = mClusters.get(i).generateCaption(mContext);
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.size();
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ ArrayList<SmallItem> items = mClusters.get(index).getItems();
+ ArrayList<Path> result = new ArrayList<Path>(items.size());
+ for (int i = 0, n = items.size(); i < n; i++) {
+ result.add(items.get(i).path);
+ }
+ return result;
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mNames[index];
+ }
+
+ private void setTimeRange(long timeRange, int numItems) {
+ if (numItems != 0) {
+ int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
+ // Heuristic to get min and max cluster size - half and double the
+ // desired items per cluster.
+ mMinClusterSize = meanItemsPerCluster / 2;
+ mMaxClusterSize = meanItemsPerCluster * 2;
+ mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
+ }
+ mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
+ mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+ mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
+ mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
+ }
+
+ private void compute(SmallItem currentItem) {
+ if (currentItem != null) {
+ int numClusters = mClusters.size();
+ int numCurrClusterItems = mCurrCluster.size();
+ boolean geographicallySeparateItem = false;
+ boolean itemAddedToCurrentCluster = false;
+
+ // Determine if this item should go in the current cluster or be the
+ // start of a new cluster.
+ if (numCurrClusterItems == 0) {
+ mCurrCluster.addItem(currentItem);
+ } else {
+ SmallItem prevItem = mCurrCluster.getLastItem();
+ if (isGeographicallySeparated(prevItem, currentItem)) {
+ mClusters.add(mCurrCluster);
+ geographicallySeparateItem = true;
+ } else if (numCurrClusterItems > mMaxClusterSize) {
+ splitAndAddCurrentCluster();
+ } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
+ mCurrCluster.addItem(currentItem);
+ itemAddedToCurrentCluster = true;
+ } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+ && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+ mergeAndAddCurrentCluster();
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+
+ // Creating a new cluster and adding the current item to it.
+ if (!itemAddedToCurrentCluster) {
+ mCurrCluster = new Cluster();
+ if (geographicallySeparateItem) {
+ mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
+ }
+ mCurrCluster.addItem(currentItem);
+ }
+ }
+ } else {
+ if (mCurrCluster.size() > 0) {
+ int numClusters = mClusters.size();
+ int numCurrClusterItems = mCurrCluster.size();
+
+ // The last cluster may potentially be too big or too small.
+ if (numCurrClusterItems > mMaxClusterSize) {
+ splitAndAddCurrentCluster();
+ } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+ && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+ mergeAndAddCurrentCluster();
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ mCurrCluster = new Cluster();
+ }
+ }
+ }
+
+ private void splitAndAddCurrentCluster() {
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
+ if (secondPartitionStartIndex != -1) {
+ Cluster partitionedCluster = new Cluster();
+ for (int j = 0; j < secondPartitionStartIndex; j++) {
+ partitionedCluster.addItem(currClusterItems.get(j));
+ }
+ mClusters.add(partitionedCluster);
+ partitionedCluster = new Cluster();
+ for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
+ partitionedCluster.addItem(currClusterItems.get(j));
+ }
+ mClusters.add(partitionedCluster);
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ }
+
+ private int getPartitionIndexForCurrentCluster() {
+ int partitionIndex = -1;
+ float largestChange = MIN_PARTITION_CHANGE_FACTOR;
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ int minClusterSize = mMinClusterSize;
+
+ // Could be slightly more efficient here but this code seems cleaner.
+ if (numCurrClusterItems > minClusterSize + 1) {
+ for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
+ SmallItem prevItem = currClusterItems.get(i - 1);
+ SmallItem currItem = currClusterItems.get(i);
+ SmallItem nextItem = currClusterItems.get(i + 1);
+
+ long timeNext = nextItem.dateInMs;
+ long timeCurr = currItem.dateInMs;
+ long timePrev = prevItem.dateInMs;
+
+ if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue;
+
+ long diff1 = Math.abs(timeNext - timeCurr);
+ long diff2 = Math.abs(timeCurr - timePrev);
+
+ float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
+ if (change > largestChange) {
+ if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
+ partitionIndex = i;
+ largestChange = change;
+ } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
+ partitionIndex = i + 1;
+ largestChange = change;
+ }
+ }
+ }
+ }
+ return partitionIndex;
+ }
+
+ private void mergeAndAddCurrentCluster() {
+ int numClusters = mClusters.size();
+ Cluster prevCluster = mClusters.get(numClusters - 1);
+ ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+ int numCurrClusterItems = mCurrCluster.size();
+ if (prevCluster.size() < mMinClusterSize) {
+ for (int i = 0; i < numCurrClusterItems; i++) {
+ prevCluster.addItem(currClusterItems.get(i));
+ }
+ mClusters.set(numClusters - 1, prevCluster);
+ } else {
+ mClusters.add(mCurrCluster);
+ }
+ }
+
+ // Returns true if a, b are sufficiently geographically separated.
+ private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) {
+ if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng)
+ || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) {
+ return false;
+ }
+
+ double distance = GalleryUtils.fastDistanceMeters(
+ Math.toRadians(itemA.lat),
+ Math.toRadians(itemA.lng),
+ Math.toRadians(itemB.lat),
+ Math.toRadians(itemB.lng));
+ return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES);
+ }
+
+ // Returns the time interval between the two items in milliseconds.
+ private static long timeDistance(SmallItem a, SmallItem b) {
+ return Math.abs(a.dateInMs - b.dateInMs);
+ }
+}
+
+class SmallItem {
+ Path path;
+ long dateInMs;
+ double lat, lng;
+}
+
+class Cluster {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Cluster";
+ private static final String MMDDYY_FORMAT = "MMddyy";
+
+ // This is for TimeClustering only.
+ public boolean mGeographicallySeparatedFromPrevCluster = false;
+
+ private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>();
+
+ public Cluster() {
+ }
+
+ public void addItem(SmallItem item) {
+ mItems.add(item);
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+
+ public SmallItem getLastItem() {
+ int n = mItems.size();
+ return (n == 0) ? null : mItems.get(n - 1);
+ }
+
+ public ArrayList<SmallItem> getItems() {
+ return mItems;
+ }
+
+ public String generateCaption(Context context) {
+ int n = mItems.size();
+ long minTimestamp = 0;
+ long maxTimestamp = 0;
+
+ for (int i = 0; i < n; i++) {
+ long t = mItems.get(i).dateInMs;
+ if (t == 0) continue;
+ if (minTimestamp == 0) {
+ minTimestamp = maxTimestamp = t;
+ } else {
+ minTimestamp = Math.min(minTimestamp, t);
+ maxTimestamp = Math.max(maxTimestamp, t);
+ }
+ }
+ if (minTimestamp == 0) return "";
+
+ String caption;
+ String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp)
+ .toString();
+ String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp)
+ .toString();
+
+ if (minDay.substring(4).equals(maxDay.substring(4))) {
+ // The items are from the same year - show at least as
+ // much granularity as abbrev_all allows.
+ caption = DateUtils.formatDateRange(context, minTimestamp,
+ maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
+
+ // Get a more granular date range string if the min and
+ // max timestamp are on the same day and from the
+ // current year.
+ if (minDay.equals(maxDay)) {
+ int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+ // Contains the year only if the date does not
+ // correspond to the current year.
+ String dateRangeWithOptionalYear = DateUtils.formatDateTime(
+ context, minTimestamp, flags);
+ String dateRangeWithYear = DateUtils.formatDateTime(
+ context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
+ if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
+ // This means both dates are from the same year
+ // - show the time.
+ // Not enough room to display the time range.
+ // Pick the mid-point.
+ long midTimestamp = (minTimestamp + maxTimestamp) / 2;
+ caption = DateUtils.formatDateRange(context, midTimestamp,
+ midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
+ }
+ }
+ } else {
+ // The items are not from the same year - only show
+ // month and year.
+ int flags = DateUtils.FORMAT_NO_MONTH_DAY
+ | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+ caption = DateUtils.formatDateRange(context, minTimestamp,
+ maxTimestamp, flags);
+ }
+
+ return caption;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
new file mode 100644
index 000000000..3a7ed7c3f
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -0,0 +1,266 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.webkit.MimeTypeMap;
+
+import java.io.FileNotFoundException;
+import java.net.URI;
+import java.net.URL;
+
+public class UriImage extends MediaItem {
+ private static final String TAG = "UriImage";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_DOWNLOADING = 1;
+ private static final int STATE_DOWNLOADED = 2;
+ private static final int STATE_ERROR = -1;
+
+ private final Uri mUri;
+ private final String mContentType;
+
+ private DownloadCache.Entry mCacheEntry;
+ private ParcelFileDescriptor mFileDescriptor;
+ private int mState = STATE_INIT;
+ private int mWidth;
+ private int mHeight;
+
+ private GalleryApp mApplication;
+
+ public UriImage(GalleryApp application, Path path, Uri uri) {
+ super(path, nextVersionNumber());
+ mUri = uri;
+ mApplication = Utils.checkNotNull(application);
+ mContentType = getMimeType(uri);
+ }
+
+ private String getMimeType(Uri uri) {
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ String extension =
+ MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String type = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(extension);
+ if (type != null) return type;
+ }
+ return mApplication.getContentResolver().getType(uri);
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new BitmapJob(type);
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new RegionDecoderJob();
+ }
+
+ private void openFileOrDownloadTempFile(JobContext jc) {
+ int state = openOrDownloadInner(jc);
+ synchronized (this) {
+ mState = state;
+ if (mState != STATE_DOWNLOADED) {
+ if (mFileDescriptor != null) {
+ Utils.closeSilently(mFileDescriptor);
+ mFileDescriptor = null;
+ }
+ }
+ notifyAll();
+ }
+ }
+
+ private int openOrDownloadInner(JobContext jc) {
+ String scheme = mUri.getScheme();
+ if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ try {
+ mFileDescriptor = mApplication.getContentResolver()
+ .openFileDescriptor(mUri, "r");
+ if (jc.isCancelled()) return STATE_INIT;
+ return STATE_DOWNLOADED;
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "fail to open: " + mUri, e);
+ return STATE_ERROR;
+ }
+ } else {
+ try {
+ URL url = new URI(mUri.toString()).toURL();
+ mCacheEntry = mApplication.getDownloadCache().download(jc, url);
+ if (jc.isCancelled()) return STATE_INIT;
+ if (mCacheEntry == null) {
+ Log.w(TAG, "download failed " + url);
+ return STATE_ERROR;
+ }
+ mFileDescriptor = ParcelFileDescriptor.open(
+ mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ return STATE_DOWNLOADED;
+ } catch (Throwable t) {
+ Log.w(TAG, "download error", t);
+ return STATE_ERROR;
+ }
+ }
+ }
+
+ private boolean prepareInputFile(JobContext jc) {
+ jc.setCancelListener(new CancelListener() {
+ public void onCancel() {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ });
+
+ while (true) {
+ synchronized (this) {
+ if (jc.isCancelled()) return false;
+ if (mState == STATE_INIT) {
+ mState = STATE_DOWNLOADING;
+ // Then leave the synchronized block and continue.
+ } else if (mState == STATE_ERROR) {
+ return false;
+ } else if (mState == STATE_DOWNLOADED) {
+ return true;
+ } else /* if (mState == STATE_DOWNLOADING) */ {
+ try {
+ wait();
+ } catch (InterruptedException ex) {
+ // ignored.
+ }
+ continue;
+ }
+ }
+ // This is only reached for STATE_INIT->STATE_DOWNLOADING
+ openFileOrDownloadTempFile(jc);
+ }
+ }
+
+ private class RegionDecoderJob implements Job<BitmapRegionDecoder> {
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ BitmapRegionDecoder decoder = DecodeUtils.requestCreateBitmapRegionDecoder(
+ jc, mFileDescriptor.getFileDescriptor(), false);
+ mWidth = decoder.getWidth();
+ mHeight = decoder.getHeight();
+ return decoder;
+ }
+ }
+
+ private class BitmapJob implements Job<Bitmap> {
+ private int mType;
+
+ protected BitmapJob(int type) {
+ mType = type;
+ }
+
+ public Bitmap run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ int targetSize = LocalImage.getTargetSize(mType);
+ Options options = new Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ Bitmap bitmap = DecodeUtils.requestDecode(jc,
+ mFileDescriptor.getFileDescriptor(), options, targetSize);
+ if (jc.isCancelled() || bitmap == null) {
+ return null;
+ }
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap,
+ targetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap,
+ targetSize, true);
+ }
+
+ return bitmap;
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ int supported = SUPPORT_EDIT | SUPPORT_SETAS;
+ if (isSharable()) supported |= SUPPORT_SHARE;
+ if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
+ supported |= SUPPORT_FULL_IMAGE;
+ }
+ return supported;
+ }
+
+ private boolean isSharable() {
+ // We cannot grant read permission to the receiver since we put
+ // the data URI in EXTRA_STREAM instead of the data part of an intent
+ // And there are issues in MediaUploader and Bluetooth file sender to
+ // share a general image data. So, we only share for local file.
+ return ContentResolver.SCHEME_FILE.equals(mUri.getScheme());
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_IMAGE;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ return mUri;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ if (mWidth != 0 && mHeight != 0) {
+ details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
+ details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
+ }
+ details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
+ if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
+ String filePath = mUri.getPath();
+ details.addDetail(MediaDetails.INDEX_PATH, filePath);
+ MediaDetails.extractExifInfo(details, filePath);
+ }
+ return details;
+ }
+
+ @Override
+ public String getMimeType() {
+ return mContentType;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mFileDescriptor != null) {
+ Utils.closeSilently(mFileDescriptor);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java
new file mode 100644
index 000000000..ac62b93a7
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriSource.java
@@ -0,0 +1,58 @@
+/*
+ * 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.net.URLDecoder;
+import java.net.URLEncoder;
+
+class UriSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UriSource";
+
+ private GalleryApp mApplication;
+
+ public UriSource(GalleryApp context) {
+ super("uri");
+ mApplication = context;
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ String segment[] = path.split();
+ if (segment.length != 2) {
+ throw new RuntimeException("bad path: " + path);
+ }
+
+ String decoded = URLDecoder.decode(segment[1]);
+ return new UriImage(mApplication, path, Uri.parse(decoded));
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri) {
+ String type = mApplication.getContentResolver().getType(uri);
+ // Assume the type is image if the type cannot be resolved
+ // This could happen for "http" URI.
+ if (type == null || type.startsWith("image/")) {
+ return Path.fromString("/uri/" + URLEncoder.encode(uri.toString()));
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java
new file mode 100644
index 000000000..f5f0f1b3c
--- /dev/null
+++ b/src/com/android/gallery3d/provider/GalleryProvider.java
@@ -0,0 +1,224 @@
+/*
+ * 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.provider;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MtpImage;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class GalleryProvider extends ContentProvider {
+ private static final String TAG = "GalleryProvider";
+
+ public static final String AUTHORITY = "com.android.gallery3d.provider";
+ public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+
+ private DataManager mDataManager;
+ private DownloadCache mDownloadCache;
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ // TODO: consider concurrent access
+ @Override
+ public String getType