summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-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(Uri uri) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ Path path = Path.fromString(uri.getPath());
+ MediaItem item = (MediaItem) mDataManager.getMediaObject(path);
+ return item != null ? item.getMimeType() : null;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean onCreate() {
+ GalleryApp app = (GalleryApp) getContext().getApplicationContext();
+ mDataManager = app.getDataManager();
+ return true;
+ }
+
+ private DownloadCache getDownloadCache() {
+ if (mDownloadCache == null) {
+ GalleryApp app = (GalleryApp) getContext().getApplicationContext();
+ mDownloadCache = app.getDownloadCache();
+ }
+ return mDownloadCache;
+ }
+
+ // TODO: consider concurrent access
+ @Override
+ public Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ Path path = Path.fromString(uri.getPath());
+ MediaObject object = mDataManager.getMediaObject(path);
+ if (object == null) {
+ Log.w(TAG, "cannot find: " + uri);
+ return null;
+ }
+ if (PicasaSource.isPicasaImage(object)) {
+ return queryPicasaItem(object,
+ projection, selection, selectionArgs, sortOrder);
+ } else if (object instanceof MtpImage) {
+ return queryMtpItem((MtpImage) object,
+ projection, selection, selectionArgs, sortOrder);
+ } else {
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private Cursor queryMtpItem(MtpImage image, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ Object[] columnValues = new Object[projection.length];
+ for (int i = 0, n = projection.length; i < n; ++i) {
+ String column = projection[i];
+ if (ImageColumns.DISPLAY_NAME.equals(column)) {
+ columnValues[i] = image.getName();
+ } else if (ImageColumns.SIZE.equals(column)){
+ columnValues[i] = image.getSize();
+ } else if (ImageColumns.MIME_TYPE.equals(column)) {
+ columnValues[i] = image.getMimeType();
+ } else if (ImageColumns.DATE_TAKEN.equals(column)) {
+ columnValues[i] = image.getDateInMs();
+ } else {
+ Log.w(TAG, "unsupported column: " + column);
+ }
+ }
+ MatrixCursor cursor = new MatrixCursor(projection);
+ cursor.addRow(columnValues);
+ return cursor;
+ }
+
+ private Cursor queryPicasaItem(MediaObject image, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ Object[] columnValues = new Object[projection.length];
+ double latitude = PicasaSource.getLatitude(image);
+ double longitude = PicasaSource.getLongitude(image);
+ boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude);
+
+ for (int i = 0, n = projection.length; i < n; ++i) {
+ String column = projection[i];
+ if (ImageColumns.DISPLAY_NAME.equals(column)) {
+ columnValues[i] = PicasaSource.getImageTitle(image);
+ } else if (ImageColumns.SIZE.equals(column)){
+ columnValues[i] = PicasaSource.getImageSize(image);
+ } else if (ImageColumns.MIME_TYPE.equals(column)) {
+ columnValues[i] = PicasaSource.getContentType(image);
+ } else if (ImageColumns.DATE_TAKEN.equals(column)) {
+ columnValues[i] = PicasaSource.getDateTaken(image);
+ } else if (ImageColumns.LATITUDE.equals(column)) {
+ columnValues[i] = isValidLatlong ? latitude : null;
+ } else if (ImageColumns.LONGITUDE.equals(column)) {
+ columnValues[i] = isValidLatlong ? longitude : null;
+ } else if (ImageColumns.ORIENTATION.equals(column)) {
+ columnValues[i] = PicasaSource.getRotation(image);
+ } else {
+ Log.w(TAG, "unsupported column: " + column);
+ }
+ }
+ MatrixCursor cursor = new MatrixCursor(projection);
+ cursor.addRow(columnValues);
+ return cursor;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ long token = Binder.clearCallingIdentity();
+ try {
+ if (mode.contains("w")) {
+ throw new FileNotFoundException("cannot open file for write");
+ }
+ Path path = Path.fromString(uri.getPath());
+ MediaObject object = mDataManager.getMediaObject(path);
+ if (object == null) {
+ throw new FileNotFoundException(uri.toString());
+ }
+ if (PicasaSource.isPicasaImage(object)) {
+ return PicasaSource.openFile(getContext(), object, mode);
+ } else if (object instanceof MtpImage) {
+ return openPipeHelper(uri, null, null, null,
+ new MtpPipeDataWriter((MtpImage) object));
+ } else {
+ throw new FileNotFoundException("unspported type: " + object);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ private final class MtpPipeDataWriter implements PipeDataWriter<Object> {
+ private final MtpImage mImage;
+
+ private MtpPipeDataWriter(MtpImage image) {
+ mImage = image;
+ }
+
+ @Override
+ public void writeDataToPipe(ParcelFileDescriptor output,
+ Uri uri, String mimeType, Bundle opts, Object args) {
+ OutputStream os = null;
+ try {
+ os = new ParcelFileDescriptor.AutoCloseOutputStream(output);
+ os.write(mImage.getImageData());
+ } catch (IOException e) {
+ Log.w(TAG, "fail to download: " + uri, e);
+ } finally {
+ Utils.closeSilently(os);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AbstractDisplayItem.java b/src/com/android/gallery3d/ui/AbstractDisplayItem.java
new file mode 100644
index 000000000..aad3919b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractDisplayItem.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.data.MediaItem;
+
+import android.graphics.Bitmap;
+
+public abstract class AbstractDisplayItem extends DisplayItem {
+
+ private static final String TAG = "AbstractDisplayItem";
+
+ private static final int STATE_INVALID = 0x01;
+ private static final int STATE_VALID = 0x02;
+ private static final int STATE_UPDATING = 0x04;
+ private static final int STATE_CANCELING = 0x08;
+ private static final int STATE_ERROR = 0x10;
+
+ private int mState = STATE_INVALID;
+ private boolean mImageRequested = false;
+ private boolean mRecycling = false;
+ private Bitmap mBitmap;
+
+ protected final MediaItem mMediaItem;
+ private int mRotation;
+
+ public AbstractDisplayItem(MediaItem item) {
+ mMediaItem = item;
+ if (item == null) mState = STATE_ERROR;
+ if (item != null) mRotation = mMediaItem.getRotation();
+ }
+
+ protected void updateImage(Bitmap bitmap, boolean isCancelled) {
+ if (mRecycling) {
+ return;
+ }
+
+ if (isCancelled && bitmap == null) {
+ mState = STATE_INVALID;
+ if (mImageRequested) {
+ // request image again.
+ requestImage();
+ }
+ return;
+ }
+
+ mBitmap = bitmap;
+ mState = bitmap == null ? STATE_ERROR : STATE_VALID ;
+ onBitmapAvailable(mBitmap);
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
+ }
+
+ @Override
+ public long getIdentity() {
+ return mMediaItem != null
+ ? System.identityHashCode(mMediaItem.getPath())
+ : System.identityHashCode(this);
+ }
+
+ public void requestImage() {
+ mImageRequested = true;
+ if (mState == STATE_INVALID) {
+ mState = STATE_UPDATING;
+ startLoadBitmap();
+ }
+ }
+
+ public void cancelImageRequest() {
+ mImageRequested = false;
+ if (mState == STATE_UPDATING) {
+ mState = STATE_CANCELING;
+ cancelLoadBitmap();
+ }
+ }
+
+ private boolean inState(int states) {
+ return (mState & states) != 0;
+ }
+
+ public void recycle() {
+ if (!inState(STATE_UPDATING | STATE_CANCELING)) {
+ if (mBitmap != null) mBitmap = null;
+ } else {
+ mRecycling = true;
+ cancelImageRequest();
+ }
+ }
+
+ public boolean isRequestInProgress() {
+ return mImageRequested && inState(STATE_UPDATING | STATE_CANCELING);
+ }
+
+ abstract protected void startLoadBitmap();
+ abstract protected void cancelLoadBitmap();
+ abstract protected void onBitmapAvailable(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
new file mode 100644
index 000000000..6c81a3f6a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActionBar;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.CustomMenu.DropDownMenu;
+import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ShareActionProvider;
+
+import java.util.ArrayList;
+
+public class ActionModeHandler implements ActionMode.Callback {
+ private static final String TAG = "ActionModeHandler";
+ private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
+ | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
+ | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT;
+
+ public interface ActionModeListener {
+ public boolean onActionItemClicked(MenuItem item);
+ }
+
+ private final GalleryActivity mActivity;
+ private final MenuExecutor mMenuExecutor;
+ private final SelectionManager mSelectionManager;
+ private Menu mMenu;
+ private DropDownMenu mSelectionMenu;
+ private ActionModeListener mListener;
+ private Future<?> mMenuTask;
+ private Handler mMainHandler;
+ private ShareActionProvider mShareActionProvider;
+
+ public ActionModeHandler(
+ GalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mMenuExecutor = new MenuExecutor(activity, selectionManager);
+ mMainHandler = new Handler(activity.getMainLooper());
+ }
+
+ public ActionMode startActionMode() {
+ Activity a = (Activity) mActivity;
+ final ActionMode actionMode = a.startActionMode(this);
+ CustomMenu customMenu = new CustomMenu(a);
+ View customView = LayoutInflater.from(a).inflate(
+ R.layout.action_mode, null);
+ actionMode.setCustomView(customView);
+ mSelectionMenu = customMenu.addDropDownMenu(
+ (Button) customView.findViewById(R.id.selection_menu),
+ R.menu.selection);
+ customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ return onActionItemClicked(actionMode, item);
+ }
+ });
+ return actionMode;
+ }
+
+ public void setTitle(String title) {
+ mSelectionMenu.setTitle(title);
+ }
+
+ public void setActionModeListener(ActionModeListener listener) {
+ mListener = listener;
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ boolean result;
+ if (mListener != null) {
+ result = mListener.onActionItemClicked(item);
+ if (result) {
+ mSelectionManager.leaveSelectionMode();
+ return result;
+ }
+ }
+ ProgressListener listener = null;
+ if (item.getItemId() == R.id.action_import) {
+ listener = new ImportCompleteListener(mActivity);
+ }
+ result = mMenuExecutor.onMenuClicked(item, listener);
+ if (item.getItemId() == R.id.action_select_all) {
+ updateSupportedOperation();
+
+ // For clients who call SelectionManager.selectAll() directly, we need to ensure the
+ // menu status is consistent with selection manager.
+ item = mSelectionMenu.findItem(R.id.action_select_all);
+ if (item != null) {
+ if (mSelectionManager.inSelectAllMode()) {
+ item.setChecked(true);
+ item.setTitle(R.string.deselect_all);
+ } else {
+ item.setChecked(false);
+ item.setTitle(R.string.select_all);
+ }
+ }
+ }
+ return result;
+ }
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.operation, menu);
+
+ mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
+
+ mMenu = menu;
+ return true;
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mSelectionManager.leaveSelectionMode();
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ private void updateMenuOptionsAndSharingIntent(JobContext jc) {
+ ArrayList<Path> paths = mSelectionManager.getSelected(true);
+ if (paths.size() == 0) return;
+
+ int operation = MediaObject.SUPPORT_ALL;
+ DataManager manager = mActivity.getDataManager();
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ int type = 0;
+ for (Path path : paths) {
+ if (jc.isCancelled()) return;
+ int support = manager.getSupportedOperations(path);
+ type |= manager.getMediaType(path);
+ operation &= support;
+ if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ }
+ final Intent intent = new Intent();
+ final String mimeType = MenuExecutor.getMimeType(type);
+
+ if (paths.size() == 1) {
+ if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) {
+ operation &= ~MediaObject.SUPPORT_EDIT;
+ }
+ } else {
+ operation &= SUPPORT_MULTIPLE_MASK;
+ }
+
+
+ Log.v(TAG, "Sharing intent MIME type=" + mimeType + ", uri size = "+ uris.size());
+ if (uris.size() > 1) {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ intent.setAction(Intent.ACTION_SEND).setType(mimeType);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+ intent.setType(mimeType);
+
+ final int supportedOperation = operation;
+
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ MenuExecutor.updateMenuOperation(mMenu, supportedOperation);
+
+ if (mShareActionProvider != null) {
+ Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction());
+ mShareActionProvider.setShareIntent(intent);
+ }
+ }
+ });
+ }
+
+ public void updateSupportedOperation(Path path, boolean selected) {
+ // TODO: We need to improve the performance
+ updateSupportedOperation();
+ }
+
+ public void updateSupportedOperation() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ }
+
+ // Disable share action until share intent is in good shape
+ if (mShareActionProvider != null) {
+ Log.v(TAG, "Disable sharing until intent is ready");
+ mShareActionProvider.setShareIntent(null);
+ }
+
+ // Generate sharing intent and update supported operations in the background
+ mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ public Void run(JobContext jc) {
+ updateMenuOptionsAndSharingIntent(jc);
+ return null;
+ }
+ });
+ }
+
+ public void pause() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ mMenuTask = null;
+ }
+ mMenuExecutor.pause();
+ }
+
+ public void resume() {
+ updateSupportedOperation();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AdaptiveBackground.java b/src/com/android/gallery3d/ui/AdaptiveBackground.java
new file mode 100644
index 000000000..42cb2ccdb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AdaptiveBackground.java
@@ -0,0 +1,128 @@
+/*
+ * 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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LightingColorFilter;
+import android.graphics.Paint;
+
+import com.android.gallery3d.anim.FloatAnimation;
+
+public class AdaptiveBackground extends GLView {
+
+ private static final int BACKGROUND_WIDTH = 128;
+ private static final int BACKGROUND_HEIGHT = 64;
+ private static final int FILTERED_COLOR = 0xffaaaaaa;
+ private static final int ANIMATION_DURATION = 500;
+
+ private BasicTexture mOldBackground;
+ private BasicTexture mBackground;
+
+ private final Paint mPaint;
+ private Bitmap mPendingBitmap;
+ private final FloatAnimation mAnimation =
+ new FloatAnimation(0, 1, ANIMATION_DURATION);
+
+ public AdaptiveBackground() {
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ paint.setColorFilter(new LightingColorFilter(FILTERED_COLOR, 0));
+ mPaint = paint;
+ }
+
+ public Bitmap getAdaptiveBitmap(Bitmap bitmap) {
+ Bitmap target = Bitmap.createBitmap(
+ BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(target);
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int left = 0;
+ int top = 0;
+ if (width * BACKGROUND_HEIGHT > height * BACKGROUND_WIDTH) {
+ float scale = (float) BACKGROUND_HEIGHT / height;
+ canvas.scale(scale, scale);
+ left = (BACKGROUND_WIDTH - (int) (width * scale + 0.5)) / 2;
+ } else {
+ float scale = (float) BACKGROUND_WIDTH / width;
+ canvas.scale(scale, scale);
+ top = (BACKGROUND_HEIGHT - (int) (height * scale + 0.5)) / 2;
+ }
+ canvas.drawBitmap(bitmap, left, top, mPaint);
+ BoxBlurFilter.apply(target,
+ BoxBlurFilter.MODE_REPEAT, BoxBlurFilter.MODE_CLAMP);
+ return target;
+ }
+
+ private void startTransition(Bitmap bitmap) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ if (mBackground == null) {
+ mBackground = texture;
+ } else {
+ if (mOldBackground != null) mOldBackground.recycle();
+ mOldBackground = mBackground;
+ mBackground = texture;
+ mAnimation.start();
+ }
+ invalidate();
+ }
+
+ public void setImage(Bitmap bitmap) {
+ if (mAnimation.isActive()) {
+ mPendingBitmap = bitmap;
+ } else {
+ startTransition(bitmap);
+ }
+ }
+
+ public void setScrollPosition(int position) {
+ if (mScrollX == position) return;
+ mScrollX = position;
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ if (mBackground == null) return;
+
+ int height = getHeight();
+ float scale = (float) height / BACKGROUND_HEIGHT;
+ int width = (int) (BACKGROUND_WIDTH * scale + 0.5f);
+ int scroll = mScrollX;
+ int start = (scroll / width) * width;
+
+ if (mOldBackground == null) {
+ for (int i = start, n = scroll + getWidth(); i < n; i += width) {
+ mBackground.draw(canvas, i - scroll, 0, width, height);
+ }
+ } else {
+ boolean moreAnimation =
+ mAnimation.calculate(canvas.currentAnimationTimeMillis());
+ float ratio = mAnimation.get();
+ for (int i = start, n = scroll + getWidth(); i < n; i += width) {
+ canvas.drawMixed(mOldBackground,
+ mBackground, ratio, i - scroll, 0, width, height);
+ }
+ if (moreAnimation) {
+ invalidate();
+ } else if (mPendingBitmap != null) {
+ startTransition(mPendingBitmap);
+ mPendingBitmap = null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
new file mode 100644
index 000000000..92d8b4156
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -0,0 +1,543 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Message;
+
+public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener {
+ private static final String TAG = "GallerySlidingWindow";
+ private static final int MSG_LOAD_BITMAP_DONE = 0;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentInvalidated();
+ public void onWindowContentChanged(
+ int slot, AlbumSetItem old, AlbumSetItem update);
+ }
+
+ private final AlbumSetView.Model mSource;
+ private int mSize;
+ private int mLabelWidth;
+ private int mDisplayItemSize;
+ private int mLabelFontSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private final MyAlbumSetItem mData[];
+ private SelectionDrawer mSelectionDrawer;
+ private final ColorTexture mWaitLoadingTexture;
+
+ private SynchronizedHandler mHandler;
+ private ThreadPool mThreadPool;
+
+ private int mActiveRequestCount = 0;
+ private String mLoadingLabel;
+ private boolean mIsActive = false;
+
+ private static class MyAlbumSetItem extends AlbumSetItem {
+ public Path setPath;
+ public int sourceType;
+ public int cacheFlag;
+ public int cacheStatus;
+ }
+
+ public AlbumSetSlidingWindow(GalleryActivity activity, int labelWidth,
+ int displayItemSize, int labelFontSize, SelectionDrawer drawer,
+ AlbumSetView.Model source, int cacheSize) {
+ source.setModelListener(this);
+ mLabelWidth = labelWidth;
+ mDisplayItemSize = displayItemSize;
+ mLabelFontSize = labelFontSize;
+ mLoadingLabel = activity.getAndroidContext().getString(R.string.loading);
+ mSource = source;
+ mSelectionDrawer = drawer;
+ mData = new MyAlbumSetItem[cacheSize];
+ mSize = source.size();
+
+ mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT);
+ mWaitLoadingTexture.setSize(1, 1);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE);
+ ((GalleryDisplayItem) message.obj).onLoadBitmapDone();
+ }
+ };
+
+ mThreadPool = activity.getThreadPool();
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumSetItem get(int slotIndex) {
+ Utils.assertTrue(isActiveSlot(slotIndex),
+ "invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ return mData[slotIndex % mData.length];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ } else {
+ for (int i = mContentStart; i < contentStart; ++i) {
+ freeSlotContent(i);
+ }
+ for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart, n = mContentStart; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ for (int i = mContentEnd; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ }
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+
+ public void setActiveWindow(int start, int end) {
+ Utils.assertTrue(
+ start <= end && end - start <= mData.length && end <= mSize,
+ "start = %s, end = %s, length = %s, size = %s",
+ start, end, mData.length, mSize);
+
+ AlbumSetItem data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ requestImagesInSlot(mActiveEnd + i);
+ requestImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ cancelImagesInSlot(mActiveEnd + i);
+ cancelImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void requestImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetItem items = mData[slotIndex % mData.length];
+ for (DisplayItem item : items.covers) {
+ ((GalleryDisplayItem) item).requestImage();
+ }
+ }
+
+ private void cancelImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetItem items = mData[slotIndex % mData.length];
+ for (DisplayItem item : items.covers) {
+ ((GalleryDisplayItem) item).cancelImageRequest();
+ }
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumSetItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumSetItem original = data[index];
+ if (original != null) {
+ data[index] = null;
+ for (DisplayItem item : original.covers) {
+ ((GalleryDisplayItem) item).recycle();
+ }
+ }
+ }
+
+ private long getMediaSetDataVersion(MediaSet set) {
+ return set == null
+ ? MediaSet.INVALID_DATA_VERSION
+ : set.getDataVersion();
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ MediaSet set = mSource.getMediaSet(slotIndex);
+
+ MyAlbumSetItem item = new MyAlbumSetItem();
+ MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
+ item.covers = new GalleryDisplayItem[coverItems.length];
+ item.sourceType = identifySourceType(set);
+ item.cacheFlag = identifyCacheFlag(set);
+ item.cacheStatus = identifyCacheStatus(set);
+ item.setPath = set == null ? null : set.getPath();
+
+ for (int i = 0; i < coverItems.length; ++i) {
+ item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]);
+ }
+ item.labelItem = new LabelDisplayItem(slotIndex);
+ item.setDataVersion = getMediaSetDataVersion(set);
+ mData[slotIndex % mData.length] = item;
+ }
+
+ private boolean isCoverItemsChanged(int slotIndex) {
+ AlbumSetItem original = mData[slotIndex % mData.length];
+ if (original == null) return true;
+ MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
+
+ if (original.covers.length != coverItems.length) return true;
+ for (int i = 0, n = coverItems.length; i < n; ++i) {
+ GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i];
+ if (g.mDataVersion != coverItems[i].getDataVersion()) return true;
+ }
+ return false;
+ }
+
+ private void updateSlotContent(final int slotIndex) {
+
+ MyAlbumSetItem data[] = mData;
+ int pos = slotIndex % data.length;
+ MyAlbumSetItem original = data[pos];
+
+ if (!isCoverItemsChanged(slotIndex)) {
+ MediaSet set = mSource.getMediaSet(slotIndex);
+ original.sourceType = identifySourceType(set);
+ original.cacheFlag = identifyCacheFlag(set);
+ original.cacheStatus = identifyCacheStatus(set);
+ original.setPath = set == null ? null : set.getPath();
+ ((LabelDisplayItem) original.labelItem).updateContent();
+ if (mListener != null) mListener.onContentInvalidated();
+ return;
+ }
+
+ prepareSlotContent(slotIndex);
+ AlbumSetItem update = data[pos];
+
+ if (mListener != null && isActiveSlot(slotIndex)) {
+ mListener.onWindowContentChanged(slotIndex, original, update);
+ }
+ if (original != null) {
+ for (DisplayItem item : original.covers) {
+ ((GalleryDisplayItem) item).recycle();
+ }
+ }
+ }
+
+ private void notifySlotChanged(int slotIndex) {
+ // If the updated content is not cached, ignore it
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) {
+ Log.w(TAG, String.format(
+ "invalid update: %s is outside (%s, %s)",
+ slotIndex, mContentStart, mContentEnd) );
+ return;
+ }
+ updateSlotContent(slotIndex);
+ boolean isActiveSlot = isActiveSlot(slotIndex);
+ if (mActiveRequestCount == 0 || isActiveSlot) {
+ for (DisplayItem item : mData[slotIndex % mData.length].covers) {
+ GalleryDisplayItem galleryItem = (GalleryDisplayItem) item;
+ galleryItem.requestImage();
+ if (isActiveSlot && galleryItem.isRequestInProgress()) {
+ ++mActiveRequestCount;
+ }
+ }
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ for (DisplayItem item : mData[i % mData.length].covers) {
+ GalleryDisplayItem coverItem = (GalleryDisplayItem) item;
+ coverItem.requestImage();
+ if (coverItem.isRequestInProgress()) ++mActiveRequestCount;
+ }
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class GalleryDisplayItem extends AbstractDisplayItem
+ implements FutureListener<Bitmap> {
+ private Future<Bitmap> mFuture;
+ private final int mSlotIndex;
+ private final int mCoverIndex;
+ private final int mMediaType;
+ private Texture mContent;
+ private final long mDataVersion;
+
+ public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
+ super(item);
+ mSlotIndex = slotIndex;
+ mCoverIndex = coverIndex;
+ mMediaType = item.getMediaType();
+ mDataVersion = item.getDataVersion();
+ updateContent(mWaitLoadingTexture);
+ }
+
+ @Override
+ protected void onBitmapAvailable(Bitmap bitmap) {
+ if (isActiveSlot(mSlotIndex)) {
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ }
+ if (bitmap != null) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setThrottled(true);
+ updateContent(texture);
+ if (mListener != null) mListener.onContentInvalidated();
+ }
+ }
+
+ private void updateContent(Texture content) {
+ mContent = content;
+
+ int width = content.getWidth();
+ int height = content.getHeight();
+
+ float scale = (float) mDisplayItemSize / Math.max(width, height);
+
+ width = (int) Math.floor(width * scale);
+ height = (int) Math.floor(height * scale);
+
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ int cacheFlag = MediaSet.CACHE_FLAG_NO;
+ int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED;
+ MyAlbumSetItem set = mData[mSlotIndex % mData.length];
+ Path path = set.setPath;
+ if (mCoverIndex == 0) {
+ sourceType = set.sourceType;
+ cacheFlag = set.cacheFlag;
+ cacheStatus = set.cacheStatus;
+ }
+
+ mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
+ getRotation(), path, mCoverIndex, sourceType, mMediaType,
+ cacheFlag == MediaSet.CACHE_FLAG_FULL,
+ (cacheFlag == MediaSet.CACHE_FLAG_FULL)
+ && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL));
+ return false;
+ }
+
+ @Override
+ public void startLoadBitmap() {
+ mFuture = mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+
+ @Override
+ public void cancelLoadBitmap() {
+ mFuture.cancel();
+ }
+
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
+ }
+
+ private void onLoadBitmapDone() {
+ Future<Bitmap> future = mFuture;
+ mFuture = null;
+ updateImage(future.get(), future.isCancelled());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex);
+ }
+ }
+
+ private static int identifySourceType(MediaSet set) {
+ if (set == null) {
+ return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ }
+
+ Path path = set.getPath();
+ if (MediaSetUtils.isCameraSource(path)) {
+ return SelectionDrawer.DATASOURCE_TYPE_CAMERA;
+ }
+
+ int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ String prefix = path.getPrefix();
+
+ if (prefix.equals("picasa")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_PICASA;
+ } else if (prefix.equals("local") || prefix.equals("merge")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_LOCAL;
+ } else if (prefix.equals("mtp")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_MTP;
+ }
+
+ return type;
+ }
+
+ private static int identifyCacheFlag(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_FLAG_NO;
+ }
+
+ return set.getCacheFlag();
+ }
+
+ private static int identifyCacheStatus(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_STATUS_NOT_CACHED;
+ }
+
+ return set.getCacheStatus();
+ }
+
+ private class LabelDisplayItem extends DisplayItem {
+ private static final int FONT_COLOR = Color.WHITE;
+
+ private StringTexture mTexture;
+ private String mLabel;
+ private String mPostfix;
+ private final int mSlotIndex;
+
+ public LabelDisplayItem(int slotIndex) {
+ mSlotIndex = slotIndex;
+ updateContent();
+ }
+
+ public boolean updateContent() {
+ String label = mLoadingLabel;
+ String postfix = null;
+ MediaSet set = mSource.getMediaSet(mSlotIndex);
+ if (set != null) {
+ label = Utils.ensureNotNull(set.getName());
+ postfix = " (" + set.getTotalMediaItemCount() + ")";
+ }
+ if (Utils.equals(label, mLabel)
+ && Utils.equals(postfix, mPostfix)) return false;
+ mTexture = StringTexture.newInstance(
+ label, postfix, mLabelFontSize, FONT_COLOR, mLabelWidth, true);
+ setSize(mTexture.getWidth(), mTexture.getHeight());
+ return true;
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ mTexture.draw(canvas, -mWidth / 2, -mHeight / 2);
+ return false;
+ }
+
+ @Override
+ public long getIdentity() {
+ return System.identityHashCode(this);
+ }
+ }
+
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null && mIsActive) mListener.onSizeChanged(mSize);
+ }
+ }
+
+ public void onWindowContentChanged(int index) {
+ if (!mIsActive) {
+ // paused, ignore slot changed event
+ return;
+ }
+ notifySlotChanged(index);
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java
new file mode 100644
index 000000000..ef066b34c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetView.java
@@ -0,0 +1,240 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.graphics.Rect;
+
+import java.util.Random;
+
+public class AlbumSetView extends SlotView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetView";
+ private static final int CACHE_SIZE = 32;
+ private static final float PHOTO_DISTANCE = 35f;
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private Random mRandom = new Random();
+ private long mSeed = mRandom.nextLong();
+
+ private AlbumSetSlidingWindow mDataWindow;
+ private final GalleryActivity mActivity;
+ private final int mSlotWidth;
+ private final int mDisplayItemSize;
+ private final int mLabelFontSize;
+ private final int mLabelOffsetY;
+ private final int mLabelMargin;
+
+ private SelectionDrawer mSelectionDrawer;
+
+ public static interface Model {
+ public MediaItem[] getCoverItems(int index);
+ public MediaSet getMediaSet(int index);
+ public int size();
+ public void setActiveWindow(int start, int end);
+ public void setModelListener(ModelListener listener);
+ }
+
+ public static interface ModelListener {
+ public void onWindowContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ public static class AlbumSetItem {
+ public DisplayItem[] covers;
+ public DisplayItem labelItem;
+ public long setDataVersion;
+ }
+
+ public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer,
+ int slotWidth, int slotHeight, int displayItemSize,
+ int labelFontSize, int labelOffsetY, int labelMargin) {
+ super(activity.getAndroidContext());
+ mActivity = activity;
+ setSelectionDrawer(drawer);
+ setSlotSize(slotWidth, slotHeight);
+ mSlotWidth = slotWidth;
+ mDisplayItemSize = displayItemSize;
+ mLabelFontSize = labelFontSize;
+ mLabelOffsetY = labelOffsetY;
+ mLabelMargin = labelMargin;
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ if (mDataWindow != null) {
+ mDataWindow.setSelectionDrawer(drawer);
+ }
+ }
+
+ public void setModel(AlbumSetView.Model model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSetSlidingWindow(mActivity,
+ mSlotWidth - mLabelMargin * 2, mDisplayItemSize, mLabelFontSize,
+ mSelectionDrawer, model, CACHE_SIZE);
+ mDataWindow.setListener(new MyCacheListener());
+ setSlotCount(mDataWindow.size());
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ private void putSlotContent(int slotIndex, AlbumSetItem entry) {
+ // Get displayItems from mItemsetMap or create them from MediaSet.
+ Utils.assertTrue(entry != null);
+ Rect rect = getSlotRect(slotIndex);
+
+ DisplayItem[] items = entry.covers;
+ mRandom.setSeed(slotIndex ^ mSeed);
+
+ int x = (rect.left + rect.right) / 2;
+ int y = (rect.top + rect.bottom) / 2;
+
+ Position basePosition = new Position(x, y, 0);
+
+ // Put the cover items in reverse order, so that the first item is on
+ // top of the rest.
+ int labelY = y + mLabelOffsetY - entry.labelItem.getHeight() / 2;
+ Position position = new Position(x, labelY, 0f);
+ putDisplayItem(position, position, entry.labelItem);
+
+ for (int i = 0, n = items.length; i < n; ++i) {
+ DisplayItem item = items[i];
+ float dx = 0;
+ float dy = 0;
+ float dz = 0f;
+ float theta = 0;
+ if (i != 0) {
+ dz = i * PHOTO_DISTANCE;
+ }
+ position = new Position(x + dx, y + dy, dz);
+ position.theta = theta;
+ putDisplayItem(position, basePosition, item);
+ }
+
+ }
+
+ private void freeSlotContent(int index, AlbumSetItem entry) {
+ if (entry == null) return;
+ for (DisplayItem item : entry.covers) {
+ removeDisplayItem(item);
+ }
+ removeDisplayItem(entry.labelItem);
+ }
+
+ public int size() {
+ return mDataWindow.size();
+ }
+
+ @Override
+ public void onLayoutChanged(int width, int height) {
+ updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ public void onScrollPositionChanged(int position) {
+ super.onScrollPositionChanged(position);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ private void updateVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) {
+ // we need to set the mDataWindow active range in any case.
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+ if (start >= mVisibleEnd || mVisibleStart >= end) {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ } else {
+ for (int i = mVisibleStart; i < start; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = end, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start, n = mVisibleStart; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = mVisibleEnd; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+ mVisibleStart = start;
+ mVisibleEnd = end;
+
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mSelectionDrawer.prepareDrawing();
+ super.render(canvas);
+ }
+
+ private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+ public void onSizeChanged(int size) {
+ // If the layout parameters are changed, we need reput all items.
+ if (setSlotCount(size)) updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ invalidate();
+ }
+
+ public void onWindowContentChanged(int slot, AlbumSetItem old, AlbumSetItem update) {
+ freeSlotContent(slot, old);
+ putSlotContent(slot, update);
+ invalidate();
+ }
+
+ public void onContentInvalidated() {
+ invalidate();
+ }
+ }
+
+ public void pause() {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.pause();
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
new file mode 100644
index 000000000..9e44bd1d2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -0,0 +1,433 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.LruCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+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 android.graphics.Color;
+import android.os.Message;
+
+public class AlbumSlidingWindow implements AlbumView.ModelListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSlidingWindow";
+
+ private static final int MSG_LOAD_BITMAP_DONE = 0;
+ private static final int MSG_UPDATE_SLOT = 1;
+ private static final int MIN_THUMB_SIZE = 100;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentInvalidated();
+ public void onWindowContentChanged(
+ int slot, DisplayItem old, DisplayItem update);
+ }
+
+ private final AlbumView.Model mSource;
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+ private int mFocusIndex = -1;
+
+ private final AlbumDisplayItem mData[];
+ private final ColorTexture mWaitLoadingTexture;
+ private SelectionDrawer mSelectionDrawer;
+
+ private SynchronizedHandler mHandler;
+ private ThreadPool mThreadPool;
+ private int mSlotWidth, mSlotHeight;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+
+ private int mDisplayItemSize; // 0: disabled
+ private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000);
+
+ public AlbumSlidingWindow(GalleryActivity activity,
+ AlbumView.Model source, int cacheSize,
+ int slotWidth, int slotHeight, int displayItemSize) {
+ source.setModelListener(this);
+ mSource = source;
+ mData = new AlbumDisplayItem[cacheSize];
+ mSize = source.size();
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ mDisplayItemSize = displayItemSize;
+
+ mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT);
+ mWaitLoadingTexture.setSize(1, 1);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_LOAD_BITMAP_DONE: {
+ ((AlbumDisplayItem) message.obj).onLoadBitmapDone();
+ break;
+ }
+ case MSG_UPDATE_SLOT: {
+ updateSlotContent(message.arg1);
+ break;
+ }
+ }
+ }
+ };
+
+ mThreadPool = activity.getThreadPool();
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ mFocusIndex = slotIndex;
+ }
+
+ public DisplayItem get(int slotIndex) {
+ Utils.assertTrue(isActiveSlot(slotIndex),
+ "invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ return mData[slotIndex % mData.length];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (!mIsActive) {
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ mSource.setActiveWindow(contentStart, contentEnd);
+ return;
+ }
+
+ if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ } else {
+ for (int i = mContentStart; i < contentStart; ++i) {
+ freeSlotContent(i);
+ }
+ for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart, n = mContentStart; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ for (int i = mContentEnd; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ }
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+
+ public void setActiveWindow(int start, int end) {
+ Utils.assertTrue(start <= end
+ && end - start <= mData.length && end <= mSize,
+ "%s, %s, %s, %s", start, end, mData.length, mSize);
+ DisplayItem data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ requestSlotImage(mActiveEnd + i, false);
+ requestSlotImage(mActiveStart - 1 - i, false);
+ }
+ }
+
+ private void requestSlotImage(int slotIndex, boolean isActive) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumDisplayItem item = mData[slotIndex % mData.length];
+ item.requestImage();
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ cancelSlotImage(mActiveEnd + i, false);
+ cancelSlotImage(mActiveStart - 1 - i, false);
+ }
+ }
+
+ private void cancelSlotImage(int slotIndex, boolean isActive) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumDisplayItem item = mData[slotIndex % mData.length];
+ item.cancelImageRequest();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumDisplayItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumDisplayItem original = data[index];
+ if (original != null) {
+ original.recycle();
+ data[index] = null;
+ }
+ }
+
+ private void prepareSlotContent(final int slotIndex) {
+ mData[slotIndex % mData.length] = new AlbumDisplayItem(
+ slotIndex, mSource.get(slotIndex));
+ }
+
+ private void updateSlotContent(final int slotIndex) {
+ MediaItem item = mSource.get(slotIndex);
+ AlbumDisplayItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumDisplayItem original = data[index];
+ AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item);
+ data[index] = update;
+ boolean isActive = isActiveSlot(slotIndex);
+ if (mListener != null && isActive) {
+ mListener.onWindowContentChanged(slotIndex, original, update);
+ }
+ if (original != null) {
+ if (isActive && original.isRequestInProgress()) {
+ --mActiveRequestCount;
+ }
+ original.recycle();
+ }
+ if (isActive) {
+ if (mActiveRequestCount == 0) cancelNonactiveImages();
+ ++mActiveRequestCount;
+ update.requestImage();
+ } else {
+ if (mActiveRequestCount == 0) update.requestImage();
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ AlbumDisplayItem data[] = mData;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumDisplayItem item = data[i % data.length];
+ item.requestImage();
+ if (item.isRequestInProgress()) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class AlbumDisplayItem extends AbstractDisplayItem
+ implements FutureListener<Bitmap>, Job<Bitmap> {
+ private Future<Bitmap> mFuture;
+ private final int mSlotIndex;
+ private final int mMediaType;
+ private Texture mContent;
+
+ public AlbumDisplayItem(int slotIndex, MediaItem item) {
+ super(item);
+ mMediaType = (item == null)
+ ? MediaItem.MEDIA_TYPE_UNKNOWN
+ : item.getMediaType();
+ mSlotIndex = slotIndex;
+ updateContent(mWaitLoadingTexture);
+ }
+
+ @Override
+ protected void onBitmapAvailable(Bitmap bitmap) {
+ boolean isActiveSlot = isActiveSlot(mSlotIndex);
+ if (isActiveSlot) {
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ }
+ if (bitmap != null) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setThrottled(true);
+ updateContent(texture);
+ if (mListener != null && isActiveSlot) {
+ mListener.onContentInvalidated();
+ }
+ }
+ }
+
+ private void updateContent(Texture content) {
+ mContent = content;
+
+ int width = mContent.getWidth();
+ int height = mContent.getHeight();
+
+ float scalex = mDisplayItemSize / (float) width;
+ float scaley = mDisplayItemSize / (float) height;
+ float scale = Math.min(scalex, scaley);
+
+ width = (int) Math.floor(width * scale);
+ height = (int) Math.floor(height * scale);
+
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ if (pass == 0) {
+ Path path = null;
+ if (mMediaItem != null) path = mMediaItem.getPath();
+ mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
+ getRotation(), path, mMediaType);
+ return (mFocusIndex == mSlotIndex);
+ } else if (pass == 1) {
+ mSelectionDrawer.drawFocus(canvas, mWidth, mHeight);
+ }
+ return false;
+ }
+
+ @Override
+ public void startLoadBitmap() {
+ if (mDisplayItemSize < MIN_THUMB_SIZE) {
+ Path path = mMediaItem.getPath();
+ if (mImageCache.containsKey(path)) {
+ Bitmap bitmap = mImageCache.get(path);
+ updateImage(bitmap, false);
+ return;
+ }
+ mFuture = mThreadPool.submit(this, this);
+ } else {
+ mFuture = mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+ }
+
+ // This gets the bitmap and scale it down.
+ public Bitmap run(JobContext jc) {
+ Job<Bitmap> job = mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL);
+ Bitmap bitmap = job.run(jc);
+ if (bitmap != null) {
+ bitmap = BitmapUtils.resizeDownBySideLength(
+ bitmap, mDisplayItemSize, true);
+ }
+ return bitmap;
+ }
+
+ @Override
+ public void cancelLoadBitmap() {
+ if (mFuture != null) {
+ mFuture.cancel();
+ }
+ }
+
+ @Override
+ public void onFutureDone(Future<Bitmap> bitmap) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
+ }
+
+ private void onLoadBitmapDone() {
+ Future<Bitmap> future = mFuture;
+ mFuture = null;
+ Bitmap bitmap = future.get();
+ boolean isCancelled = future.isCancelled();
+ if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) {
+ Path path = mMediaItem.getPath();
+ mImageCache.put(path, bitmap);
+ }
+ updateImage(bitmap, isCancelled);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AlbumDisplayItem[%s]", mSlotIndex);
+ }
+ }
+
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ }
+ }
+
+ public void onWindowContentChanged(int index) {
+ if (index >= mContentStart && index < mContentEnd && mIsActive) {
+ updateSlotContent(index);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mImageCache.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java
new file mode 100644
index 000000000..417611a69
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumView.java
@@ -0,0 +1,197 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.graphics.Rect;
+
+public class AlbumView extends SlotView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumView";
+ private static final int CACHE_SIZE = 64;
+
+ private int mVisibleStart = 0;
+ private int mVisibleEnd = 0;
+
+ private AlbumSlidingWindow mDataWindow;
+ private final GalleryActivity mActivity;
+ private SelectionDrawer mSelectionDrawer;
+ private int mSlotWidth, mSlotHeight;
+ private int mDisplayItemSize;
+
+ private boolean mIsActive = false;
+
+ public static interface Model {
+ public int size();
+ public MediaItem get(int index);
+ public void setActiveWindow(int start, int end);
+ public void setModelListener(ModelListener listener);
+ }
+
+ public static interface ModelListener {
+ public void onWindowContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ public AlbumView(GalleryActivity activity,
+ int slotWidth, int slotHeight, int displayItemSize) {
+ super(activity.getAndroidContext());
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ mDisplayItemSize = displayItemSize;
+ setSlotSize(slotWidth, slotHeight);
+ mActivity = activity;
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ if (mDataWindow != null) mDataWindow.setSelectionDrawer(drawer);
+ }
+
+ public void setModel(Model model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSlidingWindow(
+ mActivity, model, CACHE_SIZE,
+ mSlotWidth, mSlotHeight, mDisplayItemSize);
+ mDataWindow.setSelectionDrawer(mSelectionDrawer);
+ mDataWindow.setListener(new MyDataModelListener());
+ setSlotCount(model.size());
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ if (mDataWindow != null) {
+ mDataWindow.setFocusIndex(slotIndex);
+ }
+ }
+
+ private void putSlotContent(int slotIndex, DisplayItem item) {
+ Rect rect = getSlotRect(slotIndex);
+ Position position = new Position(
+ (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0);
+ putDisplayItem(position, position, item);
+ }
+
+ private void updateVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) {
+ // we need to set the mDataWindow active range in any case.
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+
+ if (!mIsActive) {
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+
+ if (start >= mVisibleEnd || mVisibleStart >= end) {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ } else {
+ for (int i = mVisibleStart; i < start; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ for (int i = end, n = mVisibleEnd; i < n; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start, n = mVisibleStart; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = mVisibleEnd; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ }
+
+ @Override
+ protected void onLayoutChanged(int width, int height) {
+ // Reput all the items
+ updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ protected void onScrollPositionChanged(int position) {
+ super.onScrollPositionChanged(position);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mSelectionDrawer.prepareDrawing();
+ super.render(canvas);
+ }
+
+ private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+
+ public void onContentInvalidated() {
+ invalidate();
+ }
+
+ public void onSizeChanged(int size) {
+ // If the layout parameters are changed, we need reput all items.
+ if (setSlotCount(size)) updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ invalidate();
+ }
+
+ public void onWindowContentChanged(
+ int slotIndex, DisplayItem old, DisplayItem update) {
+ removeDisplayItem(old);
+ putSlotContent(slotIndex, update);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ mDataWindow.resume();
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ removeDisplayItem(mDataWindow.get(i));
+ }
+ mDataWindow.pause();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
new file mode 100644
index 000000000..e93006326
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -0,0 +1,164 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+// BasicTexture is a Texture corresponds to a real GL texture.
+// The state of a BasicTexture indicates whether its data is loaded to GL memory.
+// If a BasicTexture is loaded into GL memory, it has a GL texture id.
+abstract class BasicTexture implements Texture {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "BasicTexture";
+ protected static final int UNSPECIFIED = -1;
+
+ protected static final int STATE_UNLOADED = 0;
+ protected static final int STATE_LOADED = 1;
+ protected static final int STATE_ERROR = -1;
+
+ protected int mId;
+ protected int mState;
+
+ protected int mWidth = UNSPECIFIED;
+ protected int mHeight = UNSPECIFIED;
+
+ private int mTextureWidth;
+ private int mTextureHeight;
+
+ protected WeakReference<GLCanvas> mCanvasRef = null;
+ private static WeakHashMap<BasicTexture, Object> sAllTextures
+ = new WeakHashMap<BasicTexture, Object>();
+ private static ThreadLocal sInFinalizer = new ThreadLocal();
+
+ protected BasicTexture(GLCanvas canvas, int id, int state) {
+ setAssociatedCanvas(canvas);
+ mId = id;
+ mState = state;
+ synchronized (sAllTextures) {
+ sAllTextures.put(this, null);
+ }
+ }
+
+ protected BasicTexture() {
+ this(null, 0, STATE_UNLOADED);
+ }
+
+ protected void setAssociatedCanvas(GLCanvas canvas) {
+ mCanvasRef = canvas == null
+ ? null
+ : new WeakReference<GLCanvas>(canvas);
+ }
+
+ /**
+ * Sets the content size of this texture. In OpenGL, the actual texture
+ * size must be of power of 2, the size of the content may be smaller.
+ */
+ protected void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ mTextureWidth = Utils.nextPowerOf2(width);
+ mTextureHeight = Utils.nextPowerOf2(height);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ // Returns the width rounded to the next power of 2.
+ public int getTextureWidth() {
+ return mTextureWidth;
+ }
+
+ // Returns the height rounded to the next power of 2.
+ public int getTextureHeight() {
+ return mTextureHeight;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ canvas.drawTexture(this, x, y, getWidth(), getHeight());
+ }
+
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ canvas.drawTexture(this, x, y, w, h);
+ }
+
+ // onBind is called before GLCanvas binds this texture.
+ // It should make sure the data is uploaded to GL memory.
+ abstract protected boolean onBind(GLCanvas canvas);
+
+ public boolean isLoaded(GLCanvas canvas) {
+ return mState == STATE_LOADED && mCanvasRef.get() == canvas;
+ }
+
+ // recycle() is called when the texture will never be used again,
+ // so it can free all resources.
+ public void recycle() {
+ freeResource();
+ }
+
+ // yield() is called when the texture will not be used temporarily,
+ // so it can free some resources.
+ // The default implementation unloads the texture from GL memory, so
+ // the subclass should make sure it can reload the texture to GL memory
+ // later, or it will have to override this method.
+ public void yield() {
+ freeResource();
+ }
+
+ private void freeResource() {
+ GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+ if (canvas != null && isLoaded(canvas)) {
+ canvas.unloadTexture(this);
+ }
+ mState = BasicTexture.STATE_UNLOADED;
+ setAssociatedCanvas(null);
+ }
+
+ @Override
+ protected void finalize() {
+ sInFinalizer.set(BasicTexture.class);
+ recycle();
+ sInFinalizer.set(null);
+ }
+
+ // This is for deciding if we can call Bitmap's recycle().
+ // We cannot call Bitmap's recycle() in finalizer because at that point
+ // the finalizer of Bitmap may already be called so recycle() will crash.
+ public static boolean inFinalizer() {
+ return sInFinalizer.get() != null;
+ }
+
+ public static void yieldAllTextures() {
+ synchronized (sAllTextures) {
+ for (BasicTexture t : sAllTextures.keySet()) {
+ t.yield();
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java
new file mode 100644
index 000000000..046bda94c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTexture.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Bitmap;
+
+// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
+//
+// The texture does not own the Bitmap. The user should make sure the Bitmap
+// is valid during the texture's lifetime. When the texture is recycled, it
+// does not free the Bitmap.
+public class BitmapTexture extends UploadedTexture {
+ protected Bitmap mContentBitmap;
+
+ public BitmapTexture(Bitmap bitmap) {
+ Utils.assertTrue(bitmap != null && !bitmap.isRecycled());
+ mContentBitmap = bitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ // Do nothing.
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ return mContentBitmap;
+ }
+
+ public Bitmap getBitmap() {
+ return mContentBitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
new file mode 100644
index 000000000..a47337fa2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.BitmapUtils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Bitmap.Config;
+
+import java.util.ArrayList;
+
+public class BitmapTileProvider implements TileImageView.Model {
+ private final Bitmap mBackup;
+ private final Bitmap[] mMipmaps;
+ private final Config mConfig;
+ private final int mImageWidth;
+ private final int mImageHeight;
+
+ private boolean mRecycled = false;
+
+ public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) {
+ mImageWidth = bitmap.getWidth();
+ mImageHeight = bitmap.getHeight();
+ ArrayList<Bitmap> list = new ArrayList<Bitmap>();
+ list.add(bitmap);
+ while (bitmap.getWidth() > maxBackupSize
+ || bitmap.getHeight() > maxBackupSize) {
+ bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false);
+ list.add(bitmap);
+ }
+
+ mBackup = list.remove(list.size() - 1);
+ mMipmaps = list.toArray(new Bitmap[list.size()]);
+ mConfig = Config.ARGB_8888;
+ }
+
+ public Bitmap getBackupImage() {
+ return mBackup;
+ }
+
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ public int getLevelCount() {
+ return mMipmaps.length;
+ }
+
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, mConfig);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(mMipmaps[level], -(x >> level), -(y >> level), null);
+ return result;
+ }
+
+ public void recycle() {
+ if (mRecycled) return;
+ mRecycled = true;
+ for (Bitmap bitmap : mMipmaps) {
+ BitmapUtils.recycleSilently(bitmap);
+ }
+ BitmapUtils.recycleSilently(mBackup);
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+
+ public boolean isFailedToLoad() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BoxBlurFilter.java b/src/com/android/gallery3d/ui/BoxBlurFilter.java
new file mode 100644
index 000000000..0497a61fa
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BoxBlurFilter.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ui;
+
+import android.graphics.Bitmap;
+
+
+public class BoxBlurFilter {
+ private static final int RED_MASK = 0xff0000;
+ private static final int RED_MASK_SHIFT = 16;
+ private static final int GREEN_MASK = 0x00ff00;
+ private static final int GREEN_MASK_SHIFT = 8;
+ private static final int BLUE_MASK = 0x0000ff;
+ private static final int RADIUS = 4;
+ private static final int KERNEL_SIZE = RADIUS * 2 + 1;
+ private static final int NUM_COLORS = 256;
+ private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS];
+
+ public static final int MODE_REPEAT = 1;
+ public static final int MODE_CLAMP = 2;
+
+ static {
+ int index = 0;
+ // Build a lookup table from summed to normalized kernel values.
+ // The formula: KERNAL_NORM[value] = value / KERNEL_SIZE
+ for (int i = 0; i < NUM_COLORS; ++i) {
+ for (int j = 0; j < KERNEL_SIZE; ++j) {
+ KERNEL_NORM[index++] = i;
+ }
+ }
+ }
+
+ private BoxBlurFilter() {
+ }
+
+ private static int sample(int x, int width, int mode) {
+ if (x >= 0 && x < width) return x;
+ return mode == MODE_REPEAT
+ ? x < 0 ? x + width : x - width
+ : x < 0 ? 0 : width - 1;
+ }
+
+ public static void apply(
+ Bitmap bitmap, int horizontalMode, int verticalMode) {
+
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int data[] = new int[width * height];
+ bitmap.getPixels(data, 0, width, 0, 0, width, height);
+ int temp[] = new int[width * height];
+ applyOneDimension(data, temp, width, height, horizontalMode);
+ applyOneDimension(temp, data, height, width, verticalMode);
+ bitmap.setPixels(data, 0, width, 0, 0, width, height);
+ }
+
+ private static void applyOneDimension(
+ int[] in, int[] out, int width, int height, int mode) {
+ for (int y = 0, read = 0; y < height; ++y, read += width) {
+ // Evaluate the kernel for the first pixel in the row.
+ int red = 0;
+ int green = 0;
+ int blue = 0;
+ for (int i = -RADIUS; i <= RADIUS; ++i) {
+ int argb = in[read + sample(i, width, mode)];
+ red += (argb & RED_MASK) >> RED_MASK_SHIFT;
+ green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT;
+ blue += argb & BLUE_MASK;
+ }
+ for (int x = 0, write = y; x < width; ++x, write += height) {
+ // Output the current pixel.
+ out[write] = 0xFF000000
+ | (KERNEL_NORM[red] << RED_MASK_SHIFT)
+ | (KERNEL_NORM[green] << GREEN_MASK_SHIFT)
+ | KERNEL_NORM[blue];
+
+ // Slide to the next pixel, adding the new rightmost pixel and
+ // subtracting the former leftmost.
+ int prev = in[read + sample(x - RADIUS, width, mode)];
+ int next = in[read + sample(x + RADIUS + 1, width, mode)];
+ red += ((next & RED_MASK) - (prev & RED_MASK)) >> RED_MASK_SHIFT;
+ green += ((next & GREEN_MASK) - (prev & GREEN_MASK)) >> GREEN_MASK_SHIFT;
+ blue += (next & BLUE_MASK) - (prev & BLUE_MASK);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CacheBarView.java b/src/com/android/gallery3d/ui/CacheBarView.java
new file mode 100644
index 000000000..40f84d8f9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CacheBarView.java
@@ -0,0 +1,270 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StatFs;
+import android.text.format.Formatter;
+import android.view.View.MeasureSpec;
+
+import java.io.File;
+
+public class CacheBarView extends GLView implements TextButton.OnClickedListener {
+ private static final String TAG = "CacheBarView";
+ private static final int FONT_COLOR = 0xffffffff;
+ private static final int MSG_REFRESH_STORAGE = 1;
+ private static final int PIN_SIZE = 36;
+
+ public interface Listener {
+ void onDoneClicked();
+ }
+
+ private GalleryActivity mActivity;
+ private Context mContext;
+
+ private StorageInfo mStorageInfo;
+ private long mUserChangeDelta;
+ private Future<StorageInfo> mStorageInfoFuture;
+ private Handler mHandler;
+
+ private int mTotalHeight;
+ private int mPinLeftMargin;
+ private int mPinRightMargin;
+ private int mButtonRightMargin;
+
+ private NinePatchTexture mBackground;
+ private GLView mLeftPin; // The pin icon.
+ private GLView mLeftLabel; // "Make available offline"
+ private ProgressBar mStorageBar;
+ private Label mStorageLabel; // "27.26 GB free"
+ private TextButton mDoneButton; // "Done"
+
+ private Listener mListener;
+
+ public CacheBarView(GalleryActivity activity, int resBackground, int height,
+ int pinLeftMargin, int pinRightMargin, int buttonRightMargin,
+ int fontSize) {
+ mActivity = activity;
+ mContext = activity.getAndroidContext();
+
+ mPinLeftMargin = pinLeftMargin;
+ mPinRightMargin = pinRightMargin;
+ mButtonRightMargin = buttonRightMargin;
+
+ mBackground = new NinePatchTexture(mContext, resBackground);
+ Rect paddings = mBackground.getPaddings();
+
+ // The total height of the strip that includes the bar containing Pin,
+ // Label, DoneButton, ..., ect. and the extended fading bar.
+ mTotalHeight = height + paddings.top;
+
+ mLeftPin = new Icon(mContext, R.drawable.ic_manage_pin, PIN_SIZE, PIN_SIZE);
+ mLeftLabel = new Label(mContext, R.string.make_available_offline,
+ fontSize, FONT_COLOR);
+ addComponent(mLeftPin);
+ addComponent(mLeftLabel);
+
+ mDoneButton = new TextButton(mContext, R.string.done);
+ mDoneButton.setOnClickListener(this);
+ NinePatchTexture normal = new NinePatchTexture(
+ mContext, R.drawable.btn_default_normal_holo_dark);
+ NinePatchTexture pressed = new NinePatchTexture(
+ mContext, R.drawable.btn_default_pressed_holo_dark);
+ mDoneButton.setNormalBackground(normal);
+ mDoneButton.setPressedBackground(pressed);
+ addComponent(mDoneButton);
+
+ // Initially the progress bar and label are invisible.
+ // It will be made visible after we have the storage information.
+ mStorageBar = new ProgressBar(mContext,
+ R.drawable.progress_primary_holo_dark,
+ R.drawable.progress_secondary_holo_dark,
+ R.drawable.progress_bg_holo_dark);
+ mStorageLabel = new Label(mContext, "", 14, Color.WHITE);
+ addComponent(mStorageBar);
+ addComponent(mStorageLabel);
+ mStorageBar.setVisibility(GLView.INVISIBLE);
+ mStorageLabel.setVisibility(GLView.INVISIBLE);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_REFRESH_STORAGE:
+ mStorageInfo = (StorageInfo) msg.obj;
+ refreshStorageInfo();
+ break;
+ }
+ }
+ };
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ // Called by mDoneButton
+ public void onClicked(GLView source) {
+ if (mListener != null) {
+ mListener.onDoneClicked();
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ // The size of mStorageLabel can change, so we need to layout
+ // even if the size of CacheBarView does not change.
+ int w = right - left;
+ int h = bottom - top;
+
+ mLeftPin.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int pinH = mLeftPin.getMeasuredHeight();
+ int pinW = mLeftPin.getMeasuredWidth();
+ int pinT = (h - pinH) / 2;
+ int pinL = mPinLeftMargin;
+ mLeftPin.layout(pinL, pinT, pinL + pinW, pinT + pinH);
+
+ mLeftLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int labelH = mLeftLabel.getMeasuredHeight();
+ int labelW = mLeftLabel.getMeasuredWidth();
+ int labelT = (h - labelH) / 2;
+ int labelL = pinL + pinW + mPinRightMargin;
+ mLeftLabel.layout(labelL, labelT, labelL + labelW, labelT + labelH);
+
+ mDoneButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int doneH = mDoneButton.getMeasuredHeight();
+ int doneW = mDoneButton.getMeasuredWidth();
+ int doneT = (h - doneH) / 2;
+ int doneR = w - mButtonRightMargin;
+ mDoneButton.layout(doneR - doneW, doneT, doneR, doneT + doneH);
+
+ int centerX = w / 2;
+ int centerY = h / 2;
+
+ int capBarH = 20;
+ int capBarW = 200;
+ int capBarT = centerY - capBarH / 2;
+ int capBarL = centerX - capBarW / 2;
+ mStorageBar.layout(capBarL, capBarT, capBarL + capBarW,
+ capBarT + capBarH);
+
+ mStorageLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int capLabelH = mStorageLabel.getMeasuredHeight();
+ int capLabelW = mStorageLabel.getMeasuredWidth();
+ int capLabelT = centerY - capLabelH / 2;
+ int capLabelL = centerX + capBarW / 2 + 8;
+ mStorageLabel.layout(capLabelL , capLabelT, capLabelL + capLabelW,
+ capLabelT + capLabelH);
+ }
+
+ public void refreshStorageInfo() {
+ long used = mStorageInfo.usedBytes;
+ long total = mStorageInfo.totalBytes;
+ long cached = mStorageInfo.usedCacheBytes;
+ long target = mStorageInfo.targetCacheBytes;
+
+ double primary = (double) used / total;
+ double secondary =
+ (double) (used - cached + target + mUserChangeDelta) / total;
+
+ mStorageBar.setProgress((int) (primary * 10000));
+ mStorageBar.setSecondaryProgress((int) (secondary * 10000));
+
+ long freeBytes = mStorageInfo.totalBytes - mStorageInfo.usedBytes;
+ String sizeString = Formatter.formatFileSize(mContext, freeBytes);
+ String label = mContext.getString(R.string.free_space_format, sizeString);
+ mStorageLabel.setText(label);
+ mStorageBar.setVisibility(GLView.VISIBLE);
+ mStorageLabel.setVisibility(GLView.VISIBLE);
+ requestLayout(); // because the size of the label may have changed.
+ }
+
+ public void increaseTargetCacheSize(long delta) {
+ mUserChangeDelta += delta;
+ refreshStorageInfo();
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ Rect paddings = mBackground.getPaddings();
+ mBackground.draw(canvas, 0, -paddings.top, getWidth(), mTotalHeight);
+ }
+
+ public void resume() {
+ mStorageInfoFuture = mActivity.getThreadPool().submit(
+ new StorageInfoJob(),
+ new FutureListener<StorageInfo>() {
+ public void onFutureDone(Future<StorageInfo> future) {
+ mStorageInfoFuture = null;
+ if (!future.isCancelled()) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_REFRESH_STORAGE, future.get()));
+ }
+ }
+ });
+ }
+
+ public void pause() {
+ if (mStorageInfoFuture != null) {
+ mStorageInfoFuture.cancel();
+ mStorageInfoFuture = null;
+ }
+ mStorageBar.setVisibility(GLView.INVISIBLE);
+ mStorageLabel.setVisibility(GLView.INVISIBLE);
+ }
+
+ public static class StorageInfo {
+ long totalBytes; // number of bytes the storage has.
+ long usedBytes; // number of bytes already used.
+ long usedCacheBytes; // number of bytes used for the cache (should be less
+ // then usedBytes).
+ long targetCacheBytes;// number of bytes used for the cache
+ // if all pending downloads (and removals) are completed.
+ }
+
+ private class StorageInfoJob implements Job<StorageInfo> {
+ public StorageInfo run(JobContext jc) {
+ File cacheDir = mContext.getExternalCacheDir();
+ if (cacheDir == null) {
+ cacheDir = mContext.getCacheDir();
+ }
+ String path = cacheDir.getAbsolutePath();
+ StatFs stat = new StatFs(path);
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks();
+ long totalBlocks = stat.getBlockCount();
+ StorageInfo si = new StorageInfo();
+ si.totalBytes = blockSize * totalBlocks;
+ si.usedBytes = blockSize * (totalBlocks - availableBlocks);
+ si.usedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize();
+ si.targetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize();
+ return si;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CanvasTexture.java b/src/com/android/gallery3d/ui/CanvasTexture.java
new file mode 100644
index 000000000..679a4bcdc
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CanvasTexture.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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Bitmap.Config;
+
+// CanvasTexture is a texture whose content is the drawing on a Canvas.
+// The subclasses should override onDraw() to draw on the bitmap.
+// By default CanvasTexture is not opaque.
+abstract class CanvasTexture extends UploadedTexture {
+ protected Canvas mCanvas;
+ private final Config mConfig;
+
+ public CanvasTexture(int width, int height) {
+ mConfig = Config.ARGB_8888;
+ setSize(width, height);
+ setOpaque(false);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);
+ mCanvas = new Canvas(bitmap);
+ onDraw(mCanvas, bitmap);
+ return bitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ if (!inFinalizer()) {
+ bitmap.recycle();
+ }
+ }
+
+ abstract protected void onDraw(Canvas canvas, Bitmap backing);
+}
diff --git a/src/com/android/gallery3d/ui/ColorTexture.java b/src/com/android/gallery3d/ui/ColorTexture.java
new file mode 100644
index 000000000..24e8914b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ColorTexture.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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+// ColorTexture is a texture which fills the rectangle with the specified color.
+public class ColorTexture implements Texture {
+
+ private final int mColor;
+ private int mWidth;
+ private int mHeight;
+
+ public ColorTexture(int color) {
+ mColor = color;
+ mWidth = 1;
+ mHeight = 1;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ canvas.fillRect(x, y, w, h, mColor);
+ }
+
+ public boolean isOpaque() {
+ return Utils.isOpaque(mColor);
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Config.java b/src/com/android/gallery3d/ui/Config.java
new file mode 100644
index 000000000..5c5b6210a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Config.java
@@ -0,0 +1,31 @@
+/*
+ * 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.ui;
+
+interface DetailsWindowConfig {
+ public static final int FONT_SIZE = 18;
+ public static final int PREFERRED_WIDTH = 400;
+ public static final int LEFT_RIGHT_EXTRA_PADDING = 9;
+ public static final int TOP_BOTTOM_EXTRA_PADDING = 9;
+ public static final int LINE_SPACING = 5;
+ public static final int FIRST_LINE_SPACING = 18;
+}
+
+interface TextButtonConfig {
+ public static final int HORIZONTAL_PADDINGS = 16;
+ public static final int VERTICAL_PADDINGS = 5;
+}
diff --git a/src/com/android/gallery3d/ui/CropView.java b/src/com/android/gallery3d/ui/CropView.java
new file mode 100644
index 000000000..9c59c9a84
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CropView.java
@@ -0,0 +1,801 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.media.FaceDetector;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import javax.microedition.khronos.opengles.GL11;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropView extends GLView {
+ private static final String TAG = "CropView";
+
+ private static final int FACE_PIXEL_COUNT = 120000; // around 400x300
+
+ private static final int COLOR_OUTLINE = 0xFF008AFF;
+ private static final int COLOR_FACE_OUTLINE = 0xFF000000;
+
+ private static final float OUTLINE_WIDTH = 3f;
+
+ private static final int SIZE_UNKNOWN = -1;
+ private static final int TOUCH_TOLERANCE = 30;
+
+ private static final float MIN_SELECTION_LENGTH = 16f;
+ public static final float UNSPECIFIED = -1f;
+
+ private static final int MAX_FACE_COUNT = 3;
+ private static final float FACE_EYE_RATIO = 2f;
+
+ private static final int ANIMATION_DURATION = 1250;
+
+ private static final int MOVE_LEFT = 1;
+ private static final int MOVE_TOP = 2;
+ private static final int MOVE_RIGHT = 4;
+ private static final int MOVE_BOTTOM = 8;
+ private static final int MOVE_BLOCK = 16;
+
+ private static final float MAX_SELECTION_RATIO = 0.8f;
+ private static final float MIN_SELECTION_RATIO = 0.4f;
+ private static final float SELECTION_RATIO = 0.60f;
+ private static final int ANIMATION_TRIGGER = 64;
+
+ private static final int MSG_UPDATE_FACES = 1;
+
+ private float mAspectRatio = UNSPECIFIED;
+ private float mSpotlightRatioX = 0;
+ private float mSpotlightRatioY = 0;
+
+ private Handler mMainHandler;
+
+ private FaceHighlightView mFaceDetectionView;
+ private HighlightRectangle mHighlightRectangle;
+ private TileImageView mImageView;
+ private AnimationController mAnimation = new AnimationController();
+
+ private int mImageWidth = SIZE_UNKNOWN;
+ private int mImageHeight = SIZE_UNKNOWN;
+
+ private GalleryActivity mActivity;
+
+ private GLPaint mPaint = new GLPaint();
+ private GLPaint mFacePaint = new GLPaint();
+
+ private int mImageRotation;
+
+ public CropView(GalleryActivity activity) {
+ mActivity = activity;
+ mImageView = new TileImageView(activity);
+ mFaceDetectionView = new FaceHighlightView();
+ mHighlightRectangle = new HighlightRectangle();
+
+ addComponent(mImageView);
+ addComponent(mFaceDetectionView);
+ addComponent(mHighlightRectangle);
+
+ mHighlightRectangle.setVisibility(GLView.INVISIBLE);
+
+ mPaint.setColor(COLOR_OUTLINE);
+ mPaint.setLineWidth(OUTLINE_WIDTH);
+
+ mFacePaint.setColor(COLOR_FACE_OUTLINE);
+ mFacePaint.setLineWidth(OUTLINE_WIDTH);
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_FACES);
+ ((DetectFaceTask) message.obj).updateFaces();
+ }
+ };
+ }
+
+ public void setAspectRatio(float ratio) {
+ mAspectRatio = ratio;
+ }
+
+ public void setSpotlightRatio(float ratioX, float ratioY) {
+ mSpotlightRatioX = ratioX;
+ mSpotlightRatioY = ratioY;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ mFaceDetectionView.layout(0, 0, width, height);
+ mHighlightRectangle.layout(0, 0, width, height);
+ mImageView.layout(0, 0, width, height);
+ if (mImageHeight != SIZE_UNKNOWN) {
+ mAnimation.initialize();
+ if (mHighlightRectangle.getVisibility() == GLView.VISIBLE) {
+ mAnimation.parkNow(
+ mHighlightRectangle.mHighlightRect);
+ }
+ }
+ }
+
+ private boolean setImageViewPosition(int centerX, int centerY, float scale) {
+ int inverseX = mImageWidth - centerX;
+ int inverseY = mImageHeight - centerY;
+ TileImageView t = mImageView;
+ int rotation = mImageRotation;
+ switch (rotation) {
+ case 0: return t.setPosition(centerX, centerY, scale, 0);
+ case 90: return t.setPosition(centerY, inverseX, scale, 90);
+ case 180: return t.setPosition(inverseX, inverseY, scale, 180);
+ case 270: return t.setPosition(inverseY, centerX, scale, 270);
+ default: throw new IllegalArgumentException(String.valueOf(rotation));
+ }
+ }
+
+ @Override
+ public void render(GLCanvas canvas) {
+ AnimationController a = mAnimation;
+ if (a.calculate(canvas.currentAnimationTimeMillis())) invalidate();
+ setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale());
+ super.render(canvas);
+ }
+
+ @Override
+ public void renderBackground(GLCanvas canvas) {
+ canvas.clearBuffer();
+ }
+
+ public RectF getCropRectangle() {
+ if (mHighlightRectangle.getVisibility() == GLView.INVISIBLE) return null;
+ RectF rect = mHighlightRectangle.mHighlightRect;
+ RectF result = new RectF(rect.left * mImageWidth, rect.top * mImageHeight,
+ rect.right * mImageWidth, rect.bottom * mImageHeight);
+ return result;
+ }
+
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ private class FaceHighlightView extends GLView {
+ private static final int INDEX_NONE = -1;
+ private ArrayList<RectF> mFaces = new ArrayList<RectF>();
+ private RectF mRect = new RectF();
+ private int mPressedFaceIndex = INDEX_NONE;
+
+ public void addFace(RectF faceRect) {
+ mFaces.add(faceRect);
+ invalidate();
+ }
+
+ private void renderFace(GLCanvas canvas, RectF face, boolean pressed) {
+ GL11 gl = canvas.getGLInstance();
+ if (pressed) {
+ gl.glEnable(GL11.GL_STENCIL_TEST);
+ gl.glClear(GL11.GL_STENCIL_BUFFER_BIT);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+ gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
+ }
+
+ RectF r = mAnimation.mapRect(face, mRect);
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mFacePaint);
+
+ if (pressed) {
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
+ }
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ ArrayList<RectF> faces = mFaces;
+ for (int i = 0, n = faces.size(); i < n; ++i) {
+ renderFace(canvas, faces.get(i), i == mPressedFaceIndex);
+ }
+
+ GL11 gl = canvas.getGLInstance();
+ if (mPressedFaceIndex != INDEX_NONE) {
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0x66000000);
+ gl.glDisable(GL11.GL_STENCIL_TEST);
+ }
+ }
+
+ private void setPressedFace(int index) {
+ if (mPressedFaceIndex == index) return;
+ mPressedFaceIndex = index;
+ invalidate();
+ }
+
+ private int getFaceIndexByPosition(float x, float y) {
+ ArrayList<RectF> faces = mFaces;
+ for (int i = 0, n = faces.size(); i < n; ++i) {
+ RectF r = mAnimation.mapRect(faces.get(i), mRect);
+ if (r.contains(x, y)) return i;
+ }
+ return INDEX_NONE;
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ setPressedFace(getFaceIndexByPosition(x, y));
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ int index = mPressedFaceIndex;
+ setPressedFace(INDEX_NONE);
+ if (index != INDEX_NONE) {
+ mHighlightRectangle.setRectangle(mFaces.get(index));
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ setVisibility(GLView.INVISIBLE);
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ private class AnimationController extends Animation {
+ private int mCurrentX;
+ private int mCurrentY;
+ private float mCurrentScale;
+ private int mStartX;
+ private int mStartY;
+ private float mStartScale;
+ private int mTargetX;
+ private int mTargetY;
+ private float mTargetScale;
+
+ public AnimationController() {
+ setDuration(ANIMATION_DURATION);
+ setInterpolator(new DecelerateInterpolator(4));
+ }
+
+ public void initialize() {
+ mCurrentX = mImageWidth / 2;
+ mCurrentY = mImageHeight / 2;
+ mCurrentScale = Math.min(2, Math.min(
+ (float) getWidth() / mImageWidth,
+ (float) getHeight() / mImageHeight));
+ }
+
+ public void startParkingAnimation(RectF highlight) {
+ RectF r = mAnimation.mapRect(highlight, new RectF());
+ int width = getWidth();
+ int height = getHeight();
+
+ float wr = r.width() / width;
+ float hr = r.height() / height;
+ final int d = ANIMATION_TRIGGER;
+ if (wr >= MIN_SELECTION_RATIO && wr < MAX_SELECTION_RATIO
+ && hr >= MIN_SELECTION_RATIO && hr < MAX_SELECTION_RATIO
+ && r.left >= d && r.right < width - d
+ && r.top >= d && r.bottom < height - d) return;
+
+ mStartX = mCurrentX;
+ mStartY = mCurrentY;
+ mStartScale = mCurrentScale;
+ calculateTarget(highlight);
+ start();
+ }
+
+ public void parkNow(RectF highlight) {
+ calculateTarget(highlight);
+ forceStop();
+ mStartX = mCurrentX = mTargetX;
+ mStartY = mCurrentY = mTargetY;
+ mStartScale = mCurrentScale = mTargetScale;
+ }
+
+ public void inverseMapPoint(PointF point) {
+ float s = mCurrentScale;
+ point.x = Utils.clamp(((point.x - getWidth() * 0.5f) / s
+ + mCurrentX) / mImageWidth, 0, 1);
+ point.y = Utils.clamp(((point.y - getHeight() * 0.5f) / s
+ + mCurrentY) / mImageHeight, 0, 1);
+ }
+
+ public RectF mapRect(RectF input, RectF output) {
+ float offsetX = getWidth() * 0.5f;
+ float offsetY = getHeight() * 0.5f;
+ int x = mCurrentX;
+ int y = mCurrentY;
+ float s = mCurrentScale;
+ output.set(
+ offsetX + (input.left * mImageWidth - x) * s,
+ offsetY + (input.top * mImageHeight - y) * s,
+ offsetX + (input.right * mImageWidth - x) * s,
+ offsetY + (input.bottom * mImageHeight - y) * s);
+ return output;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrentX = Math.round(mStartX + (mTargetX - mStartX) * progress);
+ mCurrentY = Math.round(mStartY + (mTargetY - mStartY) * progress);
+ mCurrentScale = mStartScale + (mTargetScale - mStartScale) * progress;
+
+ if (mCurrentX == mTargetX && mCurrentY == mTargetY
+ && mCurrentScale == mTargetScale) forceStop();
+ }
+
+ public int getCenterX() {
+ return mCurrentX;
+ }
+
+ public int getCenterY() {
+ return mCurrentY;
+ }
+
+ public float getScale() {
+ return mCurrentScale;
+ }
+
+ private void calculateTarget(RectF highlight) {
+ float width = getWidth();
+ float height = getHeight();
+
+ if (mImageWidth != SIZE_UNKNOWN) {
+ float minScale = Math.min(width / mImageWidth, height / mImageHeight);
+ float scale = Utils.clamp(SELECTION_RATIO * Math.min(
+ width / (highlight.width() * mImageWidth),
+ height / (highlight.height() * mImageHeight)), minScale, 2f);
+ int centerX = Math.round(
+ mImageWidth * (highlight.left + highlight.right) * 0.5f);
+ int centerY = Math.round(
+ mImageHeight * (highlight.top + highlight.bottom) * 0.5f);
+
+ if (Math.round(mImageWidth * scale) > width) {
+ int limitX = Math.round(width * 0.5f / scale);
+ centerX = Math.round(
+ (highlight.left + highlight.right) * mImageWidth / 2);
+ centerX = Utils.clamp(centerX, limitX, mImageWidth - limitX);
+ } else {
+ centerX = mImageWidth / 2;
+ }
+ if (Math.round(mImageHeight * scale) > height) {
+ int limitY = Math.round(height * 0.5f / scale);
+ centerY = Math.round(
+ (highlight.top + highlight.bottom) * mImageHeight / 2);
+ centerY = Utils.clamp(centerY, limitY, mImageHeight - limitY);
+ } else {
+ centerY = mImageHeight / 2;
+ }
+ mTargetX = centerX;
+ mTargetY = centerY;
+ mTargetScale = scale;
+ }
+ }
+
+ }
+
+ private class HighlightRectangle extends GLView {
+ private RectF mHighlightRect = new RectF(0.25f, 0.25f, 0.75f, 0.75f);
+ private RectF mTempRect = new RectF();
+ private PointF mTempPoint = new PointF();
+
+ private ResourceTexture mArrowX;
+ private ResourceTexture mArrowY;
+
+ private int mMovingEdges = 0;
+ private float mReferenceX;
+ private float mReferenceY;
+
+ public HighlightRectangle() {
+ mArrowX = new ResourceTexture(mActivity.getAndroidContext(),
+ R.drawable.camera_crop_width_holo);
+ mArrowY = new ResourceTexture(mActivity.getAndroidContext(),
+ R.drawable.camera_crop_height_holo);
+ }
+
+ public void setInitRectangle() {
+ float targetRatio = mAspectRatio == UNSPECIFIED
+ ? 1f
+ : mAspectRatio * mImageHeight / mImageWidth;
+ float w = SELECTION_RATIO / 2f;
+ float h = SELECTION_RATIO / 2f;
+ if (targetRatio > 1) {
+ h = w / targetRatio;
+ } else {
+ w = h * targetRatio;
+ }
+ mHighlightRect.set(0.5f - w, 0.5f - h, 0.5f + w, 0.5f + h);
+ }
+
+ public void setRectangle(RectF faceRect) {
+ mHighlightRect.set(faceRect);
+ mAnimation.startParkingAnimation(faceRect);
+ invalidate();
+ }
+
+ private void moveEdges(MotionEvent event) {
+ float scale = mAnimation.getScale();
+ float dx = (event.getX() - mReferenceX) / scale / mImageWidth;
+ float dy = (event.getY() - mReferenceY) / scale / mImageHeight;
+ mReferenceX = event.getX();
+ mReferenceY = event.getY();
+ RectF r = mHighlightRect;
+
+ if ((mMovingEdges & MOVE_BLOCK) != 0) {
+ dx = Utils.clamp(dx, -r.left, 1 - r.right);
+ dy = Utils.clamp(dy, -r.top , 1 - r.bottom);
+ r.top += dy;
+ r.bottom += dy;
+ r.left += dx;
+ r.right += dx;
+ } else {
+ PointF point = mTempPoint;
+ point.set(mReferenceX, mReferenceY);
+ mAnimation.inverseMapPoint(point);
+ float left = r.left + MIN_SELECTION_LENGTH / mImageWidth;
+ float right = r.right - MIN_SELECTION_LENGTH / mImageWidth;
+ float top = r.top + MIN_SELECTION_LENGTH / mImageHeight;
+ float bottom = r.bottom - MIN_SELECTION_LENGTH / mImageHeight;
+ if ((mMovingEdges & MOVE_RIGHT) != 0) {
+ r.right = Utils.clamp(point.x, left, 1f);
+ }
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(point.x, 0, right);
+ }
+ if ((mMovingEdges & MOVE_TOP) != 0) {
+ r.top = Utils.clamp(point.y, 0, bottom);
+ }
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(point.y, top, 1f);
+ }
+ if (mAspectRatio != UNSPECIFIED) {
+ float targetRatio = mAspectRatio * mImageHeight / mImageWidth;
+ if (r.width() / r.height() > targetRatio) {
+ float height = r.width() / targetRatio;
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(r.top + height, top, 1f);
+ } else {
+ r.top = Utils.clamp(r.bottom - height, 0, bottom);
+ }
+ } else {
+ float width = r.height() * targetRatio;
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(r.right - width, 0, right);
+ } else {
+ r.right = Utils.clamp(r.left + width, left, 1f);
+ }
+ }
+ if (r.width() / r.height() > targetRatio) {
+ float width = r.height() * targetRatio;
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(r.right - width, 0, right);
+ } else {
+ r.right = Utils.clamp(r.left + width, left, 1f);
+ }
+ } else {
+ float height = r.width() / targetRatio;
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(r.top + height, top, 1f);
+ } else {
+ r.top = Utils.clamp(r.bottom - height, 0, bottom);
+ }
+ }
+ }
+ }
+ invalidate();
+ }
+
+ private void setMovingEdges(MotionEvent event) {
+ RectF r = mAnimation.mapRect(mHighlightRect, mTempRect);
+ float x = event.getX();
+ float y = event.getY();
+
+ if (x > r.left + TOUCH_TOLERANCE && x < r.right - TOUCH_TOLERANCE
+ && y > r.top + TOUCH_TOLERANCE && y < r.bottom - TOUCH_TOLERANCE) {
+ mMovingEdges = MOVE_BLOCK;
+ return;
+ }
+
+ boolean inVerticalRange = (r.top - TOUCH_TOLERANCE) <= y
+ && y <= (r.bottom + TOUCH_TOLERANCE);
+ boolean inHorizontalRange = (r.left - TOUCH_TOLERANCE) <= x
+ && x <= (r.right + TOUCH_TOLERANCE);
+
+ if (inVerticalRange) {
+ boolean left = Math.abs(x - r.left) <= TOUCH_TOLERANCE;
+ boolean right = Math.abs(x - r.right) <= TOUCH_TOLERANCE;
+ if (left && right) {
+ left = Math.abs(x - r.left) < Math.abs(x - r.right);
+ right = !left;
+ }
+ if (left) mMovingEdges |= MOVE_LEFT;
+ if (right) mMovingEdges |= MOVE_RIGHT;
+ if (mAspectRatio != UNSPECIFIED && inHorizontalRange) {
+ mMovingEdges |= (y >
+ (r.top + r.bottom) / 2) ? MOVE_BOTTOM : MOVE_TOP;
+ }
+ }
+ if (inHorizontalRange) {
+ boolean top = Math.abs(y - r.top) <= TOUCH_TOLERANCE;
+ boolean bottom = Math.abs(y - r.bottom) <= TOUCH_TOLERANCE;
+ if (top && bottom) {
+ top = Math.abs(y - r.top) < Math.abs(y - r.bottom);
+ bottom = !top;
+ }
+ if (top) mMovingEdges |= MOVE_TOP;
+ if (bottom) mMovingEdges |= MOVE_BOTTOM;
+ if (mAspectRatio != UNSPECIFIED && inVerticalRange) {
+ mMovingEdges |= (x >
+ (r.left + r.right) / 2) ? MOVE_RIGHT : MOVE_LEFT;
+ }
+ }
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mReferenceX = event.getX();
+ mReferenceY = event.getY();
+ setMovingEdges(event);
+ invalidate();
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE:
+ moveEdges(event);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mMovingEdges = 0;
+ mAnimation.startParkingAnimation(mHighlightRect);
+ invalidate();
+ return true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ RectF r = mAnimation.mapRect(mHighlightRect, mTempRect);
+ drawHighlightRectangle(canvas, r);
+
+ float centerY = (r.top + r.bottom) / 2;
+ float centerX = (r.left + r.right) / 2;
+ if ((mMovingEdges & (MOVE_RIGHT | MOVE_BLOCK)) != 0) {
+ mArrowX.draw(canvas,
+ Math.round(r.right - mArrowX.getWidth() / 2),
+ Math.round(centerY - mArrowX.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_LEFT | MOVE_BLOCK)) != 0) {
+ mArrowX.draw(canvas,
+ Math.round(r.left - mArrowX.getWidth() / 2),
+ Math.round(centerY - mArrowX.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_TOP | MOVE_BLOCK)) != 0) {
+ mArrowY.draw(canvas,
+ Math.round(centerX - mArrowY.getWidth() / 2),
+ Math.round(r.top - mArrowY.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_BOTTOM | MOVE_BLOCK)) != 0) {
+ mArrowY.draw(canvas,
+ Math.round(centerX - mArrowY.getWidth() / 2),
+ Math.round(r.bottom - mArrowY.getHeight() / 2));
+ }
+ }
+
+ private void drawHighlightRectangle(GLCanvas canvas, RectF r) {
+ GL11 gl = canvas.getGLInstance();
+ gl.glLineWidth(3.0f);
+ gl.glEnable(GL11.GL_LINE_SMOOTH);
+
+ gl.glEnable(GL11.GL_STENCIL_TEST);
+ gl.glClear(GL11.GL_STENCIL_BUFFER_BIT);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+ gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
+
+ if (mSpotlightRatioX == 0 || mSpotlightRatioY == 0) {
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint);
+ } else {
+ float sx = r.width() * mSpotlightRatioX;
+ float sy = r.height() * mSpotlightRatioY;
+ float cx = r.centerX();
+ float cy = r.centerY();
+
+ canvas.fillRect(cx - sx / 2, cy - sy / 2, sx, sy, Color.TRANSPARENT);
+ canvas.drawRect(cx - sx / 2, cy - sy / 2, sx, sy, mPaint);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint);
+
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+
+ canvas.drawRect(cx - sy / 2, cy - sx / 2, sy, sx, mPaint);
+ canvas.fillRect(cx - sy / 2, cy - sx / 2, sy, sx, Color.TRANSPARENT);
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), 0x80000000);
+ }
+
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
+
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0xA0000000);
+
+ gl.glDisable(GL11.GL_STENCIL_TEST);
+ }
+ }
+
+ private class DetectFaceTask extends Thread {
+ private final FaceDetector.Face[] mFaces = new FaceDetector.Face[MAX_FACE_COUNT];
+ private final Bitmap mFaceBitmap;
+ private int mFaceCount;
+
+ public DetectFaceTask(Bitmap bitmap) {
+ mFaceBitmap = bitmap;
+ setName("face-detect");
+ }
+
+ @Override
+ public void run() {
+ Bitmap bitmap = mFaceBitmap;
+ FaceDetector detector = new FaceDetector(
+ bitmap.getWidth(), bitmap.getHeight(), MAX_FACE_COUNT);
+ mFaceCount = detector.findFaces(bitmap, mFaces);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_UPDATE_FACES, this));
+ }
+
+ private RectF getFaceRect(FaceDetector.Face face) {
+ PointF point = new PointF();
+ face.getMidPoint(point);
+
+ int width = mFaceBitmap.getWidth();
+ int height = mFaceBitmap.getHeight();
+ float rx = face.eyesDistance() * FACE_EYE_RATIO;
+ float ry = rx;
+ float aspect = mAspectRatio;
+ if (aspect != UNSPECIFIED) {
+ if (aspect > 1) {
+ rx = ry * aspect;
+ } else {
+ ry = rx / aspect;
+ }
+ }
+
+ RectF r = new RectF(
+ point.x - rx, point.y - ry, point.x + rx, point.y + ry);
+ r.intersect(0, 0, width, height);
+
+ if (aspect != UNSPECIFIED) {
+ if (r.width() / r.height() > aspect) {
+ float w = r.height() * aspect;
+ r.left = (r.left + r.right - w) * 0.5f;
+ r.right = r.left + w;
+ } else {
+ float h = r.width() / aspect;
+ r.top = (r.top + r.bottom - h) * 0.5f;
+ r.bottom = r.top + h;
+ }
+ }
+
+ r.left /= width;
+ r.right /= width;
+ r.top /= height;
+ r.bottom /= height;
+ return r;
+ }
+
+ public void updateFaces() {
+ if (mFaceCount > 1) {
+ for (int i = 0, n = mFaceCount; i < n; ++i) {
+ mFaceDetectionView.addFace(getFaceRect(mFaces[i]));
+ }
+ mFaceDetectionView.setVisibility(GLView.VISIBLE);
+ Toast.makeText(mActivity.getAndroidContext(),
+ R.string.multiface_crop_help, Toast.LENGTH_SHORT).show();
+ } else if (mFaceCount == 1) {
+ mFaceDetectionView.setVisibility(GLView.INVISIBLE);
+ mHighlightRectangle.setRectangle(getFaceRect(mFaces[0]));
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ } else /*mFaceCount == 0*/ {
+ mHighlightRectangle.setInitRectangle();
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ }
+ }
+ }
+
+ public void setDataModel(TileImageView.Model dataModel, int rotation) {
+ if (((rotation / 90) & 0x01) != 0) {
+ mImageWidth = dataModel.getImageHeight();
+ mImageHeight = dataModel.getImageWidth();
+ } else {
+ mImageWidth = dataModel.getImageWidth();
+ mImageHeight = dataModel.getImageHeight();
+ }
+
+ mImageRotation = rotation;
+
+ mImageView.setModel(dataModel);
+ mAnimation.initialize();
+ }
+
+ public void detectFaces(Bitmap bitmap) {
+ int rotation = mImageRotation;
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ float scale = (float) Math.sqrt(
+ (double) FACE_PIXEL_COUNT / (width * height));
+
+ // faceBitmap is a correctly rotated bitmap, as viewed by a user.
+ Bitmap faceBitmap;
+ if (((rotation / 90) & 1) == 0) {
+ int w = (Math.round(width * scale) & ~1); // must be even
+ int h = Math.round(height * scale);
+ faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
+ Canvas canvas = new Canvas(faceBitmap);
+ canvas.rotate(rotation, w / 2, h / 2);
+ canvas.scale((float) w / width, (float) h / height);
+ canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
+ } else {
+ int w = (Math.round(height * scale) & ~1); // must be even
+ int h = Math.round(width * scale);
+ faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
+ Canvas canvas = new Canvas(faceBitmap);
+ canvas.translate(w / 2, h / 2);
+ canvas.rotate(rotation);
+ canvas.translate(-h / 2, -w / 2);
+ canvas.scale((float) w / height, (float) h / width);
+ canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
+ }
+ new DetectFaceTask(faceBitmap).start();
+ }
+
+ public void initializeHighlightRectangle() {
+ mHighlightRectangle.setInitRectangle();
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ }
+
+ public void resume() {
+ mImageView.prepareTextures();
+ }
+
+ public void pause() {
+ mImageView.freeTextures();
+ }
+}
+
diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java
new file mode 100644
index 000000000..de2367e60
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CustomMenu.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+
+import java.util.ArrayList;
+
+public class CustomMenu implements OnMenuItemClickListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterMenu";
+
+ public static class DropDownMenu {
+ private Button mButton;
+ private PopupMenu mPopupMenu;
+ private Menu mMenu;
+
+ public DropDownMenu(Context context, Button button, int menuId,
+ OnMenuItemClickListener listener) {
+ mButton = button;
+ mButton.setBackgroundDrawable(context.getResources().getDrawable(
+ R.drawable.dropdown_normal_holo_dark));
+ mPopupMenu = new PopupMenu(context, mButton);
+ mMenu = mPopupMenu.getMenu();
+ mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
+ mPopupMenu.setOnMenuItemClickListener(listener);
+ mButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mPopupMenu.show();
+ }
+ });
+ }
+
+ public MenuItem findItem(int id) {
+ return mMenu.findItem(id);
+ }
+
+ public void setTitle(CharSequence title) {
+ mButton.setText(title);
+ }
+ }
+
+
+
+ private Context mContext;
+ private ArrayList<DropDownMenu> mMenus;
+ private OnMenuItemClickListener mListener;
+
+ public CustomMenu(Context context) {
+ mContext = context;
+ mMenus = new ArrayList<DropDownMenu>();
+ }
+
+ public DropDownMenu addDropDownMenu(Button button, int menuId) {
+ DropDownMenu menu = new DropDownMenu(mContext, button, menuId, this);
+ mMenus.add(menu);
+ return menu;
+ }
+
+ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+ mListener = listener;
+ }
+
+ public MenuItem findMenuItem(int id) {
+ MenuItem item = null;
+ for (DropDownMenu menu : mMenus) {
+ item = menu.findItem(id);
+ if (item != null) return item;
+ }
+ return item;
+ }
+
+ public void setMenuItemAppliedEnabled(int id, boolean applied, boolean enabled,
+ boolean updateTitle) {
+ MenuItem item = null;
+ for (DropDownMenu menu : mMenus) {
+ item = menu.findItem(id);
+ if (item != null) {
+ item.setCheckable(true);
+ item.setChecked(applied);
+ item.setEnabled(enabled);
+ if (updateTitle) {
+ menu.setTitle(item.getTitle());
+ }
+ }
+ }
+ }
+
+ public void setMenuItemVisibility(int id, boolean visibility) {
+ MenuItem item = findMenuItem(id);
+ if (item != null) {
+ item.setVisible(visibility);
+ }
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mListener != null) {
+ return mListener.onMenuItemClick(item);
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsWindow.java b/src/com/android/gallery3d/ui/DetailsWindow.java
new file mode 100644
index 000000000..03e216922
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsWindow.java
@@ -0,0 +1,451 @@
+/*
+ * 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.ui;
+
+import static com.android.gallery3d.ui.DetailsWindowConfig.FONT_SIZE;
+import static com.android.gallery3d.ui.DetailsWindowConfig.LEFT_RIGHT_EXTRA_PADDING;
+import static com.android.gallery3d.ui.DetailsWindowConfig.LINE_SPACING;
+import static com.android.gallery3d.ui.DetailsWindowConfig.PREFERRED_WIDTH;
+import static com.android.gallery3d.ui.DetailsWindowConfig.TOP_BOTTOM_EXTRA_PADDING;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ReverseGeocoder;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.location.Address;
+import android.os.Handler;
+import android.os.Message;
+import android.text.format.Formatter;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+// TODO: Add scroll bar to this window.
+public class DetailsWindow extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "DetailsWindow";
+ private static final int MSG_REFRESH_LOCATION = 1;
+ private static final int FONT_COLOR = Color.WHITE;
+ private static final int CLOSE_BUTTON_SIZE = 32;
+
+ private GalleryActivity mContext;
+ protected Texture mBackground;
+ private StringTexture mTitle;
+ private MyDataModel mModel;
+ private MediaDetails mDetails;
+ private DetailsSource mSource;
+ private int mIndex;
+ private int mLocationIndex;
+ private Future<Address> mAddressLookupJob;
+ private Handler mHandler;
+ private Icon mCloseButton;
+ private int mMaxDetailLength;
+ private CloseListener mListener;
+
+ private ScrollView mScrollView;
+ private DetailsPanel mDetailPanel = new DetailsPanel();
+
+ public interface DetailsSource {
+ public int size();
+ public int findIndex(int indexHint);
+ public MediaDetails getDetails();
+ }
+
+ public interface CloseListener {
+ public void onClose();
+ }
+
+ public DetailsWindow(GalleryActivity activity, DetailsSource source) {
+ mContext = activity;
+ mSource = source;
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_REFRESH_LOCATION:
+ mModel.updateLocation((Address) msg.obj);
+ invalidate();
+ break;
+ }
+ }
+ };
+ Context context = activity.getAndroidContext();
+ ResourceTexture icon = new ResourceTexture(context, R.drawable.ic_menu_cancel_holo_light);
+ setBackground(new NinePatchTexture(context, R.drawable.popup_full_dark));
+
+ mCloseButton = new Icon(context, icon, CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE) {
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_UP:
+ if (mListener != null) mListener.onClose();
+ }
+ return true;
+ }
+ };
+ mScrollView = new ScrollView(context);
+ mScrollView.addComponent(mDetailPanel);
+
+ super.addComponent(mScrollView);
+ super.addComponent(mCloseButton);
+
+ reloadDetails(0);
+ }
+
+ public void setCloseListener(CloseListener listener) {
+ mListener = listener;
+ }
+
+ public void setBackground(Texture background) {
+ if (background == mBackground) return;
+ mBackground = background;
+ if (background != null && background instanceof NinePatchTexture) {
+ Rect p = ((NinePatchTexture) mBackground).getPaddings();
+ p.left += LEFT_RIGHT_EXTRA_PADDING;
+ p.right += LEFT_RIGHT_EXTRA_PADDING;
+ p.top += TOP_BOTTOM_EXTRA_PADDING;
+ p.bottom += TOP_BOTTOM_EXTRA_PADDING;
+ setPaddings(p);
+ } else {
+ setPaddings(0, 0, 0, 0);
+ }
+ Rect p = getPaddings();
+ mMaxDetailLength = PREFERRED_WIDTH - p.left - p.right;
+ invalidate();
+ }
+
+ public void setTitle(String title) {
+ mTitle = StringTexture.newInstance(title, FONT_SIZE, FONT_COLOR);
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ if (mBackground == null) return;
+ int width = getWidth();
+ int height = getHeight();
+
+ //TODO: change alpha in the background image.
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.setAlpha(0.7f);
+ mBackground.draw(canvas, 0, 0, width, height);
+ canvas.restore();
+
+ Rect p = getPaddings();
+ if (mTitle != null) mTitle.draw(canvas, p.left, p.top);
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int height = MeasureSpec.getSize(heightSpec);
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean sizeChange, int l, int t, int r, int b) {
+ mCloseButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int bWidth = mCloseButton.getMeasuredWidth();
+ int bHeight = mCloseButton.getMeasuredHeight();
+ int width = getWidth();
+ int height = getHeight();
+
+ Rect p = getPaddings();
+ mCloseButton.layout(width - p.right - bWidth, p.top,
+ width - p.right, p.top + bHeight);
+ mScrollView.layout(p.left, p.top + bHeight, width - p.right,
+ height - p.bottom);
+ }
+
+ public void show() {
+ setVisibility(GLView.VISIBLE);
+ requestLayout();
+ }
+
+ public void hide() {
+ setVisibility(GLView.INVISIBLE);
+ requestLayout();
+ }
+
+ public void pause() {
+ Future<Address> lookupJob = mAddressLookupJob;
+ if (lookupJob != null) {
+ lookupJob.cancel();
+ lookupJob.waitDone();
+ }
+ }
+
+ public void reloadDetails(int indexHint) {
+ int index = mSource.findIndex(indexHint);
+ if (index == -1) return;
+ MediaDetails details = mSource.getDetails();
+ if (details != null) {
+ if (mIndex == index && mDetails == details) return;
+ mIndex = index;
+ mDetails = details;
+ setDetails(details);
+ }
+ mDetailPanel.requestLayout();
+ }
+
+ private void setDetails(MediaDetails details) {
+ mModel = new MyDataModel(details);
+ invalidate();
+ }
+
+ private class AddressLookupJob implements Job<Address> {
+ double[] mLatlng;
+ protected AddressLookupJob(double[] latlng) {
+ mLatlng = latlng;
+ }
+
+ public Address run(JobContext jc) {
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext());
+ return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true);
+ }
+ }
+
+ private class MyDataModel {
+ ArrayList<Texture> mItems;
+
+ public MyDataModel(MediaDetails details) {
+ Context context = mContext.getAndroidContext();
+ mLocationIndex = -1;
+ mItems = new ArrayList<Texture>(details.size());
+ setTitle(String.format(context.getString(R.string.sequence_in_set),
+ mIndex + 1, mSource.size()));
+ setDetails(context, details);
+ }
+
+ private void setDetails(Context context, MediaDetails details) {
+ for (Entry<Integer, Object> detail : details) {
+ String value;
+ switch (detail.getKey()) {
+ case MediaDetails.INDEX_LOCATION: {
+ value = getLocationText((double[]) detail.getValue());
+ break;
+ }
+ case MediaDetails.INDEX_SIZE: {
+ value = Formatter.formatFileSize(
+ context, (Long) detail.getValue());
+ break;
+ }
+ case MediaDetails.INDEX_WHITE_BALANCE: {
+ value = "1".equals(detail.getValue())
+ ? context.getString(R.string.manual)
+ : context.getString(R.string.auto);
+ break;
+ }
+ case MediaDetails.INDEX_FLASH: {
+ MediaDetails.FlashState flash =
+ (MediaDetails.FlashState) detail.getValue();
+ // TODO: camera doesn't fill in the complete values, show more information
+ // when it is fixed.
+ if (flash.isFlashFired()) {
+ value = context.getString(R.string.flash_on);
+ } else {
+ value = context.getString(R.string.flash_off);
+ }
+ break;
+ }
+ case MediaDetails.INDEX_EXPOSURE_TIME: {
+ value = (String) detail.getValue();
+ double time = Double.valueOf(value);
+ if (time < 1.0f) {
+ value = String.format("1/%d", (int) (0.5f + 1 / time));
+ } else {
+ int integer = (int) time;
+ time -= integer;
+ value = String.valueOf(integer) + "''";
+ if (time > 0.0001) {
+ value += String.format(" 1/%d", (int) (0.5f + 1 / time));
+ }
+ }
+ break;
+ }
+ default: {
+ Object valueObj = detail.getValue();
+ // This shouldn't happen, log its key to help us diagnose the problem.
+ Utils.assertTrue(valueObj != null, "%s's value is Null",
+ getName(context, detail.getKey()));
+ value = valueObj.toString();
+ }
+ }
+ int key = detail.getKey();
+ if (details.hasUnit(key)) {
+ value = String.format("%s : %s %s", getName(context, key), value,
+ context.getString(details.getUnit(key)));
+ } else {
+ value = String.format("%s : %s", getName(context, key), value);
+ }
+ Texture label = MultiLineTexture.newInstance(
+ value, mMaxDetailLength, FONT_SIZE, FONT_COLOR);
+ mItems.add(label);
+ }
+ }
+
+ private String getLocationText(double[] latlng) {
+ String text = String.format("(%f, %f)", latlng[0], latlng[1]);
+ mAddressLookupJob = mContext.getThreadPool().submit(
+ new AddressLookupJob(latlng),
+ new FutureListener<Address>() {
+ public void onFutureDone(Future<Address> future) {
+ mAddressLookupJob = null;
+ if (!future.isCancelled()) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_REFRESH_LOCATION, future.get()));
+ }
+ }
+ });
+ mLocationIndex = mItems.size();
+ return text;
+ }
+
+ public void updateLocation(Address address) {
+ int index = mLocationIndex;
+ if (address != null && index >=0 && index < mItems.size()) {
+ Context context = mContext.getAndroidContext();
+ String parts[] = {
+ address.getAdminArea(),
+ address.getSubAdminArea(),
+ address.getLocality(),
+ address.getSubLocality(),
+ address.getThoroughfare(),
+ address.getSubThoroughfare(),
+ address.getPremises(),
+ address.getPostalCode(),
+ address.getCountryName()
+ };
+
+ String addressText = "";
+ for (int i = 0; i < parts.length; i++) {
+ if (parts[i] == null || parts[i].isEmpty()) continue;
+ if (!addressText.isEmpty()) {
+ addressText += ", ";
+ }
+ addressText += parts[i];
+ }
+ String text = String.format("%s : %s", getName(context,
+ MediaDetails.INDEX_LOCATION), addressText);
+ mItems.set(index, MultiLineTexture.newInstance(
+ text, mMaxDetailLength, FONT_SIZE, FONT_COLOR));
+ }
+ }
+
+ public Texture getView(int index) {
+ return mItems.get(index);
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+ }
+
+ private static String getName(Context context, int key) {
+ switch (key) {
+ case MediaDetails.INDEX_TITLE:
+ return context.getString(R.string.title);
+ case MediaDetails.INDEX_DESCRIPTION:
+ return context.getString(R.string.description);
+ case MediaDetails.INDEX_DATETIME:
+ return context.getString(R.string.time);
+ case MediaDetails.INDEX_LOCATION:
+ return context.getString(R.string.location);
+ case MediaDetails.INDEX_PATH:
+ return context.getString(R.string.path);
+ case MediaDetails.INDEX_WIDTH:
+ return context.getString(R.string.width);
+ case MediaDetails.INDEX_HEIGHT:
+ return context.getString(R.string.height);
+ case MediaDetails.INDEX_ORIENTATION:
+ return context.getString(R.string.orientation);
+ case MediaDetails.INDEX_DURATION:
+ return context.getString(R.string.duration);
+ case MediaDetails.INDEX_MIMETYPE:
+ return context.getString(R.string.mimetype);
+ case MediaDetails.INDEX_SIZE:
+ return context.getString(R.string.file_size);
+ case MediaDetails.INDEX_MAKE:
+ return context.getString(R.string.maker);
+ case MediaDetails.INDEX_MODEL:
+ return context.getString(R.string.model);
+ case MediaDetails.INDEX_FLASH:
+ return context.getString(R.string.flash);
+ case MediaDetails.INDEX_APERTURE:
+ return context.getString(R.string.aperture);
+ case MediaDetails.INDEX_FOCAL_LENGTH:
+ return context.getString(R.string.focal_length);
+ case MediaDetails.INDEX_WHITE_BALANCE:
+ return context.getString(R.string.white_balance);
+ case MediaDetails.INDEX_EXPOSURE_TIME:
+ return context.getString(R.string.exposure_time);
+ case MediaDetails.INDEX_ISO:
+ return context.getString(R.string.iso);
+ default:
+ return "Unknown key" + key;
+ }
+ }
+
+ private class DetailsPanel extends GLView {
+
+ @Override
+ public void onMeasure(int widthSpec, int heightSpec) {
+ if (mTitle == null || mModel == null) {
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, 0)
+ .measure(widthSpec, heightSpec);
+ return;
+ }
+
+ int h = getPaddings().top + LINE_SPACING;
+ for (int i = 0, n = mModel.size(); i < n; ++i) {
+ h += mModel.getView(i).getHeight() + LINE_SPACING;
+ }
+
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, h)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+
+ if (mTitle == null || mModel == null) {
+ return;
+ }
+ Rect p = getPaddings();
+ int x = p.left, y = p.top + LINE_SPACING;
+ for (int i = 0, n = mModel.size(); i < n ; i++) {
+ Texture t = mModel.getView(i);
+ t.draw(canvas, x, y);
+ y += t.getHeight() + LINE_SPACING;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java
new file mode 100644
index 000000000..3038232f6
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DisplayItem.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public abstract class DisplayItem {
+
+ protected int mWidth;
+ protected int mHeight;
+
+ protected void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ // returns true if more pass is needed
+ public abstract boolean render(GLCanvas canvas, int pass);
+
+ public abstract long getIdentity();
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java
new file mode 100644
index 000000000..19db77262
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DownUpDetector.java
@@ -0,0 +1,61 @@
+/*
+ * 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.ui;
+
+import android.view.MotionEvent;
+
+public class DownUpDetector {
+ public interface DownUpListener {
+ void onDown(MotionEvent e);
+ void onUp(MotionEvent e);
+ }
+
+ private boolean mStillDown;
+ private DownUpListener mListener;
+
+ public DownUpDetector(DownUpListener listener) {
+ mListener = listener;
+ }
+
+ private void setState(boolean down, MotionEvent e) {
+ if (down == mStillDown) return;
+ mStillDown = down;
+ if (down) {
+ mListener.onDown(e);
+ } else {
+ mListener.onUp(e);
+ }
+ }
+
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort.
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DrawableTexture.java b/src/com/android/gallery3d/ui/DrawableTexture.java
new file mode 100644
index 000000000..5c3964d5c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DrawableTexture.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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+// DrawableTexture is a texture whose content is from a Drawable.
+public class DrawableTexture extends CanvasTexture {
+
+ private final Drawable mDrawable;
+
+ public DrawableTexture(Drawable drawable, int width, int height) {
+ super(width, height);
+ mDrawable = drawable;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ mDrawable.setBounds(0, 0, mWidth, mHeight);
+ mDrawable.draw(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
new file mode 100644
index 000000000..8d28f2c7b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FilmStripView.java
@@ -0,0 +1,261 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.AlphaAnimation;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.app.AlbumDataAdapter;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaSet;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+public class FilmStripView extends GLView implements SlotView.Listener,
+ ScrollBarView.Listener, UserInteractionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilmStripView";
+
+ private static final int HIDE_ANIMATION_DURATION = 300; // 0.3 sec
+
+ public interface Listener {
+ void onSlotSelected(int slotIndex);
+ }
+
+ private int mTopMargin, mMidMargin, mBottomMargin;
+ private int mContentSize, mBarSize, mGripSize;
+ private AlbumView mAlbumView;
+ private ScrollBarView mScrollBarView;
+ private AlbumDataAdapter mAlbumDataAdapter;
+ private StripDrawer mStripDrawer;
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+ private boolean mFilmStripVisible;
+ private CanvasAnimation mFilmStripAnimation;
+ private NinePatchTexture mBackgroundTexture;
+
+ // The layout of FileStripView is
+ // topMargin
+ // ----+----+
+ // / +----+--\
+ // contentSize | | thumbSize
+ // \ +----+--/
+ // ----+----+
+ // midMargin
+ // ----+----+
+ // / +----+--\
+ // barSize | | gripSize
+ // \ +----+--/
+ // ----+----+
+ // bottomMargin
+ public FilmStripView(GalleryActivity activity, MediaSet mediaSet,
+ int topMargin, int midMargin, int bottomMargin, int contentSize,
+ int thumbSize, int barSize, int gripSize, int gripWidth) {
+ mTopMargin = topMargin;
+ mMidMargin = midMargin;
+ mBottomMargin = bottomMargin;
+ mContentSize = contentSize;
+ mBarSize = barSize;
+ mGripSize = gripSize;
+
+ mStripDrawer = new StripDrawer((Context) activity);
+ mAlbumView = new AlbumView(activity, thumbSize, thumbSize, thumbSize);
+ mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM);
+ mAlbumView.setSelectionDrawer(mStripDrawer);
+ mAlbumView.setListener(this);
+ mAlbumView.setUserInteractionListener(this);
+ mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet);
+ addComponent(mAlbumView);
+ mScrollBarView = new ScrollBarView(activity.getAndroidContext(),
+ mGripSize, gripWidth);
+ mScrollBarView.setListener(this);
+ addComponent(mScrollBarView);
+
+ mAlbumView.setModel(mAlbumDataAdapter);
+ mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(),
+ R.drawable.navstrip_translucent);
+ mFilmStripVisible = true;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setUserInteractionListener(UserInteractionListener listener) {
+ mUIListener = listener;
+ }
+
+ private void setFilmStripVisible(boolean visible) {
+ if (mFilmStripVisible == visible) return;
+ mFilmStripVisible = visible;
+ if (!visible) {
+ mFilmStripAnimation = new AlphaAnimation(1, 0);
+ mFilmStripAnimation.setDuration(HIDE_ANIMATION_DURATION);
+ mFilmStripAnimation.start();
+ } else {
+ mFilmStripAnimation = null;
+ }
+ invalidate();
+ }
+
+ public void show() {
+ setFilmStripVisible(true);
+ }
+
+ public void hide() {
+ setFilmStripVisible(false);
+ }
+
+ @Override
+ protected void onVisibilityChanged(int visibility) {
+ super.onVisibilityChanged(visibility);
+ if (visibility == GLView.VISIBLE) {
+ onUserInteraction();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int height = mTopMargin + mContentSize + mMidMargin + mBarSize + mBottomMargin;
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(MeasureSpec.getSize(widthSpec), height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) return;
+ mAlbumView.layout(0, mTopMargin, right - left, mTopMargin + mContentSize);
+ int barStart = mTopMargin + mContentSize + mMidMargin;
+ mScrollBarView.layout(0, barStart, right - left, barStart + mBarSize);
+ int width = right - left;
+ int height = bottom - top;
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ // consume all touch events on the "gray area", so they don't go to
+ // the photo view below. (otherwise you can scroll the picture through
+ // it).
+ return true;
+ }
+
+ @Override
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ if (!mFilmStripVisible && mFilmStripAnimation == null) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ onUserInteractionBegin();
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onUserInteractionEnd();
+ break;
+ }
+
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ CanvasAnimation anim = mFilmStripAnimation;
+ if (anim == null && !mFilmStripVisible) return;
+
+ boolean needRestore = false;
+ if (anim != null) {
+ needRestore = true;
+ canvas.save(anim.getCanvasSaveFlags());
+ long now = canvas.currentAnimationTimeMillis();
+ boolean more = anim.calculate(now);
+ anim.apply(canvas);
+ if (more) {
+ invalidate();
+ } else {
+ mFilmStripAnimation = null;
+ }
+ }
+
+ mBackgroundTexture.draw(canvas, 0, 0, getWidth(), getHeight());
+ super.render(canvas);
+
+ if (needRestore) {
+ canvas.restore();
+ }
+ }
+
+ // Called by AlbumView
+ public void onSingleTapUp(int slotIndex) {
+ mAlbumView.setFocusIndex(slotIndex);
+ mListener.onSlotSelected(slotIndex);
+ }
+
+ // Called by AlbumView
+ public void onLongTap(int slotIndex) {
+ onSingleTapUp(slotIndex);
+ }
+
+ // Called by AlbumView
+ public void onUserInteractionBegin() {
+ mUIListener.onUserInteractionBegin();
+ }
+
+ // Called by AlbumView
+ public void onUserInteractionEnd() {
+ mUIListener.onUserInteractionEnd();
+ }
+
+ // Called by AlbumView
+ public void onUserInteraction() {
+ mUIListener.onUserInteraction();
+ }
+
+ // Called by AlbumView
+ public void onScrollPositionChanged(int position, int total) {
+ mScrollBarView.setContentPosition(position, total);
+ }
+
+ // Called by ScrollBarView
+ public void onScrollBarPositionChanged(int position) {
+ mAlbumView.setScrollPosition(position);
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ mAlbumView.setFocusIndex(slotIndex);
+ mAlbumView.makeSlotVisible(slotIndex);
+ }
+
+ public void setStartIndex(int slotIndex) {
+ mAlbumView.setStartIndex(slotIndex);
+ }
+
+ public void pause() {
+ mAlbumView.pause();
+ mAlbumDataAdapter.pause();
+ }
+
+ public void resume() {
+ mAlbumView.resume();
+ mAlbumDataAdapter.resume();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
new file mode 100644
index 000000000..88c02f3b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLCanvas.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.ui;
+
+import android.graphics.RectF;
+
+import javax.microedition.khronos.opengles.GL11;
+
+//
+// GLCanvas gives a convenient interface to draw using OpenGL.
+//
+// When a rectangle is specified in this interface, it means the region
+// [x, x+width) * [y, y+height)
+//
+public interface GLCanvas {
+ // Tells GLCanvas the size of the underlying GL surface. This should be
+ // called before first drawing and when the size of GL surface is changed.
+ // This is called by GLRoot and should not be called by the clients
+ // who only want to draw on the GLCanvas. Both width and height must be
+ // nonnegative.
+ public void setSize(int width, int height);
+
+ // Clear the drawing buffers. This should only be used by GLRoot.
+ public void clearBuffer();
+
+ // This is the time value used to calculate the animation in the current
+ // frame. The "set" function should only called by GLRoot, and the
+ // "time" parameter must be nonnegative.
+ public void setCurrentAnimationTimeMillis(long time);
+ public long currentAnimationTimeMillis();
+
+ public void setBlendEnabled(boolean enabled);
+
+ // Sets and gets the current alpha, alpha must be in [0, 1].
+ public void setAlpha(float alpha);
+ public float getAlpha();
+
+ // (current alpha) = (current alpha) * alpha
+ public void multiplyAlpha(float alpha);
+
+ // Change the current transform matrix.
+ public void translate(float x, float y, float z);
+ public void scale(float sx, float sy, float sz);
+ public void rotate(float angle, float x, float y, float z);
+ public void multiplyMatrix(float[] mMatrix, int offset);
+
+ // Modifies the current clip with the specified rectangle.
+ // (current clip) = (current clip) intersect (specified rectangle).
+ // Returns true if the result clip is non-empty.
+ public boolean clipRect(int left, int top, int right, int bottom);
+
+ // Pushes the configuration state (matrix, alpha, and clip) onto
+ // a private stack.
+ public int save();
+
+ // Same as save(), but only save those specified in saveFlags.
+ public int save(int saveFlags);
+
+ public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
+ public static final int SAVE_FLAG_CLIP = 0x01;
+ public static final int SAVE_FLAG_ALPHA = 0x02;
+ public static final int SAVE_FLAG_MATRIX = 0x04;
+
+ // Pops from the top of the stack as current configuration state (matrix,
+ // alpha, and clip). This call balances a previous call to save(), and is
+ // used to remove all modifications to the configuration state since the
+ // last save call.
+ public void restore();
+
+ // Draws a line using the specified paint from (x1, y1) to (x2, y2).
+ // (Both end points are included).
+ public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
+
+ // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
+ // (Both end points are included).
+ public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
+
+ // Fills the specified rectangle with the specified color.
+ public void fillRect(float x, float y, float width, float height, int color);
+
+ // Draws a texture to the specified rectangle.
+ public void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height);
+ public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+ int uvBuffer, int indexBuffer, int indexCount);
+
+ // Draws a texture to the specified rectangle. The "alpha" parameter
+ // overrides the current drawing alpha value.
+ public void drawTexture(BasicTexture texture,
+ int x, int y, int width, int height, float alpha);
+
+ // Draws a the source rectangle part of the texture to the target rectangle.
+ public void drawTexture(BasicTexture texture, RectF source, RectF target);
+
+ // Draw two textures to the specified rectangle. The actual texture used is
+ // from * (1 - ratio) + to * ratio
+ // The two textures must have the same size.
+ public void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int w, int h);
+
+ public void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int w, int h);
+
+ // Return a texture copied from the specified rectangle.
+ public BasicTexture copyTexture(int x, int y, int width, int height);
+
+ // Gets the underlying GL instance. This is used only when direct access to
+ // GL is needed.
+ public GL11 getGLInstance();
+
+ // Unloads the specified texture from the canvas. The resource allocated
+ // to draw the texture will be released. The specified texture will return
+ // to the unloaded state. This function should be called only from
+ // BasicTexture or its descendant
+ public boolean unloadTexture(BasicTexture texture);
+
+ // Delete the specified buffer object, similar to unloadTexture.
+ public void deleteBuffer(int bufferId);
+
+ // Delete the textures and buffers in GL side. This function should only be
+ // called in the GL thread.
+ public void deleteRecycledResources();
+
+}
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
new file mode 100644
index 000000000..387743f5d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -0,0 +1,913 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IntArray;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLU;
+import android.opengl.Matrix;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.Stack;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+public class GLCanvasImpl implements GLCanvas {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GLCanvasImp";
+
+ private static final float OPAQUE_ALPHA = 0.95f;
+
+ private static final int OFFSET_FILL_RECT = 0;
+ private static final int OFFSET_DRAW_LINE = 4;
+ private static final int OFFSET_DRAW_RECT = 6;
+ private static final float[] BOX_COORDINATES = {
+ 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle
+ 0, 0, 1, 1, // used for drawing a line
+ 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle
+
+ private final GL11 mGL;
+
+ private final float mMatrixValues[] = new float[16];
+ private final float mTextureMatrixValues[] = new float[16];
+
+ // mapPoints needs 10 input and output numbers.
+ private final float mMapPointsBuffer[] = new float[10];
+
+ private final float mTextureColor[] = new float[4];
+
+ private int mBoxCoords;
+
+ private final GLState mGLState;
+
+ private long mAnimationTime;
+
+ private float mAlpha;
+ private final Rect mClipRect = new Rect();
+ private final Stack<ConfigState> mRestoreStack =
+ new Stack<ConfigState>();
+ private ConfigState mRecycledRestoreAction;
+
+ private final RectF mDrawTextureSourceRect = new RectF();
+ private final RectF mDrawTextureTargetRect = new RectF();
+ private final float[] mTempMatrix = new float[32];
+ private final IntArray mUnboundTextures = new IntArray();
+ private final IntArray mDeleteBuffers = new IntArray();
+ private int mHeight;
+ private boolean mBlendEnabled = true;
+
+ // Drawing statistics
+ int mCountDrawLine;
+ int mCountFillRect;
+ int mCountDrawMesh;
+ int mCountTextureRect;
+ int mCountTextureOES;
+
+ GLCanvasImpl(GL11 gl) {
+ mGL = gl;
+ mGLState = new GLState(gl);
+ initialize();
+ }
+
+ public void setSize(int width, int height) {
+ Utils.assertTrue(width >= 0 && height >= 0);
+ mHeight = height;
+
+ GL11 gl = mGL;
+ gl.glViewport(0, 0, width, height);
+ gl.glMatrixMode(GL11.GL_PROJECTION);
+ gl.glLoadIdentity();
+ GLU.gluOrtho2D(gl, 0, width, 0, height);
+
+ gl.glMatrixMode(GL11.GL_MODELVIEW);
+ gl.glLoadIdentity();
+ float matrix[] = mMatrixValues;
+
+ Matrix.setIdentityM(matrix, 0);
+ Matrix.translateM(matrix, 0, 0, mHeight, 0);
+ Matrix.scaleM(matrix, 0, 1, -1, 1);
+
+ mClipRect.set(0, 0, width, height);
+ gl.glScissor(0, 0, width, height);
+ }
+
+ public long currentAnimationTimeMillis() {
+ return mAnimationTime;
+ }
+
+ public void setAlpha(float alpha) {
+ Utils.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha = alpha;
+ }
+
+ public void multiplyAlpha(float alpha) {
+ Utils.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha *= alpha;
+ }
+
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ private void initialize() {
+ GL11 gl = mGL;
+
+ // First create an nio buffer, then create a VBO from it.
+ int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
+ FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
+
+ int[] name = new int[1];
+ gl.glGenBuffers(1, name, 0);
+ mBoxCoords = name[0];
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ xyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ xyBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+ gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ // Enable the texture coordinate array for Texture 1
+ gl.glClientActiveTexture(GL11.GL_TEXTURE1);
+ gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+ gl.glClientActiveTexture(GL11.GL_TEXTURE0);
+ gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+
+ // mMatrixValues will be initialized in setSize()
+ mAlpha = 1.0f;
+ }
+
+ public void drawRect(float x, float y, float width, float height, GLPaint paint) {
+ GL11 gl = mGL;
+
+ mGLState.setColorMode(paint.getColor(), mAlpha);
+ mGLState.setLineWidth(paint.getLineWidth());
+ mGLState.setLineSmooth(paint.getAntiAlias());
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
+ GL11 gl = mGL;
+
+ mGLState.setColorMode(paint.getColor(), mAlpha);
+ mGLState.setLineWidth(paint.getLineWidth());
+ mGLState.setLineSmooth(paint.getAntiAlias());
+
+ saveTransform();
+ translate(x1, y1, 0);
+ scale(x2 - x1, y2 - y1, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ public void fillRect(float x, float y, float width, float height, int color) {
+ mGLState.setColorMode(color, mAlpha);
+ GL11 gl = mGL;
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountFillRect++;
+ }
+
+ public void translate(float x, float y, float z) {
+ Matrix.translateM(mMatrixValues, 0, x, y, z);
+ }
+
+ public void scale(float sx, float sy, float sz) {
+ Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
+ }
+
+ public void rotate(float angle, float x, float y, float z) {
+ float[] temp = mTempMatrix;
+ Matrix.setRotateM(temp, 0, angle, x, y, z);
+ Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
+ System.arraycopy(temp, 16, mMatrixValues, 0, 16);
+ }
+
+ public void multiplyMatrix(float matrix[], int offset) {
+ float[] temp = mTempMatrix;
+ Matrix.multiplyMM(temp, 0, mMatrixValues , 0, matrix, 0);
+ System.arraycopy(temp, 0, mMatrixValues, 0, 16);
+ }
+
+ private void textureRect(float x, float y, float width, float height) {
+ GL11 gl = mGL;
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountTextureRect++;
+ }
+
+ public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+ int uvBuffer, int indexBuffer, int indexCount) {
+ float alpha = mAlpha;
+ if (!bindTexture(tex)) return;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!tex.isOpaque() || alpha < OPAQUE_ALPHA));
+ mGLState.setTextureAlpha(alpha);
+
+ // Reset the texture matrix. We will set our own texture coordinates
+ // below.
+ setTextureCoords(0, 0, 1, 1);
+
+ saveTransform();
+ translate(x, y, 0);
+
+ mGL.glLoadMatrixf(mMatrixValues, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);
+ mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);
+ mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+ mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP,
+ indexCount, GL11.GL_UNSIGNED_BYTE, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+ mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+ mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ restoreTransform();
+ mCountDrawMesh++;
+ }
+
+ private float[] mapPoints(float matrix[], int x1, int y1, int x2, int y2) {
+ float[] point = mMapPointsBuffer;
+ int srcOffset = 6;
+ point[srcOffset] = x1;
+ point[srcOffset + 1] = y1;
+ point[srcOffset + 2] = 0;
+ point[srcOffset + 3] = 1;
+
+ int resultOffset = 0;
+ Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
+ point[resultOffset] /= point[resultOffset + 3];
+ point[resultOffset + 1] /= point[resultOffset + 3];
+
+ // map the second point
+ point[srcOffset] = x2;
+ point[srcOffset + 1] = y2;
+ resultOffset = 2;
+ Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
+ point[resultOffset] /= point[resultOffset + 3];
+ point[resultOffset + 1] /= point[resultOffset + 3];
+
+ return point;
+ }
+
+ public boolean clipRect(int left, int top, int right, int bottom) {
+ float point[] = mapPoints(mMatrixValues, left, top, right, bottom);
+
+ // mMatrix could be a rotation matrix. In this case, we need to find
+ // the boundaries after rotation. (only handle 90 * n degrees)
+ if (point[0] > point[2]) {
+ left = (int) point[2];
+ right = (int) point[0];
+ } else {
+ left = (int) point[0];
+ right = (int) point[2];
+ }
+ if (point[1] > point[3]) {
+ top = (int) point[3];
+ bottom = (int) point[1];
+ } else {
+ top = (int) point[1];
+ bottom = (int) point[3];
+ }
+ Rect clip = mClipRect;
+
+ boolean intersect = clip.intersect(left, top, right, bottom);
+ if (!intersect) clip.set(0, 0, 0, 0);
+ mGL.glScissor(clip.left, clip.top, clip.width(), clip.height());
+ return intersect;
+ }
+
+ private void drawBoundTexture(
+ BasicTexture texture, int x, int y, int width, int height) {
+ // Test whether it has been rotated or flipped, if so, glDrawTexiOES
+ // won't work
+ if (isMatrixRotatedOrFlipped(mMatrixValues)) {
+ setTextureCoords(0, 0,
+ (float) texture.getWidth() / texture.getTextureWidth(),
+ (float) texture.getHeight() / texture.getTextureHeight());
+ textureRect(x, y, width, height);
+ } else {
+ // draw the rect from bottom-left to top-right
+ float points[] = mapPoints(
+ mMatrixValues, x, y + height, x + width, y);
+ x = Math.round(points[0]);
+ y = Math.round(points[1]);
+ width = Math.round(points[2]) - x;
+ height = Math.round(points[3]) - y;
+ if (width > 0 && height > 0) {
+ ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
+ mCountTextureOES++;
+ }
+ }
+ }
+
+ public void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height) {
+ drawTexture(texture, x, y, width, height, mAlpha);
+ }
+
+ public void setBlendEnabled(boolean enabled) {
+ mBlendEnabled = enabled;
+ }
+
+ public void drawTexture(BasicTexture texture,
+ int x, int y, int width, int height, float alpha) {
+ if (width <= 0 || height <= 0) return;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
+ if (!bindTexture(texture)) return;
+ mGLState.setTextureAlpha(alpha);
+ drawBoundTexture(texture, x, y, width, height);
+ }
+
+ public void drawTexture(BasicTexture texture, RectF source, RectF target) {
+ if (target.width() <= 0 || target.height() <= 0) return;
+
+ // Copy the input to avoid changing it.
+ mDrawTextureSourceRect.set(source);
+ mDrawTextureTargetRect.set(target);
+ source = mDrawTextureSourceRect;
+ target = mDrawTextureTargetRect;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+ if (!bindTexture(texture)) return;
+ convertCoordinate(source, target, texture);
+ setTextureCoords(source);
+ mGLState.setTextureAlpha(mAlpha);
+ textureRect(target.left, target.top, target.width(), target.height());
+ }
+
+ // This function changes the source coordinate to the texture coordinates.
+ // It also clips the source and target coordinates if it is beyond the
+ // bound of the texture.
+ private void convertCoordinate(RectF source, RectF target,
+ BasicTexture texture) {
+
+ int width = texture.getWidth();
+ int height = texture.getHeight();
+ int texWidth = texture.getTextureWidth();
+ int texHeight = texture.getTextureHeight();
+ // Convert to texture coordinates
+ source.left /= texWidth;
+ source.right /= texWidth;
+ source.top /= texHeight;
+ source.bottom /= texHeight;
+
+ // Clip if the rendering range is beyond the bound of the texture.
+ float xBound = (float) width / texWidth;
+ if (source.right > xBound) {
+ target.right = target.left + target.width() *
+ (xBound - source.left) / source.width();
+ source.right = xBound;
+ }
+ float yBound = (float) height / texHeight;
+ if (source.bottom > yBound) {
+ target.bottom = target.top + target.height() *
+ (yBound - source.top) / source.height();
+ source.bottom = yBound;
+ }
+ }
+
+ public void drawMixed(BasicTexture from,
+ int toColor, float ratio, int x, int y, int w, int h) {
+ drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
+ }
+
+ public void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int w, int h) {
+ drawMixed(from, to, ratio, x, y, w, h, mAlpha);
+ }
+
+ private boolean bindTexture(BasicTexture texture) {
+ if (!texture.onBind(this)) return false;
+ mGLState.setTexture2DEnabled(true);
+ mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+ return true;
+ }
+
+ private void setTextureColor(float r, float g, float b, float alpha) {
+ float[] color = mTextureColor;
+ color[0] = r;
+ color[1] = g;
+ color[2] = b;
+ color[3] = alpha;
+ }
+
+ private void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int width, int height, float alpha) {
+
+ if (ratio <= 0) {
+ drawTexture(from, x, y, width, height, alpha);
+ return;
+ } else if (ratio >= 1) {
+ fillRect(x, y, width, height, toColor);
+ return;
+ }
+
+ mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+ || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+ final GL11 gl = mGL;
+ if (!bindTexture(from)) return;
+
+ //
+ // The formula we want:
+ // alpha * ((1 - ratio) * from + ratio * to)
+ // The formula that GL supports is in the form of:
+ // combo * (modulate * from) + (1 - combo) * to
+ //
+ // So, we have combo = 1 - alpha * ratio
+ // and modulate = alpha * (1f - ratio) / combo
+ //
+ float comboRatio = 1 - alpha * ratio;
+
+ // handle the case that (1 - comboRatio) == 0
+ if (alpha < OPAQUE_ALPHA) {
+ mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
+ } else {
+ mGLState.setTextureAlpha(1f);
+ }
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ // Specify the interpolation factor via the alpha component of
+ // GL_TEXTURE_ENV_COLORs.
+ // RGB component are get from toColor and will used as SRC1
+ float colorAlpha = (float) (toColor >>> 24) / (0xff * 0xff);
+ setTextureColor(((toColor >>> 16) & 0xff) * colorAlpha,
+ ((toColor >>> 8) & 0xff) * colorAlpha,
+ (toColor & 0xff) * colorAlpha, comboRatio);
+ gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for RGB.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for alpha.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+ drawBoundTexture(from, x, y, width, height);
+ mGLState.setTexEnvMode(GL11.GL_REPLACE);
+ }
+
+ private void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int width, int height, float alpha) {
+
+ if (ratio <= 0) {
+ drawTexture(from, x, y, width, height, alpha);
+ return;
+ } else if (ratio >= 1) {
+ drawTexture(to, x, y, width, height, alpha);
+ return;
+ }
+
+ // In the current implementation the two textures must have the
+ // same size.
+ Utils.assertTrue(from.getWidth() == to.getWidth()
+ && from.getHeight() == to.getHeight());
+
+ mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+ || !to.isOpaque() || alpha < OPAQUE_ALPHA));
+
+ final GL11 gl = mGL;
+ if (!bindTexture(from)) return;
+
+ //
+ // The formula we want:
+ // alpha * ((1 - ratio) * from + ratio * to)
+ // The formula that GL supports is in the form of:
+ // combo * (modulate * from) + (1 - combo) * to
+ //
+ // So, we have combo = 1 - alpha * ratio
+ // and modulate = alpha * (1f - ratio) / combo
+ //
+ float comboRatio = 1 - alpha * ratio;
+
+ // handle the case that (1 - comboRatio) == 0
+ if (alpha < OPAQUE_ALPHA) {
+ mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
+ } else {
+ mGLState.setTextureAlpha(1f);
+ }
+
+ gl.glActiveTexture(GL11.GL_TEXTURE1);
+ if (!bindTexture(to)) {
+ // Disable TEXTURE1.
+ gl.glDisable(GL11.GL_TEXTURE_2D);
+ // Switch back to the default texture unit.
+ gl.glActiveTexture(GL11.GL_TEXTURE0);
+ return;
+ }
+ gl.glEnable(GL11.GL_TEXTURE_2D);
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+
+ // Specify the interpolation factor via the alpha component of
+ // GL_TEXTURE_ENV_COLORs.
+ // We don't use the RGB color, so just give them 0s.
+ setTextureColor(0, 0, 0, comboRatio);
+ gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+ // Wire up the interpolation factor for RGB.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for alpha.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+ // Draw the combined texture.
+ drawBoundTexture(to, x, y, width, height);
+
+ // Disable TEXTURE1.
+ gl.glDisable(GL11.GL_TEXTURE_2D);
+ // Switch back to the default texture unit.
+ gl.glActiveTexture(GL11.GL_TEXTURE0);
+ }
+
+ // TODO: the code only work for 2D should get fixed for 3D or removed
+ private static final int MSKEW_X = 4;
+ private static final int MSKEW_Y = 1;
+ private static final int MSCALE_X = 0;
+ private static final int MSCALE_Y = 5;
+
+ private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
+ final float eps = 1e-5f;
+ return Math.abs(matrix[MSKEW_X]) > eps
+ || Math.abs(matrix[MSKEW_Y]) > eps
+ || matrix[MSCALE_X] < -eps
+ || matrix[MSCALE_Y] > eps;
+ }
+
+ public BasicTexture copyTexture(int x, int y, int width, int height) {
+
+ if (isMatrixRotatedOrFlipped(mMatrixValues)) {
+ throw new IllegalArgumentException("cannot support rotated matrix");
+ }
+ float points[] = mapPoints(mMatrixValues, x, y + height, x + width, y);
+ x = (int) points[0];
+ y = (int) points[1];
+ width = (int) points[2] - x;
+ height = (int) points[3] - y;
+
+ GL11 gl = mGL;
+
+ RawTexture texture = RawTexture.newInstance(this);
+ gl.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+ texture.setSize(width, height);
+
+ int[] cropRect = {0, 0, width, height};
+ gl.glTexParameteriv(GL11.GL_TEXTURE_2D,
+ GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ gl.glCopyTexImage2D(GL11.GL_TEXTURE_2D, 0,
+ GL11.GL_RGB, x, y, texture.getTextureWidth(),
+ texture.getTextureHeight(), 0);
+
+ return texture;
+ }
+
+ private static class GLState {
+
+ private final GL11 mGL;
+
+ private int mTexEnvMode = GL11.GL_REPLACE;
+ private float mTextureAlpha = 1.0f;
+ private boolean mTexture2DEnabled = true;
+ private boolean mBlendEnabled = true;
+ private float mLineWidth = 1.0f;
+ private boolean mLineSmooth = false;
+
+ public GLState(GL11 gl) {
+ mGL = gl;
+
+ // Disable unused state
+ gl.glDisable(GL11.GL_LIGHTING);
+
+ // Enable used features
+ gl.glEnable(GL11.GL_DITHER);
+ gl.glEnable(GL11.GL_SCISSOR_TEST);
+
+ gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
+ gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+ gl.glEnable(GL11.GL_TEXTURE_2D);
+
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV,
+ GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+
+ // Set the background color
+ gl.glClearColor(0f, 0f, 0f, 0f);
+ gl.glClearStencil(0);
+
+ gl.glEnable(GL11.GL_BLEND);
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+
+ // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
+ gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);
+ }
+
+ public void setTexEnvMode(int mode) {
+ if (mTexEnvMode == mode) return;
+ mTexEnvMode = mode;
+ mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);
+ }
+
+ public void setLineWidth(float width) {
+ if (mLineWidth == width) return;
+ mLineWidth = width;
+ mGL.glLineWidth(width);
+ }
+
+ public void setLineSmooth(boolean enabled) {
+ if (mLineSmooth == enabled) return;
+ mLineSmooth = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_LINE_SMOOTH);
+ } else {
+ mGL.glDisable(GL11.GL_LINE_SMOOTH);
+ }
+ }
+
+ public void setTextureAlpha(float alpha) {
+ if (mTextureAlpha == alpha) return;
+ mTextureAlpha = alpha;
+ if (alpha >= OPAQUE_ALPHA) {
+ // The alpha is need for those texture without alpha channel
+ mGL.glColor4f(1, 1, 1, 1);
+ setTexEnvMode(GL11.GL_REPLACE);
+ } else {
+ mGL.glColor4f(alpha, alpha, alpha, alpha);
+ setTexEnvMode(GL11.GL_MODULATE);
+ }
+ }
+
+ public void setColorMode(int color, float alpha) {
+ setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA);
+
+ // Set mTextureAlpha to an invalid value, so that it will reset
+ // again in setTextureAlpha(float) later.
+ mTextureAlpha = -1.0f;
+
+ setTexture2DEnabled(false);
+
+ float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
+ mGL.glColor4x(
+ Math.round(((color >> 16) & 0xFF) * prealpha),
+ Math.round(((color >> 8) & 0xFF) * prealpha),
+ Math.round((color & 0xFF) * prealpha),
+ Math.round(255 * prealpha));
+ }
+
+ public void setTexture2DEnabled(boolean enabled) {
+ if (mTexture2DEnabled == enabled) return;
+ mTexture2DEnabled = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_TEXTURE_2D);
+ } else {
+ mGL.glDisable(GL11.GL_TEXTURE_2D);
+ }
+ }
+
+ public void setBlendEnabled(boolean enabled) {
+ if (mBlendEnabled == enabled) return;
+ mBlendEnabled = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_BLEND);
+ } else {
+ mGL.glDisable(GL11.GL_BLEND);
+ }
+ }
+ }
+
+ public GL11 getGLInstance() {
+ return mGL;
+ }
+
+ public void setCurrentAnimationTimeMillis(long time) {
+ Utils.assertTrue(time >= 0);
+ mAnimationTime = time;
+ }
+
+ public void clearBuffer() {
+ mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
+ }
+
+ private void setTextureCoords(RectF source) {
+ setTextureCoords(source.left, source.top, source.right, source.bottom);
+ }
+
+ private void setTextureCoords(float left, float top,
+ float right, float bottom) {
+ mGL.glMatrixMode(GL11.GL_TEXTURE);
+ mTextureMatrixValues[0] = right - left;
+ mTextureMatrixValues[5] = bottom - top;
+ mTextureMatrixValues[10] = 1;
+ mTextureMatrixValues[12] = left;
+ mTextureMatrixValues[13] = top;
+ mTextureMatrixValues[15] = 1;
+ mGL.glLoadMatrixf(mTextureMatrixValues, 0);
+ mGL.glMatrixMode(GL11.GL_MODELVIEW);
+ }
+
+ // unloadTexture and deleteBuffer can be called from the finalizer thread,
+ // so we synchronized on the mUnboundTextures object.
+ public boolean unloadTexture(BasicTexture t) {
+ synchronized (mUnboundTextures) {
+ if (!t.isLoaded(this)) return false;
+ mUnboundTextures.add(t.mId);
+ return true;
+ }
+ }
+
+ public void deleteBuffer(int bufferId) {
+ synchronized (mUnboundTextures) {
+ mDeleteBuffers.add(bufferId);
+ }
+ }
+
+ public void deleteRecycledResources() {
+ synchronized (mUnboundTextures) {
+ IntArray ids = mUnboundTextures;
+ if (ids.size() > 0) {
+ mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+
+ ids = mDeleteBuffers;
+ if (ids.size() > 0) {
+ mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+ }
+ }
+
+ public int save() {
+ return save(SAVE_FLAG_ALL);
+ }
+
+ public int save(int saveFlags) {
+ ConfigState config = obtainRestoreConfig();
+
+ if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
+ config.mAlpha = mAlpha;
+ } else {
+ config.mAlpha = -1;
+ }
+
+ if ((saveFlags & SAVE_FLAG_CLIP) != 0) {
+ config.mRect.set(mClipRect);
+ } else {
+ config.mRect.left = Integer.MAX_VALUE;
+ }
+
+ if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
+ System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
+ } else {
+ config.mMatrix[0] = Float.NEGATIVE_INFINITY;
+ }
+
+ mRestoreStack.push(config);
+ return mRestoreStack.size() - 1;
+ }
+
+ public void restore() {
+ if (mRestoreStack.isEmpty()) throw new IllegalStateException();
+ ConfigState config = mRestoreStack.pop();
+ config.restore(this);
+ freeRestoreConfig(config);
+ }
+
+ private void freeRestoreConfig(ConfigState action) {
+ action.mNextFree = mRecycledRestoreAction;
+ mRecycledRestoreAction = action;
+ }
+
+ private ConfigState obtainRestoreConfig() {
+ if (mRecycledRestoreAction != null) {
+ ConfigState result = mRecycledRestoreAction;
+ mRecycledRestoreAction = result.mNextFree;
+ return result;
+ }
+ return new ConfigState();
+ }
+
+ private static class ConfigState {
+ float mAlpha;
+ Rect mRect = new Rect();
+ float mMatrix[] = new float[16];
+ ConfigState mNextFree;
+
+ public void restore(GLCanvasImpl canvas) {
+ if (mAlpha >= 0) canvas.setAlpha(mAlpha);
+ if (mRect.left != Integer.MAX_VALUE) {
+ Rect rect = mRect;
+ canvas.mClipRect.set(rect);
+ canvas.mGL.glScissor(
+ rect.left, rect.top, rect.width(), rect.height());
+ }
+ if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
+ System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
+ }
+ }
+ }
+
+ public void dumpStatisticsAndClear() {
+ String line = String.format(
+ "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d",
+ mCountDrawMesh, mCountTextureRect, mCountTextureOES,
+ mCountFillRect, mCountDrawLine);
+ mCountDrawMesh = 0;
+ mCountTextureRect = 0;
+ mCountTextureOES = 0;
+ mCountFillRect = 0;
+ mCountDrawLine = 0;
+ Log.d(TAG, line);
+ }
+
+ private void saveTransform() {
+ System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
+ }
+
+ private void restoreTransform() {
+ System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLPaint.java b/src/com/android/gallery3d/ui/GLPaint.java
new file mode 100644
index 000000000..9f7b6f1f3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLPaint.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+
+public class GLPaint {
+ public static final int FLAG_ANTI_ALIAS = 0x01;
+
+ private int mFlags = 0;
+ private float mLineWidth = 1f;
+ private int mColor = 0;
+
+ public int getFlags() {
+ return mFlags;
+ }
+
+ public void setFlags(int flags) {
+ mFlags = flags;
+ }
+
+ public void setColor(int color) {
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public void setLineWidth(float width) {
+ Utils.assertTrue(width >= 0);
+ mLineWidth = width;
+ }
+
+ public float getLineWidth() {
+ return mLineWidth;
+ }
+
+ public void setAntiAlias(boolean enabled) {
+ if (enabled) {
+ mFlags |= FLAG_ANTI_ALIAS;
+ } else {
+ mFlags &= ~FLAG_ANTI_ALIAS;
+ }
+ }
+
+ public boolean getAntiAlias(){
+ return (mFlags & FLAG_ANTI_ALIAS) != 0;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
new file mode 100644
index 000000000..24e5794b0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -0,0 +1,37 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+
+public interface GLRoot {
+
+ public static interface OnGLIdleListener {
+ public boolean onGLIdle(GLRoot root, GLCanvas canvas);
+ }
+
+ public void addOnGLIdleListener(OnGLIdleListener listener);
+ public void registerLaunchedAnimation(CanvasAnimation animation);
+ public void requestRender();
+ public void requestLayoutContentPane();
+ public boolean hasStencil();
+
+ public void lockRenderThread();
+ public void unlockRenderThread();
+
+ public void setContentPane(GLView content);
+}
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
new file mode 100644
index 000000000..e03adf1c4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -0,0 +1,414 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.opengl.GLSurfaceView;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.locks.ReentrantLock;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+// The root component of all <code>GLView</code>s. The rendering is done in GL
+// thread while the event handling is done in the main thread. To synchronize
+// the two threads, the entry points of this package need to synchronize on the
+// <code>GLRootView</code> instance unless it can be proved that the rendering
+// thread won't access the same thing as the method. The entry points include:
+// (1) The public methods of HeadUpDisplay
+// (2) The public methods of CameraHeadUpDisplay
+// (3) The overridden methods in GLRootView.
+public class GLRootView extends GLSurfaceView
+ implements GLSurfaceView.Renderer, GLRoot {
+ private static final String TAG = "GLRootView";
+
+ private static final boolean DEBUG_FPS = false;
+ private int mFrameCount = 0;
+ private long mFrameCountingStart = 0;
+
+ private static final boolean DEBUG_INVALIDATE = false;
+ private int mInvalidateColor = 0;
+
+ private static final boolean DEBUG_DRAWING_STAT = false;
+
+ private static final int FLAG_INITIALIZED = 1;
+ private static final int FLAG_NEED_LAYOUT = 2;
+
+ private GL11 mGL;
+ private GLCanvasImpl mCanvas;
+
+ private GLView mContentView;
+ private DisplayMetrics mDisplayMetrics;
+
+ private int mFlags = FLAG_NEED_LAYOUT;
+ private volatile boolean mRenderRequested = false;
+
+ private Rect mClipRect = new Rect();
+ private int mClipRetryCount = 0;
+
+ private final GalleryEGLConfigChooser mEglConfigChooser =
+ new GalleryEGLConfigChooser();
+
+ private final ArrayList<CanvasAnimation> mAnimations =
+ new ArrayList<CanvasAnimation>();
+
+ private final LinkedList<OnGLIdleListener> mIdleListeners =
+ new LinkedList<OnGLIdleListener>();
+
+ private final IdleRunner mIdleRunner = new IdleRunner();
+
+ private final ReentrantLock mRenderLock = new ReentrantLock();
+
+ private static final int TARGET_FRAME_TIME = 33;
+ private long mLastDrawFinishTime;
+ private boolean mInDownState = false;
+
+ public GLRootView(Context context) {
+ this(context, null);
+ }
+
+ public GLRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlags |= FLAG_INITIALIZED;
+ setBackgroundDrawable(null);
+ setEGLConfigChooser(mEglConfigChooser);
+ setRenderer(this);
+ getHolder().setFormat(PixelFormat.RGB_565);
+
+ // Uncomment this to enable gl error check.
+ //setDebugFlags(DEBUG_CHECK_GL_ERROR);
+ }
+
+ public GalleryEGLConfigChooser getEGLConfigChooser() {
+ return mEglConfigChooser;
+ }
+
+ @Override
+ public boolean hasStencil() {
+ return getEGLConfigChooser().getStencilBits() > 0;
+ }
+
+ @Override
+ public void registerLaunchedAnimation(CanvasAnimation animation) {
+ // Register the newly launched animation so that we can set the start
+ // time more precisely. (Usually, it takes much longer for first
+ // rendering, so we set the animation start time as the time we
+ // complete rendering)
+ mAnimations.add(animation);
+ }
+
+ @Override
+ public void addOnGLIdleListener(OnGLIdleListener listener) {
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ mIdleRunner.enable();
+ }
+ }
+
+ @Override
+ public void setContentPane(GLView content) {
+ if (mContentView == content) return;
+ if (mContentView != null) {
+ if (mInDownState) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mContentView.dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ mInDownState = false;
+ }
+ mContentView.detachFromRoot();
+ BasicTexture.yieldAllTextures();
+ }
+ mContentView = content;
+ if (content != null) {
+ content.attachToRoot(this);
+ requestLayoutContentPane();
+ }
+ }
+
+ public GLView getContentPane() {
+ return mContentView;
+ }
+
+ @Override
+ public void requestRender() {
+ if (DEBUG_INVALIDATE) {
+ StackTraceElement e = Thread.currentThread().getStackTrace()[4];
+ String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
+ Log.d(TAG, "invalidate: " + caller);
+ }
+ if (mRenderRequested) return;
+ mRenderRequested = true;
+ super.requestRender();
+ }
+
+ @Override
+ public void requestLayoutContentPane() {
+ mRenderLock.lock();
+ try {
+ if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
+
+ // "View" system will invoke onLayout() for initialization(bug ?), we
+ // have to ignore it since the GLThread is not ready yet.
+ if ((mFlags & FLAG_INITIALIZED) == 0) return;
+
+ mFlags |= FLAG_NEED_LAYOUT;
+ requestRender();
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private void layoutContentPane() {
+ mFlags &= ~FLAG_NEED_LAYOUT;
+ int width = getWidth();
+ int height = getHeight();
+ Log.i(TAG, "layout content pane " + width + "x" + height);
+ if (mContentView != null && width != 0 && height != 0) {
+ mContentView.layout(0, 0, width, height);
+ }
+ // Uncomment this to dump the view hierarchy.
+ //mContentView.dumpTree("");
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (changed) requestLayoutContentPane();
+ }
+
+ /**
+ * Called when the context is created, possibly after automatic destruction.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+ GL11 gl = (GL11) gl1;
+ if (mGL != null) {
+ // The GL Object has changed
+ Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+ }
+ mGL = gl;
+ mCanvas = new GLCanvasImpl(gl);
+ if (!DEBUG_FPS) {
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ } else {
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ }
+ }
+
+ /**
+ * Called when the OpenGL surface is recreated without destroying the
+ * context.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceChanged(GL10 gl1, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
+ + ", gl10: " + gl1.toString());
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+ GalleryUtils.setRenderThread();
+ GL11 gl = (GL11) gl1;
+ Utils.assertTrue(mGL == gl);
+
+ mCanvas.setSize(width, height);
+
+ mClipRect.set(0, 0, width, height);
+ mClipRetryCount = 2;
+ }
+
+ private void outputFps() {
+ long now = System.nanoTime();
+ if (mFrameCountingStart == 0) {
+ mFrameCountingStart = now;
+ } else if ((now - mFrameCountingStart) > 1000000000) {
+ Log.d(TAG, "fps: " + (double) mFrameCount
+ * 1000000000 / (now - mFrameCountingStart));
+ mFrameCountingStart = now;
+ mFrameCount = 0;
+ }
+ ++mFrameCount;
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ mRenderLock.lock();
+ try {
+ onDrawFrameLocked(gl);
+ } finally {
+ mRenderLock.unlock();
+ }
+ long end = SystemClock.uptimeMillis();
+
+ if (mLastDrawFinishTime != 0) {
+ long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end;
+ if (wait > 0) {
+ SystemClock.sleep(wait);
+ }
+ }
+ mLastDrawFinishTime = SystemClock.uptimeMillis();
+ }
+
+ private void onDrawFrameLocked(GL10 gl) {
+ if (DEBUG_FPS) outputFps();
+
+ // release the unbound textures and deleted buffers.
+ mCanvas.deleteRecycledResources();
+
+ // reset texture upload limit
+ UploadedTexture.resetUploadLimit();
+
+ mRenderRequested = false;
+
+ if ((mFlags & FLAG_NEED_LAYOUT) != 0) layoutContentPane();
+
+ // OpenGL seems having a bug causing us not being able to reset the
+ // scissor box in "onSurfaceChanged()". We have to do it in the second
+ // onDrawFrame().
+ if (mClipRetryCount > 0) {
+ --mClipRetryCount;
+ Rect clip = mClipRect;
+ gl.glScissor(clip.left, clip.top, clip.width(), clip.height());
+ }
+
+ mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis());
+ if (mContentView != null) {
+ mContentView.render(mCanvas);
+ }
+
+ if (!mAnimations.isEmpty()) {
+ long now = SystemClock.uptimeMillis();
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).setStartTime(now);
+ }
+ mAnimations.clear();
+ }
+
+ if (UploadedTexture.uploadLimitReached()) {
+ requestRender();
+ }
+
+ synchronized (mIdleListeners) {
+ if (!mRenderRequested && !mIdleListeners.isEmpty()) {
+ mIdleRunner.enable();
+ }
+ }
+
+ if (DEBUG_INVALIDATE) {
+ mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+ mInvalidateColor = ~mInvalidateColor;
+ }
+
+ if (DEBUG_DRAWING_STAT) {
+ mCanvas.dumpStatisticsAndClear();
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mInDownState = false;
+ } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ mRenderLock.lock();
+ try {
+ // If this has been detached from root, we don't need to handle event
+ boolean handled = mContentView != null
+ && mContentView.dispatchTouchEvent(event);
+ if (action == MotionEvent.ACTION_DOWN && handled) {
+ mInDownState = true;
+ }
+ return handled;
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ public DisplayMetrics getDisplayMetrics() {
+ if (mDisplayMetrics == null) {
+ mDisplayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay().getMetrics(mDisplayMetrics);
+ }
+ return mDisplayMetrics;
+ }
+
+ public GLCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ private class IdleRunner implements Runnable {
+ // true if the idle runner is in the queue
+ private boolean mActive = false;
+
+ @Override
+ public void run() {
+ OnGLIdleListener listener;
+ synchronized (mIdleListeners) {
+ mActive = false;
+ if (mRenderRequested) return;
+ if (mIdleListeners.isEmpty()) return;
+ listener = mIdleListeners.removeFirst();
+ }
+ mRenderLock.lock();
+ try {
+ if (!listener.onGLIdle(GLRootView.this, mCanvas)) return;
+ } finally {
+ mRenderLock.unlock();
+ }
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ enable();
+ }
+ }
+
+ public void enable() {
+ // Who gets the flag can add it to the queue
+ if (mActive) return;
+ mActive = true;
+ queueEvent(this);
+ }
+ }
+
+ @Override
+ public void lockRenderThread() {
+ mRenderLock.lock();
+ }
+
+ @Override
+ public void unlockRenderThread() {
+ mRenderLock.unlock();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
new file mode 100644
index 000000000..c59327831
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -0,0 +1,431 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+
+// GLView is a UI component. It can render to a GLCanvas and accept touch
+// events. A GLView may have zero or more child GLView and they form a tree
+// structure. The rendering and event handling will pass through the tree
+// structure.
+//
+// A GLView tree should be attached to a GLRoot before event dispatching and
+// rendering happens. GLView asks GLRoot to re-render or re-layout the
+// GLView hierarchy using requestRender() and requestLayoutContentPane().
+//
+// The render() method is called in a separate thread. Before calling
+// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
+// rendering thread running at the same time. If there are other entry points
+// from main thread (like a Handler) in your GLView, you need to call
+// lockRendering() if the rendering thread should not run at the same time.
+//
+public class GLView {
+ private static final String TAG = "GLView";
+
+ public static final int VISIBLE = 0;
+ public static final int INVISIBLE = 1;
+
+ private static final int FLAG_INVISIBLE = 1;
+ private static final int FLAG_SET_MEASURED_SIZE = 2;
+ private static final int FLAG_LAYOUT_REQUESTED = 4;
+
+ protected final Rect mBounds = new Rect();
+ protected final Rect mPaddings = new Rect();
+
+ private GLRoot mRoot;
+ protected GLView mParent;
+ private ArrayList<GLView> mComponents;
+ private GLView mMotionTarget;
+
+ private CanvasAnimation mAnimation;
+
+ private int mViewFlags = 0;
+
+ protected int mMeasuredWidth = 0;
+ protected int mMeasuredHeight = 0;
+
+ private int mLastWidthSpec = -1;
+ private int mLastHeightSpec = -1;
+
+ protected int mScrollY = 0;
+ protected int mScrollX = 0;
+ protected int mScrollHeight = 0;
+ protected int mScrollWidth = 0;
+
+ public void startAnimation(CanvasAnimation animation) {
+ GLRoot root = getGLRoot();
+ if (root == null) throw new IllegalStateException();
+
+ mAnimation = animation;
+ mAnimation.start();
+ root.registerLaunchedAnimation(mAnimation);
+ invalidate();
+ }
+
+ // Sets the visiblity of this GLView (either GLView.VISIBLE or
+ // GLView.INVISIBLE).
+ public void setVisibility(int visibility) {
+ if (visibility == getVisibility()) return;
+ if (visibility == VISIBLE) {
+ mViewFlags &= ~FLAG_INVISIBLE;
+ } else {
+ mViewFlags |= FLAG_INVISIBLE;
+ }
+ onVisibilityChanged(visibility);
+ invalidate();
+ }
+
+ // Returns GLView.VISIBLE or GLView.INVISIBLE
+ public int getVisibility() {
+ return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void attachToRoot(GLRoot root) {
+ Utils.assertTrue(mParent == null && mRoot == null);
+ onAttachToRoot(root);
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void detachFromRoot() {
+ Utils.assertTrue(mParent == null && mRoot != null);
+ onDetachFromRoot();
+ }
+
+ // Returns the number of children of the GLView.
+ public int getComponentCount() {
+ return mComponents == null ? 0 : mComponents.size();
+ }
+
+ // Returns the children for the given index.
+ public GLView getComponent(int index) {
+ if (mComponents == null) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mComponents.get(index);
+ }
+
+ // Adds a child to this GLView.
+ public void addComponent(GLView component) {
+ // Make sure the component doesn't have a parent currently.
+ if (component.mParent != null) throw new IllegalStateException();
+
+ // Build parent-child links
+ if (mComponents == null) {
+ mComponents = new ArrayList<GLView>();
+ }
+ mComponents.add(component);
+ component.mParent = this;
+
+ // If this is added after we have a root, tell the component.
+ if (mRoot != null) {
+ component.onAttachToRoot(mRoot);
+ }
+ }
+
+ // Removes a child from this GLView.
+ public boolean removeComponent(GLView component) {
+ if (mComponents == null) return false;
+ if (mComponents.remove(component)) {
+ removeOneComponent(component);
+ return true;
+ }
+ return false;
+ }
+
+ // Removes all children of this GLView.
+ public void removeAllComponents() {
+ for (int i = 0, n = mComponents.size(); i < n; ++i) {
+ removeOneComponent(mComponents.get(i));
+ }
+ mComponents.clear();
+ }
+
+ private void removeOneComponent(GLView component) {
+ if (mMotionTarget == component) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+ component.onDetachFromRoot();
+ component.mParent = null;
+ }
+
+ public Rect bounds() {
+ return mBounds;
+ }
+
+ public int getWidth() {
+ return mBounds.right - mBounds.left;
+ }
+
+ public int getHeight() {
+ return mBounds.bottom - mBounds.top;
+ }
+
+ public GLRoot getGLRoot() {
+ return mRoot;
+ }
+
+ // Request re-rendering of the view hierarchy.
+ // This is used for animation or when the contents changed.
+ public void invalidate() {
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestRender();
+ }
+
+ // Request re-layout of the view hierarchy.
+ public void requestLayout() {
+ mViewFlags |= FLAG_LAYOUT_REQUESTED;
+ mLastHeightSpec = -1;
+ mLastWidthSpec = -1;
+ if (mParent != null) {
+ mParent.requestLayout();
+ } else {
+ // Is this a content pane ?
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestLayoutContentPane();
+ }
+ }
+
+ protected void render(GLCanvas canvas) {
+ renderBackground(canvas);
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ renderChild(canvas, getComponent(i));
+ }
+ }
+
+ protected void renderBackground(GLCanvas view) {
+ }
+
+ protected void renderChild(GLCanvas canvas, GLView component) {
+ if (component.getVisibility() != GLView.VISIBLE
+ && component.mAnimation == null) return;
+
+ int xoffset = component.mBounds.left - mScrollX;
+ int yoffset = component.mBounds.top - mScrollY;
+
+ canvas.translate(xoffset, yoffset, 0);
+
+ CanvasAnimation anim = component.mAnimation;
+ if (anim != null) {
+ canvas.save(anim.getCanvasSaveFlags());
+ if (anim.calculate(canvas.currentAnimationTimeMillis())) {
+ invalidate();
+ } else {
+ component.mAnimation = null;
+ }
+ anim.apply(canvas);
+ }
+ component.render(canvas);
+ if (anim != null) canvas.restore();
+ canvas.translate(-xoffset, -yoffset, 0);
+ }
+
+ protected boolean onTouch(MotionEvent event) {
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event,
+ int x, int y, GLView component, boolean checkBounds) {
+ Rect rect = component.mBounds;
+ int left = rect.left;
+ int top = rect.top;
+ if (!checkBounds || rect.contains(x, y)) {
+ event.offsetLocation(-left, -top);
+ if (component.dispatchTouchEvent(event)) {
+ event.offsetLocation(left, top);
+ return true;
+ }
+ event.offsetLocation(left, top);
+ }
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int action = event.getAction();
+ if (mMotionTarget != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ MotionEvent cancel = MotionEvent.obtain(event);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
+ mMotionTarget = null;
+ } else {
+ dispatchTouchEvent(event, x, y, mMotionTarget, false);
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mMotionTarget = null;
+ }
+ return true;
+ }
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ // in the reverse rendering order
+ for (int i = getComponentCount() - 1; i >= 0; --i) {
+ GLView component = getComponent(i);
+ if (component.getVisibility() != GLView.VISIBLE) continue;
+ if (dispatchTouchEvent(event, x, y, component, true)) {
+ mMotionTarget = component;
+ return true;
+ }
+ }
+ }
+ return onTouch(event);
+ }
+
+ public Rect getPaddings() {
+ return mPaddings;
+ }
+
+ public void setPaddings(Rect paddings) {
+ mPaddings.set(paddings);
+ }
+
+ public void setPaddings(int left, int top, int right, int bottom) {
+ mPaddings.set(left, top, right, bottom);
+ }
+
+ public void layout(int left, int top, int right, int bottom) {
+ boolean sizeChanged = setBounds(left, top, right, bottom);
+ if (sizeChanged) {
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ onLayout(true, left, top, right, bottom);
+ } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) {
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ onLayout(false, left, top, right, bottom);
+ }
+ }
+
+ private boolean setBounds(int left, int top, int right, int bottom) {
+ boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
+ || (bottom - top) != (mBounds.bottom - mBounds.top);
+ mBounds.set(left, top, right, bottom);
+ return sizeChanged;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
+ && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
+ return;
+ }
+
+ mLastWidthSpec = widthSpec;
+ mLastHeightSpec = heightSpec;
+
+ mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
+ onMeasure(widthSpec, heightSpec);
+ if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
+ throw new IllegalStateException(getClass().getName()
+ + " should call setMeasuredSize() in onMeasure()");
+ }
+ }
+
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mViewFlags |= FLAG_SET_MEASURED_SIZE;
+ mMeasuredWidth = width;
+ mMeasuredHeight = height;
+ }
+
+ public int getMeasuredWidth() {
+ return mMeasuredWidth;
+ }
+
+ public int getMeasuredHeight() {
+ return mMeasuredHeight;
+ }
+
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Gets the bounds of the given descendant that relative to this view.
+ */
+ public boolean getBoundsOf(GLView descendant, Rect out) {
+ int xoffset = 0;
+ int yoffset = 0;
+ GLView view = descendant;
+ while (view != this) {
+ if (view == null) return false;
+ Rect bounds = view.mBounds;
+ xoffset += bounds.left;
+ yoffset += bounds.top;
+ view = view.mParent;
+ }
+ out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
+ yoffset + descendant.getHeight());
+ return true;
+ }
+
+ protected void onVisibilityChanged(int visibility) {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ GLView child = getComponent(i);
+ if (child.getVisibility() == GLView.VISIBLE) {
+ child.onVisibilityChanged(visibility);
+ }
+ }
+ }
+
+ protected void onAttachToRoot(GLRoot root) {
+ mRoot = root;
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onAttachToRoot(root);
+ }
+ }
+
+ protected void onDetachFromRoot() {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onDetachFromRoot();
+ }
+ mRoot = null;
+ }
+
+ public void lockRendering() {
+ if (mRoot != null) {
+ mRoot.lockRenderThread();
+ }
+ }
+
+ public void unlockRendering() {
+ if (mRoot != null) {
+ mRoot.unlockRenderThread();
+ }
+ }
+
+ // This is for debugging only.
+ // Dump the view hierarchy into log.
+ void dumpTree(String prefix) {
+ Log.d(TAG, prefix + getClass().getSimpleName());
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).dumpTree(prefix + "....");
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
new file mode 100644
index 000000000..1d50d43f7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ui;
+
+import android.opengl.GLSurfaceView.EGLConfigChooser;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLDisplay;
+
+/*
+ * The code is copied/adapted from
+ * <code>android.opengl.GLSurfaceView.BaseConfigChooser</code>. Here we try to
+ * choose a configuration that support RGBA_8888 format and if possible,
+ * with stencil buffer, but is not required.
+ */
+class GalleryEGLConfigChooser implements EGLConfigChooser {
+
+ private static final String TAG = "GalleryEGLConfigChooser";
+ private int mStencilBits;
+
+ private final int mConfigSpec[] = new int[] {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_ALPHA_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+
+ public int getStencilBits() {
+ return mStencilBits;
+ }
+
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ int[] numConfig = new int[1];
+ if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) {
+ throw new RuntimeException("eglChooseConfig failed");
+ }
+
+ if (numConfig[0] <= 0) {
+ throw new RuntimeException("No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfig[0]];
+ if (!egl.eglChooseConfig(display,
+ mConfigSpec, configs, configs.length, numConfig)) {
+ throw new RuntimeException();
+ }
+
+ return chooseConfig(egl, display, configs);
+ }
+
+ private EGLConfig chooseConfig(
+ EGL10 egl, EGLDisplay display, EGLConfig configs[]) {
+
+ EGLConfig result = null;
+ int minStencil = Integer.MAX_VALUE;
+ int value[] = new int[1];
+
+ // Because we need only one bit of stencil, try to choose a config that
+ // has stencil support but with smallest number of stencil bits. If
+ // none is found, choose any one.
+ for (int i = 0, n = configs.length; i < n; ++i) {
+ if (egl.eglGetConfigAttrib(
+ display, configs[i], EGL10.EGL_RED_SIZE, value)) {
+ // Filter out ARGB 8888 configs.
+ if (value[0] == 8) continue;
+ }
+ if (egl.eglGetConfigAttrib(
+ display, configs[i], EGL10.EGL_STENCIL_SIZE, value)) {
+ if (value[0] == 0) continue;
+ if (value[0] < minStencil) {
+ minStencil = value[0];
+ result = configs[i];
+ }
+ } else {
+ throw new RuntimeException(
+ "eglGetConfigAttrib error: " + egl.eglGetError());
+ }
+ }
+ if (result == null) result = configs[0];
+ egl.eglGetConfigAttrib(
+ display, result, EGL10.EGL_STENCIL_SIZE, value);
+ mStencilBits = value[0];
+ logConfig(egl, display, result);
+ return result;
+ }
+
+ private static final int[] ATTR_ID = {
+ EGL10.EGL_RED_SIZE,
+ EGL10.EGL_GREEN_SIZE,
+ EGL10.EGL_BLUE_SIZE,
+ EGL10.EGL_ALPHA_SIZE,
+ EGL10.EGL_DEPTH_SIZE,
+ EGL10.EGL_STENCIL_SIZE,
+ EGL10.EGL_CONFIG_ID,
+ EGL10.EGL_CONFIG_CAVEAT
+ };
+
+ private static final String[] ATTR_NAME = {
+ "R", "G", "B", "A", "D", "S", "ID", "CAVEAT"
+ };
+
+ private void logConfig(EGL10 egl, EGLDisplay display, EGLConfig config) {
+ int value[] = new int[1];
+ StringBuilder sb = new StringBuilder();
+ for (int j = 0; j < ATTR_ID.length; j++) {
+ egl.eglGetConfigAttrib(display, config, ATTR_ID[j], value);
+ sb.append(ATTR_NAME[j] + value[0] + " ");
+ }
+ Log.i(TAG, "Config chosen: " + sb.toString());
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java
new file mode 100644
index 000000000..54b175cb4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GridDrawer.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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+import android.graphics.Color;
+
+public class GridDrawer extends IconDrawer {
+ private final NinePatchTexture mFrame;
+ private final NinePatchTexture mFrameSelected;
+ private final NinePatchTexture mFrameSelectedTop;
+ private final NinePatchTexture mImportBackground;
+ private Texture mImportLabel;
+ private int mGridWidth;
+ private final SelectionManager mSelectionManager;
+ private final Context mContext;
+ private final int FONT_SIZE = 14;
+ private final int FONT_COLOR = Color.WHITE;
+ private final int IMPORT_LABEL_PADDING = 10;
+ private boolean mSelectionMode;
+
+ public GridDrawer(Context context, SelectionManager selectionManager) {
+ super(context);
+ mContext = context;
+ mFrame = new NinePatchTexture(context, R.drawable.album_frame);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
+ mImportBackground = new NinePatchTexture(context, R.drawable.import_translucent);
+ mSelectionManager = selectionManager;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ NinePatchTexture frame;
+ if (mSelectionMode && mSelectionManager.isItemSelected(path)) {
+ frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
+ } else {
+ frame = mFrame;
+ }
+
+ drawFrame(canvas, frame, x, y, width, height);
+
+ if (topIndex == 0) {
+ ResourceTexture icon = getIcon(dataSourceType);
+ if (icon != null) {
+ IconDimension id = getIconDimension(icon, width, height);
+ if (dataSourceType == DATASOURCE_TYPE_MTP) {
+ if (mImportLabel == null || mGridWidth != width) {
+ mGridWidth = width;
+ mImportLabel = MultiLineTexture.newInstance(
+ mContext.getString(R.string.click_import),
+ width - id.width - IMPORT_LABEL_PADDING, FONT_SIZE, FONT_COLOR);
+ }
+ int bgHeight = Math.max(id.height, mImportLabel.getHeight());
+ mImportBackground.setSize(width, bgHeight);
+ mImportBackground.draw(canvas, x, -y - bgHeight);
+ mImportLabel.draw(canvas, x + id.width + IMPORT_LABEL_PADDING,
+ -y - bgHeight + Math.abs(bgHeight - mImportLabel.getHeight()) / 2);
+ }
+ icon.draw(canvas, id.x, id.y, id.width, id.height);
+ }
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java
new file mode 100644
index 000000000..9d5868bcb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/HighlightDrawer.java
@@ -0,0 +1,73 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+
+public class HighlightDrawer extends IconDrawer {
+ private final NinePatchTexture mFrame;
+ private final NinePatchTexture mFrameSelected;
+ private final NinePatchTexture mFrameSelectedTop;
+ private SelectionManager mSelectionManager;
+ private Path mHighlightItem;
+
+ public HighlightDrawer(Context context) {
+ super(context);
+ mFrame = new NinePatchTexture(context, R.drawable.album_frame);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
+ }
+
+ public void setHighlightItem(Path item) {
+ mHighlightItem = item;
+ }
+
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ NinePatchTexture frame;
+ if (path == mHighlightItem) {
+ frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
+ } else {
+ frame = mFrame;
+ }
+
+ drawFrame(canvas, frame, x, y, width, height);
+
+ if (topIndex == 0) {
+ drawIcon(canvas, width, height, dataSourceType);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Icon.java b/src/com/android/gallery3d/ui/Icon.java
new file mode 100644
index 000000000..c710859f8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Icon.java
@@ -0,0 +1,59 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class Icon extends GLView {
+ private final BasicTexture mIcon;
+
+ // The width and height requested by the user.
+ private int mReqWidth;
+ private int mReqHeight;
+
+ public Icon(Context context, int iconId, int width, int height) {
+ this(context, new ResourceTexture(context, iconId), width, height);
+ }
+
+ public Icon(Context context, BasicTexture icon, int width, int height) {
+ mIcon = icon;
+ mReqWidth = width;
+ mReqHeight = height;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(mReqWidth, mReqHeight)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ // Draw the icon in the center of the space
+ int xoffset = p.left + (width - mReqWidth) / 2;
+ int yoffset = p.top + (height - mReqHeight) / 2;
+
+ mIcon.draw(canvas, xoffset, yoffset, mReqWidth, mReqHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java
new file mode 100644
index 000000000..91732d338
--- /dev/null
+++ b/src/com/android/gallery3d/ui/IconDrawer.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaObject;
+
+import android.content.Context;
+
+public abstract class IconDrawer extends SelectionDrawer {
+ private final String TAG = "IconDrawer";
+ private final ResourceTexture mLocalSetIcon;
+ private final ResourceTexture mCameraIcon;
+ private final ResourceTexture mPicasaIcon;
+ private final ResourceTexture mMtpIcon;
+ private final Texture mVideoOverlay;
+ private final Texture mVideoPlayIcon;
+
+ public static class IconDimension {
+ int x;
+ int y;
+ int width;
+ int height;
+ }
+
+ public IconDrawer(Context context) {
+ mLocalSetIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_folder_holo);
+ mCameraIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_camera_holo);
+ mPicasaIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_picassa_holo);
+ mMtpIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_ptp_holo);
+ mVideoOverlay = new ResourceTexture(context,
+ R.drawable.thumbnail_album_video_overlay_holo);
+ mVideoPlayIcon = new ResourceTexture(context,
+ R.drawable.videooverlay);
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ protected IconDimension drawIcon(GLCanvas canvas, int width, int height,
+ int dataSourceType) {
+ ResourceTexture icon = getIcon(dataSourceType);
+
+ if (icon != null) {
+ IconDimension id = getIconDimension(icon, width, height);
+ icon.draw(canvas, id.x, id.y, id.width, id.height);
+ return id;
+ }
+ return null;
+ }
+
+ protected ResourceTexture getIcon(int dataSourceType) {
+ ResourceTexture icon = null;
+ switch (dataSourceType) {
+ case DATASOURCE_TYPE_LOCAL:
+ icon = mLocalSetIcon;
+ break;
+ case DATASOURCE_TYPE_PICASA:
+ icon = mPicasaIcon;
+ break;
+ case DATASOURCE_TYPE_CAMERA:
+ icon = mCameraIcon;
+ break;
+ case DATASOURCE_TYPE_MTP:
+ icon = mMtpIcon;
+ break;
+ default:
+ break;
+ }
+
+ return icon;
+ }
+
+ protected IconDimension getIconDimension(ResourceTexture icon, int width,
+ int height) {
+ IconDimension id = new IconDimension();
+ float scale = 0.25f * width / icon.getWidth();
+ id.width = (int) (scale * icon.getWidth());
+ id.height = (int) (scale * icon.getHeight());
+ id.x = -width / 2;
+ id.y = height / 2 - id.height;
+ return id;
+ }
+
+ protected void drawVideoOverlay(GLCanvas canvas, int mediaType,
+ int x, int y, int width, int height, int topIndex) {
+ if (mediaType != MediaObject.MEDIA_TYPE_VIDEO) return;
+ mVideoOverlay.draw(canvas, x, y, width, height);
+ if (topIndex == 0) {
+ int side = Math.min(width, height) / 6;
+ mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side);
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ImportCompleteListener.java b/src/com/android/gallery3d/ui/ImportCompleteListener.java
new file mode 100644
index 000000000..5c52ea135
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ImportCompleteListener.java
@@ -0,0 +1,57 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPage;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.Toast;
+
+public class ImportCompleteListener implements MenuExecutor.ProgressListener {
+ private GalleryActivity mActivity;
+
+ public ImportCompleteListener(GalleryActivity galleryActivity) {
+ mActivity = galleryActivity;
+ }
+
+ public void onProgressComplete(int result) {
+ int message;
+ if (result == MenuExecutor.EXECUTION_RESULT_SUCCESS) {
+ message = R.string.import_complete;
+ goToImportedAlbum();
+ } else {
+ message = R.string.import_fail;
+ }
+ Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ public void onProgressUpdate(int index) {
+ }
+
+ private void goToImportedAlbum() {
+ String pathOfImportedAlbum = "/local/all/" + MediaSetUtils.IMPORTED_BUCKET_ID;
+ Bundle data = new Bundle();
+ data.putString(AlbumPage.KEY_MEDIA_PATH, pathOfImportedAlbum);
+ mActivity.getStateManager().startState(AlbumPage.class, data);
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/Label.java b/src/com/android/gallery3d/ui/Label.java
new file mode 100644
index 000000000..6a70a1895
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Label.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+
+public class Label extends GLView {
+ private static final String TAG = "Label";
+ public static final int NULL_ID = 0;
+
+ private static final int FONT_SIZE = 18;
+ private static final int FONT_COLOR = Color.WHITE;
+
+ private String mText;
+ private StringTexture mTexture;
+ private int mFontSize, mFontColor;
+
+ public Label(Context context, int stringId,
+ int fontSize, int fontColor) {
+ this(context, context.getString(stringId), fontSize, fontColor);
+ }
+
+ public Label(Context context, int stringId) {
+ this(context, stringId, FONT_SIZE, FONT_COLOR);
+ }
+
+ public Label(Context context, String text) {
+ this(context, text, FONT_SIZE, FONT_COLOR);
+ }
+
+ public Label(Context context, String text, int fontSize, int fontColor) {
+ //TODO: cut the text if it is too long
+ mText = text;
+ mTexture = StringTexture.newInstance(text, fontSize, fontColor);
+ mFontSize = fontSize;
+ mFontColor = fontColor;
+ }
+
+ public void setText(String text) {
+ if (!mText.equals(text)) {
+ mText = text;
+ mTexture = StringTexture.newInstance(text, mFontSize, mFontColor);
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int width = mTexture.getWidth();
+ int height = mTexture.getHeight();
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(width, height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ int xoffset = p.left + (width - mTexture.getWidth()) / 2;
+ int yoffset = p.top + (height - mTexture.getHeight()) / 2;
+
+ mTexture.draw(canvas, xoffset, yoffset);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java
new file mode 100644
index 000000000..32adc98eb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/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.ui;
+
+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/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
new file mode 100644
index 000000000..cf1e39e24
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+
+public class ManageCacheDrawer extends IconDrawer {
+ private static final int COLOR_CACHING_BACKGROUND = 0x7F000000;
+ private static final int ICON_SIZE = 36;
+ private final NinePatchTexture mFrame;
+ private final ResourceTexture mCheckedItem;
+ private final ResourceTexture mUnCheckedItem;
+ private final SelectionManager mSelectionManager;
+
+ private final ResourceTexture mLocalAlbumIcon;
+ private final StringTexture mCaching;
+
+ public ManageCacheDrawer(Context context, SelectionManager selectionManager) {
+ super(context);
+ mFrame = new NinePatchTexture(context, R.drawable.manage_frame);
+ mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark);
+ mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark);
+ mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark);
+ String cachingLabel = context.getString(R.string.caching_label);
+ mCaching = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+ mSelectionManager = selectionManager;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ private static boolean isLocal(int dataSourceType) {
+ return dataSourceType != DATASOURCE_TYPE_PICASA;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ boolean selected = mSelectionManager.isItemSelected(path);
+ boolean chooseToCache = wantCache ^ selected;
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ drawFrame(canvas, mFrame, x, y, width, height);
+
+ if (topIndex == 0) {
+ drawIcon(canvas, width, height, dataSourceType);
+ }
+
+ if (topIndex == 0) {
+ ResourceTexture icon = null;
+ if (isLocal(dataSourceType)) {
+ icon = mLocalAlbumIcon;
+ } else if (chooseToCache) {
+ icon = mCheckedItem;
+ } else {
+ icon = mUnCheckedItem;
+ }
+
+ int w = ICON_SIZE;
+ int h = ICON_SIZE;
+ x = width / 2 - w / 2;
+ y = -height / 2 - h / 2;
+
+ icon.draw(canvas, x, y, w, h);
+
+ if (isCaching) {
+ int textWidth = mCaching.getWidth();
+ int textHeight = mCaching.getHeight();
+ x = -textWidth / 2;
+ y = height / 2 - textHeight;
+
+ // Leave a few pixels of margin in the background rect.
+ float sideMargin = Utils.clamp(textWidth * 0.1f, 2.0f,
+ 6.0f);
+ float clearance = Utils.clamp(textHeight * 0.1f, 2.0f,
+ 6.0f);
+
+ // Overlay the "Caching" wording at the bottom-center of the content.
+ canvas.fillRect(x - sideMargin, y - clearance,
+ textWidth + sideMargin * 2, textHeight + clearance,
+ COLOR_CACHING_BACKGROUND);
+ mCaching.draw(canvas, x, y);
+ }
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java
new file mode 100644
index 000000000..f65dc10b3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MeasureHelper.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ui;
+
+import android.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+class MeasureHelper {
+
+ private static MeasureHelper sInstance = new MeasureHelper(null);
+
+ private GLView mComponent;
+ private int mPreferredWidth;
+ private int mPreferredHeight;
+
+ private MeasureHelper(GLView component) {
+ mComponent = component;
+ }
+
+ public static MeasureHelper getInstance(GLView component) {
+ sInstance.mComponent = component;
+ return sInstance;
+ }
+
+ public MeasureHelper setPreferredContentSize(int width, int height) {
+ mPreferredWidth = width;
+ mPreferredHeight = height;
+ return this;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ Rect p = mComponent.getPaddings();
+ setMeasuredSize(
+ getLength(widthSpec, mPreferredWidth + p.left + p.right),
+ getLength(heightSpec, mPreferredHeight + p.top + p.bottom));
+ }
+
+ private static int getLength(int measureSpec, int prefered) {
+ int specLength = MeasureSpec.getSize(measureSpec);
+ switch(MeasureSpec.getMode(measureSpec)) {
+ case MeasureSpec.EXACTLY: return specLength;
+ case MeasureSpec.AT_MOST: return Math.min(prefered, specLength);
+ default: return prefered;
+ }
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mComponent.setMeasuredSize(width, height);
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
new file mode 100644
index 000000000..710ddc422
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MenuExecutor.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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.CropImage;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class MenuExecutor {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MenuExecutor";
+
+ private static final int MSG_TASK_COMPLETE = 1;
+ private static final int MSG_TASK_UPDATE = 2;
+ private static final int MSG_DO_SHARE = 3;
+
+ public static final int EXECUTION_RESULT_SUCCESS = 1;
+ public static final int EXECUTION_RESULT_FAIL = 2;
+ public static final int EXECUTION_RESULT_CANCEL = 3;
+
+ private ProgressDialog mDialog;
+ private Future<?> mTask;
+
+ private final GalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ private final Handler mHandler;
+
+ private static ProgressDialog showProgressDialog(
+ Context context, int titleId, int progressMax) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(titleId);
+ dialog.setMax(progressMax);
+ dialog.setCancelable(false);
+ dialog.setIndeterminate(false);
+ if (progressMax > 1) {
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ }
+ dialog.show();
+ return dialog;
+ }
+
+ public interface ProgressListener {
+ public void onProgressUpdate(int index);
+ public void onProgressComplete(int result);
+ }
+
+ public MenuExecutor(
+ GalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TASK_COMPLETE: {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressComplete(message.arg1);
+ }
+ mSelectionManager.leaveSelectionMode();
+ break;
+ }
+ case MSG_TASK_UPDATE: {
+ if (mDialog != null) mDialog.setProgress(message.arg1);
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressUpdate(message.arg1);
+ }
+ break;
+ }
+ case MSG_DO_SHARE: {
+ ((Activity) mActivity).startActivity((Intent) message.obj);
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ if (mTask != null) {
+ mTask.cancel();
+ mTask.waitDone();
+ mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ }
+
+ private void onProgressUpdate(int index, ProgressListener listener) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener));
+ }
+
+ private void onProgressComplete(int result, ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener));
+ }
+
+ private int getShareType(SelectionManager selectionManager) {
+ ArrayList<Path> items = selectionManager.getSelected(false);
+ int type = 0;
+ DataManager dataManager = mActivity.getDataManager();
+ for (Path id : items) {
+ type |= dataManager.getMediaType(id);
+ }
+ return type;
+ }
+
+ private void onShareItemClicked(final SelectionManager selectionManager,
+ final String mimeType, final ComponentName component) {
+ Utils.assertTrue(mDialog == null);
+ final ArrayList<Path> items = selectionManager.getSelected(true);
+ mDialog = showProgressDialog((Activity) mActivity,
+ R.string.loading_image, items.size());
+
+ mTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ @Override
+ public Void run(JobContext jc) {
+ DataManager manager = mActivity.getDataManager();
+ ArrayList<Uri> uris = new ArrayList<Uri>(items.size());
+ int index = 0;
+ for (Path path : items) {
+ if ((manager.getSupportedOperations(path)
+ & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ onProgressUpdate(++index, null);
+ }
+ if (jc.isCancelled()) return null;
+ Intent intent = new Intent()
+ .setComponent(component).setType(mimeType);
+ if (uris.isEmpty()) {
+ return null;
+ } else if (uris.size() == 1) {
+ intent.setAction(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ } else {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ }
+ onProgressComplete(EXECUTION_RESULT_SUCCESS, null);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_DO_SHARE, intent));
+ return null;
+ }
+ }, null);
+ }
+
+ private static void setMenuItemVisibility(
+ Menu menu, int id, boolean visibility) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) item.setVisible(visibility);
+ }
+
+ public static void updateMenuOperation(Menu menu, int supported) {
+ boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0;
+ boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0;
+ boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0;
+ boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0;
+ boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0;
+ boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0;
+ boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0;
+ boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0;
+ boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0;
+ boolean supportImport = (supported & MediaObject.SUPPORT_IMPORT) != 0;
+
+ setMenuItemVisibility(menu, R.id.action_delete, supportDelete);
+ setMenuItemVisibility(menu, R.id.action_rotate_ccw, supportRotate);
+ setMenuItemVisibility(menu, R.id.action_rotate_cw, supportRotate);
+ setMenuItemVisibility(menu, R.id.action_crop, supportCrop);
+ setMenuItemVisibility(menu, R.id.action_share, supportShare);
+ setMenuItemVisibility(menu, R.id.action_setas, supportSetAs);
+ setMenuItemVisibility(menu, R.id.action_show_on_map, supportShowOnMap);
+ setMenuItemVisibility(menu, R.id.action_edit, supportEdit);
+ setMenuItemVisibility(menu, R.id.action_details, supportInfo);
+ setMenuItemVisibility(menu, R.id.action_import, supportImport);
+ }
+
+ private Path getSingleSelectedPath() {
+ ArrayList<Path> ids = mSelectionManager.getSelected(true);
+ Utils.assertTrue(ids.size() == 1);
+ return ids.get(0);
+ }
+
+ public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) {
+ int title;
+ DataManager manager = mActivity.getDataManager();
+ int action = menuItem.getItemId();
+ switch (action) {
+ case R.id.action_select_all:
+ if (mSelectionManager.inSelectAllMode()) {
+ mSelectionManager.deSelectAll();
+ } else {
+ mSelectionManager.selectAll();
+ }
+ return true;
+ case R.id.action_crop: {
+ Path path = getSingleSelectedPath();
+ String mimeType = getMimeType(manager.getMediaType(path));
+ Intent intent = new Intent(CropImage.ACTION_CROP)
+ .setDataAndType(manager.getContentUri(path), mimeType);
+ ((Activity) mActivity).startActivity(intent);
+ return true;
+ }
+ case R.id.action_setas: {
+ Path path = getSingleSelectedPath();
+ int type = manager.getMediaType(path);
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+ String mimeType = getMimeType(type);
+ intent.setDataAndType(manager.getContentUri(path), mimeType);
+ intent.putExtra("mimeType", mimeType);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Activity activity = (Activity) mActivity;
+ activity.startActivity(Intent.createChooser(
+ intent, activity.getString(R.string.set_as)));
+ return true;
+ }
+ case R.id.action_confirm_delete:
+ title = R.string.delete;
+ break;
+ case R.id.action_rotate_cw:
+ title = R.string.rotate_right;
+ break;
+ case R.id.action_rotate_ccw:
+ title = R.string.rotate_left;
+ break;
+ case R.id.action_show_on_map:
+ title = R.string.show_on_map;
+ break;
+ case R.id.action_edit:
+ title = R.string.edit;
+ break;
+ case R.id.action_import:
+ title = R.string.Import;
+ break;
+ default:
+ return false;
+ }
+ startAction(action, title, listener);
+ return true;
+ }
+
+ public void startAction(int action, int title, ProgressListener listener) {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ Utils.assertTrue(mDialog == null);
+
+ Activity activity = (Activity) mActivity;
+ mDialog = showProgressDialog(activity, title, ids.size());
+ MediaOperation operation = new MediaOperation(action, ids, listener);
+ mTask = mActivity.getThreadPool().submit(operation, null);
+ }
+
+ public static String getMimeType(int type) {
+ switch (type) {
+ case MediaObject.MEDIA_TYPE_IMAGE :
+ return "image/*";
+ case MediaObject.MEDIA_TYPE_VIDEO :
+ return "video/*";
+ default: return "*/*";
+ }
+ }
+
+ private boolean execute(
+ DataManager manager, JobContext jc, int cmd, Path path) {
+ boolean result = true;
+ switch (cmd) {
+ case R.id.action_confirm_delete:
+ manager.delete(path);
+ break;
+ case R.id.action_rotate_cw:
+ manager.rotate(path, 90);
+ break;
+ case R.id.action_rotate_ccw:
+ manager.rotate(path, -90);
+ break;
+ case R.id.action_toggle_full_caching: {
+ MediaObject obj = manager.getMediaObject(path);
+ int cacheFlag = obj.getCacheFlag();
+ if (cacheFlag == MediaObject.CACHE_FLAG_FULL) {
+ cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL;
+ } else {
+ cacheFlag = MediaObject.CACHE_FLAG_FULL;
+ }
+ obj.cache(cacheFlag);
+ break;
+ }
+ case R.id.action_show_on_map: {
+ MediaItem item = (MediaItem) manager.getMediaObject(path);
+ double latlng[] = new double[2];
+ item.getLatLong(latlng);
+ if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) {
+ GalleryUtils.showOnMap((Context) mActivity, latlng[0], latlng[1]);
+ }
+ break;
+ }
+ case R.id.action_import: {
+ MediaObject obj = manager.getMediaObject(path);
+ result = obj.Import();
+ break;
+ }
+ case R.id.action_edit: {
+ Activity activity = (Activity) mActivity;
+ MediaItem item = (MediaItem) manager.getMediaObject(path);
+ try {
+ activity.startActivity(Intent.createChooser(
+ new Intent(Intent.ACTION_EDIT)
+ .setData(item.getContentUri())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
+ null));
+ } catch (Throwable t) {
+ Log.w(TAG, "failed to start edit activity: ", t);
+ Toast.makeText(activity,
+ activity.getString(R.string.activity_not_found),
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+ return result;
+ }
+
+ private class MediaOperation implements Job<Void> {
+ private final ArrayList<Path> mItems;
+ private final int mOperation;
+ private final ProgressListener mListener;
+
+ public MediaOperation(int operation, ArrayList<Path> items, ProgressListener listener) {
+ mOperation = operation;
+ mItems = items;
+ mListener = listener;
+ }
+
+ public Void run(JobContext jc) {
+ int index = 0;
+ DataManager manager = mActivity.getDataManager();
+ int result = EXECUTION_RESULT_SUCCESS;
+ for (Path id : mItems) {
+ if (jc.isCancelled()) {
+ result = EXECUTION_RESULT_CANCEL;
+ break;
+ }
+ try {
+ if (!execute(manager, jc, mOperation, id)) result = EXECUTION_RESULT_FAIL;
+ } catch (Throwable th) {
+ Log.e(TAG, "failed to execute operation " + mOperation
+ + " for " + id, th);
+ }
+ onProgressUpdate(index++, mListener);
+ }
+ onProgressComplete(result, mListener);
+ return null;
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/ui/MultiLineTexture.java b/src/com/android/gallery3d/ui/MultiLineTexture.java
new file mode 100644
index 000000000..be62d59c0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MultiLineTexture.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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+
+// MultiLineTexture is a texture shows the content of a specified String.
+//
+// To create a MultiLineTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+class MultiLineTexture extends CanvasTexture {
+ private final Layout mLayout;
+
+ private MultiLineTexture(Layout layout) {
+ super(layout.getWidth(), layout.getHeight());
+ mLayout = layout;
+ }
+
+ public static MultiLineTexture newInstance(
+ String text, int maxWidth, float textSize, int color) {
+ TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
+ Layout layout = new StaticLayout(text, 0, text.length(), paint,
+ maxWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, true, null, 0);
+
+ return new MultiLineTexture(layout);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ mLayout.draw(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/NinePatchChunk.java b/src/com/android/gallery3d/ui/NinePatchChunk.java
new file mode 100644
index 000000000..61bf22c33
--- /dev/null
+++ b/src/com/android/gallery3d/ui/NinePatchChunk.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ui;
+
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+// See "frameworks/base/include/utils/ResourceTypes.h" for the format of
+// NinePatch chunk.
+class NinePatchChunk {
+
+ public static final int NO_COLOR = 0x00000001;
+ public static final int TRANSPARENT_COLOR = 0x00000000;
+
+ public Rect mPaddings = new Rect();
+
+ public int mDivX[];
+ public int mDivY[];
+ public int mColor[];
+
+ private static void readIntArray(int[] data, ByteBuffer buffer) {
+ for (int i = 0, n = data.length; i < n; ++i) {
+ data[i] = buffer.getInt();
+ }
+ }
+
+ private static void checkDivCount(int length) {
+ if (length == 0 || (length & 0x01) != 0) {
+ throw new RuntimeException("invalid nine-patch: " + length);
+ }
+ }
+
+ public static NinePatchChunk deserialize(byte[] data) {
+ ByteBuffer byteBuffer =
+ ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
+
+ byte wasSerialized = byteBuffer.get();
+ if (wasSerialized == 0) return null;
+
+ NinePatchChunk chunk = new NinePatchChunk();
+ chunk.mDivX = new int[byteBuffer.get()];
+ chunk.mDivY = new int[byteBuffer.get()];
+ chunk.mColor = new int[byteBuffer.get()];
+
+ checkDivCount(chunk.mDivX.length);
+ checkDivCount(chunk.mDivY.length);
+
+ // skip 8 bytes
+ byteBuffer.getInt();
+ byteBuffer.getInt();
+
+ chunk.mPaddings.left = byteBuffer.getInt();
+ chunk.mPaddings.right = byteBuffer.getInt();
+ chunk.mPaddings.top = byteBuffer.getInt();
+ chunk.mPaddings.bottom = byteBuffer.getInt();
+
+ // skip 4 bytes
+ byteBuffer.getInt();
+
+ readIntArray(chunk.mDivX, byteBuffer);
+ readIntArray(chunk.mDivY, byteBuffer);
+ readIntArray(chunk.mColor, byteBuffer);
+
+ return chunk;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java
new file mode 100644
index 000000000..15b057a92
--- /dev/null
+++ b/src/com/android/gallery3d/ui/NinePatchTexture.java
@@ -0,0 +1,401 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.microedition.khronos.opengles.GL11;
+
+// NinePatchTexture is a texture backed by a NinePatch resource.
+//
+// getPaddings() returns paddings specified in the NinePatch.
+// getNinePatchChunk() returns the layout data specified in the NinePatch.
+//
+public class NinePatchTexture extends ResourceTexture {
+ @SuppressWarnings("unused")
+ private static final String TAG = "NinePatchTexture";
+ private NinePatchChunk mChunk;
+ private MyCacheMap<Long, NinePatchInstance> mInstanceCache =
+ new MyCacheMap<Long, NinePatchInstance>();
+
+ public NinePatchTexture(Context context, int resId) {
+ super(context, resId);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ if (mBitmap != null) return mBitmap;
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ mBitmap = bitmap;
+ setSize(bitmap.getWidth(), bitmap.getHeight());
+ byte[] chunkData = bitmap.getNinePatchChunk();
+ mChunk = chunkData == null
+ ? null
+ : NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
+ if (mChunk == null) {
+ throw new RuntimeException("invalid nine-patch image: " + mResId);
+ }
+ return bitmap;
+ }
+
+ public Rect getPaddings() {
+ // get the paddings from nine patch
+ if (mChunk == null) onGetBitmap();
+ return mChunk.mPaddings;
+ }
+
+ public NinePatchChunk getNinePatchChunk() {
+ if (mChunk == null) onGetBitmap();
+ return mChunk;
+ }
+
+ private static class MyCacheMap<K, V> extends LinkedHashMap<K, V> {
+ private int CACHE_SIZE = 16;
+ private V mJustRemoved;
+
+ public MyCacheMap() {
+ super(4, 0.75f, true);
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ if (size() > CACHE_SIZE) {
+ mJustRemoved = eldest.getValue();
+ return true;
+ }
+ return false;
+ }
+
+ public V getJustRemoved() {
+ V result = mJustRemoved;
+ mJustRemoved = null;
+ return result;
+ }
+ }
+
+ private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
+ long key = w;
+ key = (key << 32) | h;
+ NinePatchInstance instance = mInstanceCache.get(key);
+
+ if (instance == null) {
+ instance = new NinePatchInstance(this, w, h);
+ mInstanceCache.put(key, instance);
+ NinePatchInstance removed = mInstanceCache.getJustRemoved();
+ if (removed != null) {
+ removed.recycle(canvas);
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (!isLoaded(canvas)) {
+ mInstanceCache.clear();
+ }
+
+ if (w != 0 && h != 0) {
+ findInstance(canvas, w, h).draw(canvas, this, x, y);
+ }
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+ if (canvas == null) return;
+ for (NinePatchInstance instance : mInstanceCache.values()) {
+ instance.recycle(canvas);
+ }
+ mInstanceCache.clear();
+ }
+}
+
+// This keeps data for a specialization of NinePatchTexture with the size
+// (width, height). We pre-compute the coordinates for efficiency.
+class NinePatchInstance {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "NinePatchInstance";
+
+ // We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
+ private static final int VERTEX_BUFFER_SIZE = 16 * 2;
+
+ // We need 22 indices for a normal nine-patch image, plus 2 for each
+ // transparent region. Current there are at most 1 transparent region.
+ private static final int INDEX_BUFFER_SIZE = 22 + 2;
+
+ private FloatBuffer mXyBuffer;
+ private FloatBuffer mUvBuffer;
+ private ByteBuffer mIndexBuffer;
+
+ // Names for buffer names: xy, uv, index.
+ private int[] mBufferNames;
+
+ private int mIdxCount;
+
+ public NinePatchInstance(NinePatchTexture tex, int width, int height) {
+ NinePatchChunk chunk = tex.getNinePatchChunk();
+
+ if (width <= 0 || height <= 0) {
+ throw new RuntimeException("invalid dimension");
+ }
+
+ // The code should be easily extended to handle the general cases by
+ // allocating more space for buffers. But let's just handle the only
+ // use case.
+ if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
+ throw new RuntimeException("unsupported nine patch");
+ }
+
+ float divX[] = new float[4];
+ float divY[] = new float[4];
+ float divU[] = new float[4];
+ float divV[] = new float[4];
+
+ int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
+ int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
+
+ prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
+ }
+
+ /**
+ * Stretches the texture according to the nine-patch rules. It will
+ * linearly distribute the strechy parts defined in the nine-patch chunk to
+ * the target area.
+ *
+ * <pre>
+ * source
+ * /--------------^---------------\
+ * u0 u1 u2 u3 u4 u5
+ * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+ * | div0 div1 div2 div3 |
+ * | | / / / /
+ * | | / / / /
+ * | | / / / /
+ * |fffff|ssss|fff|sss|ffff| ---> x
+ * x0 x1 x2 x3 x4 x5
+ * \----------v------------/
+ * target
+ *
+ * f: fixed segment
+ * s: stretchy segment
+ * </pre>
+ *
+ * @param div the stretch parts defined in nine-patch chunk
+ * @param source the length of the texture
+ * @param target the length on the drawing plan
+ * @param u output, the positions of these dividers in the texture
+ * coordinate
+ * @param x output, the corresponding position of these dividers on the
+ * drawing plan
+ * @return the number of these dividers.
+ */
+ private static int stretch(
+ float x[], float u[], int div[], int source, int target) {
+ int textureSize = Utils.nextPowerOf2(source);
+ float textureBound = (float) source / textureSize;
+
+ float stretch = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ stretch += div[i + 1] - div[i];
+ }
+
+ float remaining = target - source + stretch;
+
+ float lastX = 0;
+ float lastU = 0;
+
+ x[0] = 0;
+ u[0] = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ // Make the stretchy segment a little smaller to prevent sampling
+ // on neighboring fixed segments.
+ // fixed segment
+ x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
+ u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
+
+ // stretchy segment
+ float partU = div[i + 1] - div[i];
+ float partX = remaining * partU / stretch;
+ remaining -= partX;
+ stretch -= partU;
+
+ lastX = x[i + 1] + partX;
+ lastU = div[i + 1];
+ x[i + 2] = lastX - 0.5f;
+ u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
+ }
+ // the last fixed segment
+ x[div.length + 1] = target;
+ u[div.length + 1] = textureBound;
+
+ // remove segments with length 0.
+ int last = 0;
+ for (int i = 1, n = div.length + 2; i < n; ++i) {
+ if ((x[i] - x[last]) < 1f) continue;
+ x[++last] = x[i];
+ u[last] = u[i];
+ }
+ return last + 1;
+ }
+
+ private void prepareVertexData(float x[], float y[], float u[], float v[],
+ int nx, int ny, int[] color) {
+ /*
+ * Given a 3x3 nine-patch image, the vertex order is defined as the
+ * following graph:
+ *
+ * (0) (1) (2) (3)
+ * | /| /| /|
+ * | / | / | / |
+ * (4) (5) (6) (7)
+ * | \ | \ | \ |
+ * | \| \| \|
+ * (8) (9) (A) (B)
+ * | /| /| /|
+ * | / | / | / |
+ * (C) (D) (E) (F)
+ *
+ * And we draw the triangle strip in the following index order:
+ *
+ * index: 04152637B6A5948C9DAEBF
+ */
+ int pntCount = 0;
+ float xy[] = new float[VERTEX_BUFFER_SIZE];
+ float uv[] = new float[VERTEX_BUFFER_SIZE];
+ for (int j = 0; j < ny; ++j) {
+ for (int i = 0; i < nx; ++i) {
+ int xIndex = (pntCount++) << 1;
+ int yIndex = xIndex + 1;
+ xy[xIndex] = x[i];
+ xy[yIndex] = y[j];
+ uv[xIndex] = u[i];
+ uv[yIndex] = v[j];
+ }
+ }
+
+ int idxCount = 1;
+ boolean isForward = false;
+ byte index[] = new byte[INDEX_BUFFER_SIZE];
+ for (int row = 0; row < ny - 1; row++) {
+ --idxCount;
+ isForward = !isForward;
+
+ int start, end, inc;
+ if (isForward) {
+ start = 0;
+ end = nx;
+ inc = 1;
+ } else {
+ start = nx - 1;
+ end = -1;
+ inc = -1;
+ }
+
+ for (int col = start; col != end; col += inc) {
+ int k = row * nx + col;
+ if (col != start) {
+ int colorIdx = row * (nx - 1) + col;
+ if (isForward) colorIdx--;
+ if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
+ index[idxCount] = index[idxCount - 1];
+ ++idxCount;
+ index[idxCount++] = (byte) k;
+ }
+ }
+
+ index[idxCount++] = (byte) k;
+ index[idxCount++] = (byte) (k + nx);
+ }
+ }
+
+ mIdxCount = idxCount;
+
+ int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
+ mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
+
+ mXyBuffer.put(xy, 0, pntCount * 2).position(0);
+ mUvBuffer.put(uv, 0, pntCount * 2).position(0);
+ mIndexBuffer.put(index, 0, idxCount).position(0);
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ private void prepareBuffers(GLCanvas canvas) {
+ mBufferNames = new int[3];
+ GL11 gl = canvas.getGLInstance();
+ gl.glGenBuffers(3, mBufferNames, 0);
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ mXyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ mXyBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[1]);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ mUvBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ mUvBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mBufferNames[2]);
+ gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER,
+ mIndexBuffer.capacity(),
+ mIndexBuffer, GL11.GL_STATIC_DRAW);
+
+ // These buffers are never used again.
+ mXyBuffer = null;
+ mUvBuffer = null;
+ mIndexBuffer = null;
+ }
+
+ public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
+ if (mBufferNames == null) {
+ prepareBuffers(canvas);
+ }
+ canvas.drawMesh(tex, x, y, mBufferNames[0], mBufferNames[1],
+ mBufferNames[2], mIdxCount);
+ }
+
+ public void recycle(GLCanvas canvas) {
+ if (mBufferNames != null) {
+ canvas.deleteBuffer(mBufferNames[0]);
+ canvas.deleteBuffer(mBufferNames[1]);
+ canvas.deleteBuffer(mBufferNames[2]);
+ mBufferNames = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/OnSelectedListener.java b/src/com/android/gallery3d/ui/OnSelectedListener.java
new file mode 100644
index 000000000..2cc5809bf
--- /dev/null
+++ b/src/com/android/gallery3d/ui/OnSelectedListener.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.ui;
+
+public interface OnSelectedListener {
+ public void onSelected(GLView source);
+}
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
new file mode 100644
index 000000000..641fc2c8e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.opengl.Matrix;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+// This class does the overscroll effect.
+class Paper {
+ private static final String TAG = "Paper";
+ private static final int ROTATE_FACTOR = 4;
+ private OverscrollAnimation mAnimationLeft = new OverscrollAnimation();
+ private OverscrollAnimation mAnimationRight = new OverscrollAnimation();
+ private int mWidth, mHeight;
+ private float[] mMatrix = new float[16];
+
+ public void overScroll(float distance) {
+ if (distance < 0) {
+ mAnimationLeft.scroll(-distance);
+ } else {
+ mAnimationRight.scroll(distance);
+ }
+ }
+
+ public boolean advanceAnimation(long currentTimeMillis) {
+ return mAnimationLeft.advanceAnimation(currentTimeMillis)
+ | mAnimationRight.advanceAnimation(currentTimeMillis);
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public float[] getTransform(Position target, Position base,
+ float scrollX, float scrollY) {
+ float left = mAnimationLeft.getValue();
+ float right = mAnimationRight.getValue();
+ float screenX = target.x - scrollX;
+ float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth);
+ // compress t to the range (-1, 1) by the function
+ // f(t) = (1 / (1 + e^-t) - 0.5) * 2
+ // then multiply by 90 to make the range (-45, 45)
+ float degrees =
+ (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45;
+ Matrix.setIdentityM(mMatrix, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0, base.x, base.y, base.z);
+ Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0,
+ target.x - base.x, target.y - base.y, target.z - base.z);
+ return mMatrix;
+ }
+}
+
+class OverscrollAnimation {
+ private static final String TAG = "OverscrollAnimation";
+ private static final long START_ANIMATION = -1;
+ private static final long NO_ANIMATION = -2;
+ private static final long ANIMATION_DURATION = 500;
+
+ private long mAnimationStartTime = NO_ANIMATION;
+ private float mVelocity;
+ private float mCurrentValue;
+
+ public void scroll(float distance) {
+ mAnimationStartTime = START_ANIMATION;
+ mCurrentValue += distance;
+ }
+
+ public boolean advanceAnimation(long currentTimeMillis) {
+ if (mAnimationStartTime == NO_ANIMATION) return false;
+ if (mAnimationStartTime == START_ANIMATION) {
+ mAnimationStartTime = currentTimeMillis;
+ return true;
+ }
+
+ long deltaTime = currentTimeMillis - mAnimationStartTime;
+ float t = deltaTime / 100f;
+ mCurrentValue *= Math.pow(0.5f, t);
+ mAnimationStartTime = currentTimeMillis;
+
+ if (mCurrentValue < 1) {
+ mAnimationStartTime = NO_ANIMATION;
+ mCurrentValue = 0;
+ return false;
+ }
+ return true;
+ }
+
+ public float getValue() {
+ return mCurrentValue;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
new file mode 100644
index 000000000..aba572b00
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -0,0 +1,1191 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+public class PhotoView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoView";
+
+ public static final int INVALID_SIZE = -1;
+
+ private static final int MSG_TRANSITION_COMPLETE = 1;
+ private static final int MSG_SHOW_LOADING = 2;
+
+ private static final long DELAY_SHOW_LOADING = 250; // 250ms;
+
+ private static final int TRANS_NONE = 0;
+ private static final int TRANS_SWITCH_NEXT = 3;
+ private static final int TRANS_SWITCH_PREVIOUS = 4;
+
+ public static final int TRANS_SLIDE_IN_RIGHT = 1;
+ public static final int TRANS_SLIDE_IN_LEFT = 2;
+ public static final int TRANS_OPEN_ANIMATION = 5;
+
+ private static final int LOADING_INIT = 0;
+ private static final int LOADING_TIMEOUT = 1;
+ private static final int LOADING_COMPLETE = 2;
+ private static final int LOADING_FAIL = 3;
+
+ private static final int ENTRY_PREVIOUS = 0;
+ private static final int ENTRY_NEXT = 1;
+
+ private static final int IMAGE_GAP = 96;
+ private static final int SWITCH_THRESHOLD = 256;
+ private static final float SWIPE_THRESHOLD = 300f;
+
+ private static final float DEFAULT_TEXT_SIZE = 20;
+
+ // We try to scale up the image to fill the screen. But in order not to
+ // scale too much for small icons, we limit the max up-scaling factor here.
+ private static final float SCALE_LIMIT = 4;
+
+ public interface PhotoTapListener {
+ public void onSingleTapUp(int x, int y);
+ }
+
+ // the previous/next image entries
+ private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
+
+ private final ScaleGestureDetector mScaleDetector;
+ private final GestureDetector mGestureDetector;
+ private final DownUpDetector mDownUpDetector;
+
+ private PhotoTapListener mPhotoTapListener;
+
+ private final PositionController mPositionController;
+
+ private Model mModel;
+ private StringTexture mLoadingText;
+ private StringTexture mNoThumbnailText;
+ private int mTransitionMode = TRANS_NONE;
+ private final TileImageView mTileView;
+ private Texture mVideoPlayIcon;
+
+ private boolean mShowVideoPlayIcon;
+ private ProgressSpinner mLoadingSpinner;
+
+ private SynchronizedHandler mHandler;
+
+ private int mLoadingState = LOADING_COMPLETE;
+
+ private RectF mTempRect = new RectF();
+ private float[] mTempPoints = new float[8];
+
+ private int mImageRotation;
+
+ private Path mOpenedItemPath;
+ private GalleryActivity mActivity;
+
+ public PhotoView(GalleryActivity activity) {
+ mActivity = activity;
+ mTileView = new TileImageView(activity);
+ addComponent(mTileView);
+ Context context = activity.getAndroidContext();
+ mLoadingSpinner = new ProgressSpinner(context);
+ mLoadingText = StringTexture.newInstance(
+ context.getString(R.string.loading),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+ mNoThumbnailText = StringTexture.newInstance(
+ context.getString(R.string.no_thumbnail),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TRANSITION_COMPLETE: {
+ onTransitionComplete();
+ break;
+ }
+ case MSG_SHOW_LOADING: {
+ if (mLoadingState == LOADING_INIT) {
+ // We don't need the opening animation
+ mOpenedItemPath = null;
+
+ mLoadingSpinner.startAnimation();
+ mLoadingState = LOADING_TIMEOUT;
+ invalidate();
+ }
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+
+ mGestureDetector = new GestureDetector(context,
+ new MyGestureListener(), null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+
+ for (int i = 0, n = mScreenNails.length; i < n; ++i) {
+ mScreenNails[i] = new ScreenNailEntry();
+ }
+
+ mPositionController = new PositionController(this);
+ mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
+ }
+
+
+ public void setModel(Model model) {
+ if (mModel == model) return;
+ mModel = model;
+ mTileView.setModel(model);
+ if (model != null) notifyOnNewImage();
+ }
+
+ public void setPhotoTapListener(PhotoTapListener listener) {
+ mPhotoTapListener = listener;
+ }
+
+ private boolean setTileViewPosition(int centerX, int centerY, float scale) {
+ int inverseX = mPositionController.mImageW - centerX;
+ int inverseY = mPositionController.mImageH - centerY;
+ TileImageView t = mTileView;
+ int rotation = mImageRotation;
+ switch (rotation) {
+ case 0: return t.setPosition(centerX, centerY, scale, 0);
+ case 90: return t.setPosition(centerY, inverseX, scale, 90);
+ case 180: return t.setPosition(inverseX, inverseY, scale, 180);
+ case 270: return t.setPosition(inverseY, centerX, scale, 270);
+ default: throw new IllegalArgumentException(String.valueOf(rotation));
+ }
+ }
+
+ public void setPosition(int centerX, int centerY, float scale) {
+ if (setTileViewPosition(centerX, centerY, scale)) {
+ layoutScreenNails();
+ }
+ }
+
+ private void updateScreenNailEntry(int which, ImageData data) {
+ if (mTransitionMode == TRANS_SWITCH_NEXT
+ || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
+ // ignore screen nail updating during switching
+ return;
+ }
+ ScreenNailEntry entry = mScreenNails[which];
+ if (data == null) {
+ entry.set(false, null, 0);
+ } else {
+ entry.set(true, data.bitmap, data.rotation);
+ }
+ }
+
+ // -1 previous, 0 current, 1 next
+ public void notifyImageInvalidated(int which) {
+ switch (which) {
+ case -1: {
+ updateScreenNailEntry(
+ ENTRY_PREVIOUS, mModel.getPreviousImage());
+ layoutScreenNails();
+ invalidate();
+ break;
+ }
+ case 1: {
+ updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+ layoutScreenNails();
+ invalidate();
+ break;
+ }
+ case 0: {
+ // mImageWidth and mImageHeight will get updated
+ mTileView.notifyModelInvalidated();
+
+ mImageRotation = mModel.getImageRotation();
+ if (((mImageRotation / 90) & 1) == 0) {
+ mPositionController.setImageSize(
+ mTileView.mImageWidth, mTileView.mImageHeight);
+ } else {
+ mPositionController.setImageSize(
+ mTileView.mImageHeight, mTileView.mImageWidth);
+ }
+ updateLoadingState();
+ break;
+ }
+ }
+ }
+
+ private void updateLoadingState() {
+ // Possible transitions of mLoadingState:
+ // INIT --> TIMEOUT, COMPLETE, FAIL
+ // TIMEOUT --> COMPLETE, FAIL, INIT
+ // COMPLETE --> INIT
+ // FAIL --> INIT
+ if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) {
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mLoadingState = LOADING_COMPLETE;
+ } else if (mModel.isFailedToLoad()) {
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mLoadingState = LOADING_FAIL;
+ } else if (mLoadingState != LOADING_INIT) {
+ mLoadingState = LOADING_INIT;
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
+ }
+ }
+
+ public void notifyModelInvalidated() {
+ if (mModel == null) {
+ updateScreenNailEntry(ENTRY_PREVIOUS, null);
+ updateScreenNailEntry(ENTRY_NEXT, null);
+ } else {
+ updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage());
+ updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+ }
+ layoutScreenNails();
+
+ if (mModel == null) {
+ mTileView.notifyModelInvalidated();
+ mImageRotation = 0;
+ mPositionController.setImageSize(0, 0);
+ updateLoadingState();
+ } else {
+ notifyImageInvalidated(0);
+ }
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
+ return true;
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ mTileView.layout(left, top, right, bottom);
+ if (changeSize) {
+ mPositionController.setViewSize(getWidth(), getHeight());
+ for (ScreenNailEntry entry : mScreenNails) {
+ entry.updateDrawingSize();
+ }
+ }
+ }
+
+ private static int gapToSide(int imageWidth, int viewWidth) {
+ return Math.max(0, (viewWidth - imageWidth) / 2);
+ }
+
+ private RectF getImageBounds() {
+ PositionController p = mPositionController;
+ float points[] = mTempPoints;
+
+ /*
+ * (p0,p1)----------(p2,p3)
+ * | |
+ * | |
+ * (p4,p5)----------(p6,p7)
+ */
+ points[0] = points[4] = -p.mCurrentX;
+ points[1] = points[3] = -p.mCurrentY;
+ points[2] = points[6] = p.mImageW - p.mCurrentX;
+ points[5] = points[7] = p.mImageH - p.mCurrentY;
+
+ RectF rect = mTempRect;
+ rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+ Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+
+ float scale = p.mCurrentScale;
+ float offsetX = p.mViewW / 2;
+ float offsetY = p.mViewH / 2;
+ for (int i = 0; i < 4; ++i) {
+ float x = points[i + i] * scale + offsetX;
+ float y = points[i + i + 1] * scale + offsetY;
+ if (x < rect.left) rect.left = x;
+ if (x > rect.right) rect.right = x;
+ if (y < rect.top) rect.top = y;
+ if (y > rect.bottom) rect.bottom = y;
+ }
+ return rect;
+ }
+
+
+ /*
+ * Here is how we layout the screen nails
+ *
+ * previous current next
+ * ___________ ________________ __________
+ * | _______ | | __________ | | ______ |
+ * | | | | | | right->| | | | | |
+ * | | |<-------->|<--left | | | | | |
+ * | |_______| | | | |__________| | | |______| |
+ * |___________| | |________________| |__________|
+ * | <--> gapToSide()
+ * |
+ * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
+ */
+ private void layoutScreenNails() {
+ int width = getWidth();
+ int height = getHeight();
+
+ // Use the image width in AC, since we may fake the size if the
+ // image is unavailable
+ RectF bounds = getImageBounds();
+ int left = Math.round(bounds.left);
+ int right = Math.round(bounds.right);
+ int gap = gapToSide(right - left, width);
+
+ // layout the previous image
+ ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
+
+ if (entry.isEnabled()) {
+ entry.layoutRightEdgeAt(left - (
+ IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ }
+
+ // layout the next image
+ entry = mScreenNails[ENTRY_NEXT];
+ if (entry.isEnabled()) {
+ entry.layoutLeftEdgeAt(right + (
+ IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ }
+ }
+
+ private static class PositionController {
+ private long mAnimationStartTime = NO_ANIMATION;
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ // Animation time in milliseconds.
+ private static final float ANIM_TIME_SCROLL = 0;
+ private static final float ANIM_TIME_SCALE = 50;
+ private static final float ANIM_TIME_SNAPBACK = 600;
+ private static final float ANIM_TIME_SLIDE = 400;
+ private static final float ANIM_TIME_ZOOM = 300;
+
+ private int mAnimationKind;
+ private final static int ANIM_KIND_SCROLL = 0;
+ private final static int ANIM_KIND_SCALE = 1;
+ private final static int ANIM_KIND_SNAPBACK = 2;
+ private final static int ANIM_KIND_SLIDE = 3;
+ private final static int ANIM_KIND_ZOOM = 4;
+
+ private PhotoView mViewer;
+ private int mImageW, mImageH;
+ private int mViewW, mViewH;
+
+ // The X, Y are the coordinate on bitmap which shows on the center of
+ // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual
+ // values used currently.
+ private int mCurrentX, mFromX, mToX;
+ private int mCurrentY, mFromY, mToY;
+ private float mCurrentScale, mFromScale, mToScale;
+
+ // The offsets from the center of the view to the user's focus point,
+ // converted to the bitmap domain.
+ private float mPrevOffsetX;
+ private float mPrevOffsetY;
+ private boolean mInScale;
+ private boolean mUseViewSize = true;
+
+ // The limits for position and scale.
+ private float mScaleMin, mScaleMax = 4f;
+
+ PositionController(PhotoView viewer) {
+ mViewer = viewer;
+ }
+
+ public void setImageSize(int width, int height) {
+
+ // If no image available, use view size.
+ if (width == 0 || height == 0) {
+ mUseViewSize = true;
+ mImageW = mViewW;
+ mImageH = mViewH;
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = 1;
+ mScaleMin = 1;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ return;
+ }
+
+ mUseViewSize = false;
+
+ float ratio = Math.min(
+ (float) mImageW / width, (float) mImageH / height);
+
+ mCurrentX = translate(mCurrentX, mImageW, width, ratio);
+ mCurrentY = translate(mCurrentY, mImageH, height, ratio);
+ mCurrentScale = mCurrentScale * ratio;
+
+ mFromX = translate(mFromX, mImageW, width, ratio);
+ mFromY = translate(mFromY, mImageH, height, ratio);
+ mFromScale = mFromScale * ratio;
+
+ mToX = translate(mToX, mImageW, width, ratio);
+ mToY = translate(mToY, mImageH, height, ratio);
+ mToScale = mToScale * ratio;
+
+ mImageW = width;
+ mImageH = height;
+
+ mScaleMin = getMinimalScale(width, height, 0);
+
+ // Scale the new image to fit into the old one
+ if (mViewer.mOpenedItemPath != null) {
+ Position position = PositionRepository
+ .getInstance(mViewer.mActivity).get(Long.valueOf(
+ System.identityHashCode(mViewer.mOpenedItemPath)));
+ mViewer.mOpenedItemPath = null;
+ if (position != null) {
+ float scale = 240f / Math.min(width, height);
+ mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
+ mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
+ mCurrentScale = scale;
+ mViewer.mTransitionMode = TRANS_OPEN_ANIMATION;
+ startSnapback();
+ }
+ } else if (mAnimationStartTime == NO_ANIMATION) {
+ mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ }
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ if (targetScale > mScaleMax) targetScale = mScaleMax;
+ float scale = mCurrentScale;
+ float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX;
+ float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY;
+
+ // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW
+ // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0
+ float min = mViewW / 2.0f / targetScale;
+ float max = mImageW - mViewW / 2.0f / targetScale;
+ int targetX = (int) Utils.clamp(tempX, min, max);
+
+ min = mViewH / 2.0f / targetScale;
+ max = mImageH - mViewH / 2.0f / targetScale;
+ int targetY = (int) Utils.clamp(tempY, min, max);
+
+ // If the width of the image is less then the view, center the image
+ if (mImageW * targetScale < mViewW) targetX = mImageW / 2;
+ if (mImageH * targetScale < mViewH) targetY = mImageH / 2;
+
+ startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+ }
+
+ public void resetToFullView() {
+ startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ private float getMinimalScale(int w, int h, int rotation) {
+ return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0
+ ? Math.min((float) mViewW / w, (float) mViewH / h)
+ : Math.min((float) mViewW / h, (float) mViewH / w));
+ }
+
+ private static int translate(int value, int size, int updateSize, float ratio) {
+ return Math.round(
+ (value + (updateSize * ratio - size) / 2f) / ratio);
+ }
+
+ public void setViewSize(int viewW, int viewH) {
+ boolean needLayout = mViewW == 0 || mViewH == 0;
+
+ mViewW = viewW;
+ mViewH = viewH;
+
+ if (mUseViewSize) {
+ mImageW = viewW;
+ mImageH = viewH;
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = 1;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ } else {
+ boolean wasMinScale = (mCurrentScale == mScaleMin);
+ mScaleMin = Math.min(SCALE_LIMIT, Math.min(
+ (float) viewW / mImageW, (float) viewH / mImageH));
+ if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = mScaleMin;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+ }
+ }
+
+ public void stopAnimation() {
+ mAnimationStartTime = NO_ANIMATION;
+ }
+
+ public void skipAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) return;
+ mAnimationStartTime = NO_ANIMATION;
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ }
+
+ public void scrollBy(float dx, float dy, int type) {
+ startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
+ getTargetY() + Math.round(dy / mCurrentScale),
+ mCurrentScale, type);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ mInScale = true;
+ mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale;
+ }
+
+ public void scaleBy(float s, float focusX, float focusY) {
+
+ // The focus point should keep this position on the ImageView.
+ // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX.
+ // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY.
+ float offsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ float offsetY = (focusY - mViewH / 2f) / mCurrentScale;
+
+ startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX),
+ getTargetY() - Math.round(offsetY - mPrevOffsetY),
+ getTargetScale() * s, ANIM_KIND_SCALE);
+ mPrevOffsetX = offsetX;
+ mPrevOffsetY = offsetY;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ startSnapbackIfNeeded();
+ }
+
+ public void up() {
+ startSnapback();
+ }
+
+ public void startSlideInAnimation(int fromX) {
+ mFromX = Math.round(fromX + (mImageW - mViewW) / 2f);
+ mFromY = Math.round(mImageH / 2f);
+ mCurrentX = mFromX;
+ mCurrentY = mFromY;
+ startAnimation(mImageW / 2, mImageH / 2, mCurrentScale,
+ ANIM_KIND_SLIDE);
+ }
+
+ public void startHorizontalSlide(int distance) {
+ scrollBy(distance, 0, ANIM_KIND_SLIDE);
+ }
+
+ private void startAnimation(
+ int centerX, int centerY, float scale, int kind) {
+ if (centerX == mCurrentX && centerY == mCurrentY
+ && scale == mCurrentScale) return;
+
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+
+ mToX = centerX;
+ mToY = centerY;
+ mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
+
+ // If the scaled dimension is smaller than the view,
+ // force it to be in the center.
+ if (Math.floor(mImageH * mToScale) <= mViewH) {
+ mToY = mImageH / 2;
+ }
+
+ mAnimationStartTime = SystemClock.uptimeMillis();
+ mAnimationKind = kind;
+ if (advanceAnimation()) mViewer.invalidate();
+ }
+
+ // Returns true if redraw is needed.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ } else if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ if (mViewer.mTransitionMode != TRANS_NONE) {
+ mViewer.mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
+ return false;
+ } else {
+ return startSnapbackIfNeeded();
+ }
+ }
+
+ float animationTime;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ animationTime = ANIM_TIME_SCROLL;
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ animationTime = ANIM_TIME_SCALE;
+ } else if (mAnimationKind == ANIM_KIND_SLIDE) {
+ animationTime = ANIM_TIME_SLIDE;
+ } else if (mAnimationKind == ANIM_KIND_ZOOM) {
+ animationTime = ANIM_TIME_ZOOM;
+ } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ {
+ animationTime = ANIM_TIME_SNAPBACK;
+ }
+
+ float progress;
+ if (animationTime == 0) {
+ progress = 1;
+ } else {
+ long now = SystemClock.uptimeMillis();
+ progress = (now - mAnimationStartTime) / animationTime;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ mAnimationStartTime = LAST_ANIMATION;
+ } else {
+ float f = 1 - progress;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ progress = 1 - f; // linear
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ progress = 1 - f * f; // quadratic
+ } else /* if mAnimationKind is ANIM_KIND_SNAPBACK,
+ ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ {
+ progress = 1 - f * f * f * f * f; // x^5
+ }
+ linearInterpolate(progress);
+ }
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ return true;
+ }
+
+ private void linearInterpolate(float progress) {
+ // To linearly interpolate the position, we have to translate the
+ // coordinates. The meaning of the translated point (x, y) is the
+ // coordinates of the center of the bitmap on the view component.
+ float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale;
+ float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale;
+ float currentX = fromX + progress * (toX - fromX);
+
+ float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale;
+ float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale;
+ float currentY = fromY + progress * (toY - fromY);
+
+ mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+ mCurrentX = Math.round(
+ mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale);
+ mCurrentY = Math.round(
+ mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale);
+ }
+
+ // Returns true if redraw is needed.
+ private boolean startSnapbackIfNeeded() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mInScale) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
+ return false;
+ }
+ return startSnapback();
+ }
+
+ public boolean startSnapback() {
+ boolean needAnimation = false;
+ int x = mCurrentX;
+ int y = mCurrentY;
+ float scale = mCurrentScale;
+
+ if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
+ needAnimation = true;
+ scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ }
+
+ // The number of pixels when the edge is aligned.
+ int left = (int) Math.ceil(mViewW / (2 * scale));
+ int right = mImageW - left;
+ int top = (int) Math.ceil(mViewH / (2 * scale));
+ int bottom = mImageH - top;
+
+ if (mImageW * scale > mViewW) {
+ if (mCurrentX < left) {
+ needAnimation = true;
+ x = left;
+ } else if (mCurrentX > right) {
+ needAnimation = true;
+ x = right;
+ }
+ } else if (mCurrentX != mImageW / 2) {
+ needAnimation = true;
+ x = mImageW / 2;
+ }
+
+ if (mImageH * scale > mViewH) {
+ if (mCurrentY < top) {
+ needAnimation = true;
+ y = top;
+ } else if (mCurrentY > bottom) {
+ needAnimation = true;
+ y = bottom;
+ }
+ } else if (mCurrentY != mImageH / 2) {
+ needAnimation = true;
+ y = mImageH / 2;
+ }
+
+ if (needAnimation) {
+ startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
+ }
+
+ return needAnimation;
+ }
+
+ private float getTargetScale() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
+ return mToScale;
+ }
+
+ private int getTargetX() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
+ return mToX;
+ }
+
+ private int getTargetY() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
+ return mToY;
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ PositionController p = mPositionController;
+
+ // Draw the current photo
+ if (mLoadingState == LOADING_COMPLETE) {
+ super.render(canvas);
+ }
+
+ // Draw the previous and the next photo
+ if (mTransitionMode != TRANS_SLIDE_IN_LEFT
+ && mTransitionMode != TRANS_SLIDE_IN_RIGHT
+ && mTransitionMode != TRANS_OPEN_ANIMATION) {
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+
+ if (prevNail.mVisible) prevNail.draw(canvas);
+ if (nextNail.mVisible) nextNail.draw(canvas);
+ }
+
+ // Draw the progress spinner and the text below it
+ //
+ // (x, y) is where we put the center of the spinner.
+ // s is the size of the video play icon, and we use s to layout text
+ // because we want to keep the text at the same place when the video
+ // play icon is shown instead of the spinner.
+ int w = getWidth();
+ int h = getHeight();
+ int x = Math.round(getImageBounds().centerX());
+ int y = h / 2;
+ int s = Math.min(getWidth(), getHeight()) / 6;
+
+ if (mLoadingState == LOADING_TIMEOUT) {
+ StringTexture m = mLoadingText;
+ ProgressSpinner r = mLoadingSpinner;
+ r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ invalidate(); // we need to keep the spinner rotating
+ } else if (mLoadingState == LOADING_FAIL) {
+ StringTexture m = mNoThumbnailText;
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ }
+
+ // Draw the video play icon (in the place where the spinner was)
+ if (mShowVideoPlayIcon
+ && mLoadingState != LOADING_INIT
+ && mLoadingState != LOADING_TIMEOUT) {
+ mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
+ }
+
+ if (mPositionController.advanceAnimation()) invalidate();
+ }
+
+ private void stopCurrentSwipingIfNeeded() {
+ // Enable fast sweeping
+ if (mTransitionMode == TRANS_SWITCH_NEXT) {
+ mTransitionMode = TRANS_NONE;
+ mPositionController.stopAnimation();
+ switchToNextImage();
+ } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
+ mTransitionMode = TRANS_NONE;
+ mPositionController.stopAnimation();
+ switchToPreviousImage();
+ }
+ }
+
+ private static boolean isAlmostEquals(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
+ }
+
+ private boolean swipeImages(float velocity) {
+ if (mTransitionMode != TRANS_NONE
+ && mTransitionMode != TRANS_SWITCH_NEXT
+ && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
+
+ ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
+ ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
+
+ int width = getWidth();
+
+ // If the edge of the current photo is visible and the sweeping velocity
+ // exceed the threshold, switch to next / previous image
+ PositionController controller = mPositionController;
+ if (isAlmostEquals(controller.mCurrentScale, controller.mScaleMin)) {
+ if (velocity < -SWIPE_THRESHOLD) {
+ stopCurrentSwipingIfNeeded();
+ if (next.isEnabled()) {
+ mTransitionMode = TRANS_SWITCH_NEXT;
+ controller.startHorizontalSlide(next.mOffsetX - width / 2);
+ return true;
+ }
+ return false;
+ }
+ if (velocity > SWIPE_THRESHOLD) {
+ stopCurrentSwipingIfNeeded();
+ if (prev.isEnabled()) {
+ mTransitionMode = TRANS_SWITCH_PREVIOUS;
+ controller.startHorizontalSlide(prev.mOffsetX - width / 2);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ if (mTransitionMode != TRANS_NONE) return false;
+
+ // Decide whether to swiping to the next/prev image in the zoom-in case
+ RectF bounds = getImageBounds();
+ int left = Math.round(bounds.left);
+ int right = Math.round(bounds.right);
+ int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
+
+ // If we have moved the picture a lot, switching.
+ if (next.isEnabled() && threshold < width - right) {
+ mTransitionMode = TRANS_SWITCH_NEXT;
+ controller.startHorizontalSlide(next.mOffsetX - width / 2);
+ return true;
+ }
+ if (prev.isEnabled() && threshold < left) {
+ mTransitionMode = TRANS_SWITCH_PREVIOUS;
+ controller.startHorizontalSlide(prev.mOffsetX - width / 2);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean mIgnoreUpEvent = false;
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ if (mTransitionMode != TRANS_NONE) return true;
+ mPositionController.scrollBy(
+ dx, dy, PositionController.ANIM_KIND_SCROLL);
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (mPhotoTapListener != null) {
+ mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY());
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ mIgnoreUpEvent = true;
+ if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) {
+ mPositionController.up();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mTransitionMode != TRANS_NONE) return true;
+ PositionController controller = mPositionController;
+ float scale = controller.mCurrentScale;
+ // onDoubleTap happened on the second ACTION_DOWN.
+ // We need to ignore the next UP event.
+ mIgnoreUpEvent = true;
+ if (scale <= 1.0f || isAlmostEquals(scale, controller.mScaleMin)) {
+ controller.zoomIn(
+ e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f));
+ } else {
+ controller.resetToFullView();
+ }
+ return true;
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ float scale = detector.getScaleFactor();
+ if (Float.isNaN(scale) || Float.isInfinite(scale)
+ || mTransitionMode != TRANS_NONE) return true;
+ mPositionController.scaleBy(scale,
+ detector.getFocusX(), detector.getFocusY());
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (mTransitionMode != TRANS_NONE) return false;
+ mPositionController.beginScale(
+ detector.getFocusX(), detector.getFocusY());
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mPositionController.endScale();
+ swipeImages(0);
+ }
+ }
+
+ public void notifyOnNewImage() {
+ mPositionController.setImageSize(0, 0);
+ }
+
+ public void startSlideInAnimation(int direction) {
+ PositionController a = mPositionController;
+ a.stopAnimation();
+ switch (direction) {
+ case TRANS_SLIDE_IN_LEFT: {
+ mTransitionMode = TRANS_SLIDE_IN_LEFT;
+ a.startSlideInAnimation(a.mViewW);
+ break;
+ }
+ case TRANS_SLIDE_IN_RIGHT: {
+ mTransitionMode = TRANS_SLIDE_IN_RIGHT;
+ a.startSlideInAnimation(-a.mViewW);
+ break;
+ }
+ default: throw new IllegalArgumentException(String.valueOf(direction));
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ public void onDown(MotionEvent e) {
+ }
+
+ public void onUp(MotionEvent e) {
+ if (mIgnoreUpEvent) {
+ mIgnoreUpEvent = false;
+ return;
+ }
+ if (!swipeImages(0) && mTransitionMode == TRANS_NONE) {
+ mPositionController.up();
+ }
+ }
+ }
+
+ private void switchToNextImage() {
+ // We update the texture here directly to prevent texture uploading.
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+ mTileView.invalidateTiles();
+ if (prevNail.mTexture != null) prevNail.mTexture.recycle();
+ prevNail.mTexture = mTileView.mBackupImage;
+ mTileView.mBackupImage = nextNail.mTexture;
+ nextNail.mTexture = null;
+ mModel.next();
+ }
+
+ private void switchToPreviousImage() {
+ // We update the texture here directly to prevent texture uploading.
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+ mTileView.invalidateTiles();
+ if (nextNail.mTexture != null) nextNail.mTexture.recycle();
+ nextNail.mTexture = mTileView.mBackupImage;
+ mTileView.mBackupImage = prevNail.mTexture;
+ nextNail.mTexture = null;
+ mModel.previous();
+ }
+
+ private void onTransitionComplete() {
+ int mode = mTransitionMode;
+ mTransitionMode = TRANS_NONE;
+
+ if (mModel == null) return;
+ if (mode == TRANS_SWITCH_NEXT) {
+ switchToNextImage();
+ } else if (mode == TRANS_SWITCH_PREVIOUS) {
+ switchToPreviousImage();
+ }
+ }
+
+ private boolean isDown() {
+ return mDownUpDetector.isDown();
+ }
+
+ public static interface Model extends TileImageView.Model {
+ public void next();
+ public void previous();
+ public int getImageRotation();
+
+ // Return null if the specified image is unavailable.
+ public ImageData getNextImage();
+ public ImageData getPreviousImage();
+ }
+
+ public static class ImageData {
+ public int rotation;
+ public Bitmap bitmap;
+
+ public ImageData(Bitmap bitmap, int rotation) {
+ this.bitmap = bitmap;
+ this.rotation = rotation;
+ }
+ }
+
+ private static int getRotated(int degree, int original, int theother) {
+ return ((degree / 90) & 1) == 0 ? original : theother;
+ }
+
+ private class ScreenNailEntry {
+ private boolean mVisible;
+ private boolean mEnabled;
+
+ private int mRotation;
+ private int mDrawWidth;
+ private int mDrawHeight;
+ private int mOffsetX;
+
+ private BitmapTexture mTexture;
+
+ public void set(boolean enabled, Bitmap bitmap, int rotation) {
+ mEnabled = enabled;
+ mRotation = rotation;
+ if (bitmap == null) {
+ if (mTexture != null) mTexture.recycle();
+ mTexture = null;
+ } else {
+ if (mTexture != null) {
+ if (mTexture.getBitmap() != bitmap) {
+ mTexture.recycle();
+ mTexture = new BitmapTexture(bitmap);
+ }
+ } else {
+ mTexture = new BitmapTexture(bitmap);
+ }
+ updateDrawingSize();
+ }
+ }
+
+ public void layoutRightEdgeAt(int x) {
+ mVisible = x > 0;
+ mOffsetX = x - getRotated(
+ mRotation, mDrawWidth, mDrawHeight) / 2;
+ }
+
+ public void layoutLeftEdgeAt(int x) {
+ mVisible = x < getWidth();
+ mOffsetX = x + getRotated(
+ mRotation, mDrawWidth, mDrawHeight) / 2;
+ }
+
+ public int gapToSide() {
+ return ((mRotation / 90) & 1) != 0
+ ? PhotoView.gapToSide(mDrawHeight, getWidth())
+ : PhotoView.gapToSide(mDrawWidth, getWidth());
+ }
+
+ public void updateDrawingSize() {
+ if (mTexture == null) return;
+
+ int width = mTexture.getWidth();
+ int height = mTexture.getHeight();
+ float s = mPositionController.getMinimalScale(width, height, mRotation);
+ mDrawWidth = Math.round(width * s);
+ mDrawHeight = Math.round(height * s);
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void draw(GLCanvas canvas) {
+ int x = mOffsetX;
+ int y = getHeight() / 2;
+
+ if (mTexture != null) {
+ if (mRotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.translate(x, y, 0);
+ canvas.rotate(mRotation, 0, 0, 1); //mRotation
+ canvas.translate(-x, -y, 0);
+ }
+ mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
+ mDrawWidth, mDrawHeight);
+ if (mRotation != 0) {
+ canvas.restore();
+ }
+ }
+ }
+ }
+
+ public void pause() {
+ mPositionController.skipAnimation();
+ mTransitionMode = TRANS_NONE;
+ mTileView.freeTextures();
+ for (ScreenNailEntry entry : mScreenNails) {
+ entry.set(false, null, 0);
+ }
+ }
+
+ public void resume() {
+ mTileView.prepareTextures();
+ }
+
+ public void setOpenedItem(Path itemPath) {
+ mOpenedItemPath = itemPath;
+ }
+
+ public void showVideoPlayIcon(boolean show) {
+ mShowVideoPlayIcon = show;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PositionProvider.java b/src/com/android/gallery3d/ui/PositionProvider.java
new file mode 100644
index 000000000..930c61ee9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionProvider.java
@@ -0,0 +1,23 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+public interface PositionProvider {
+ public Position getPosition(long identity, Position target);
+}
diff --git a/src/com/android/gallery3d/ui/PositionRepository.java b/src/com/android/gallery3d/ui/PositionRepository.java
new file mode 100644
index 000000000..0b829fa25
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionRepository.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+
+import java.util.HashMap;
+import java.util.WeakHashMap;
+
+public class PositionRepository {
+ private static final WeakHashMap<GalleryActivity, PositionRepository>
+ sMap = new WeakHashMap<GalleryActivity, PositionRepository>();
+
+ public static class Position implements Cloneable {
+ public float x;
+ public float y;
+ public float z;
+ public float theta;
+ public float alpha;
+
+ public Position() {
+ }
+
+ public Position(float x, float y, float z) {
+ this(x, y, z, 0f, 1f);
+ }
+
+ public Position(float x, float y, float z, float ftheta, float alpha) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.theta = ftheta;
+ this.alpha = alpha;
+ }
+
+ @Override
+ public Position clone() {
+ try {
+ return (Position) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(); // we do support clone.
+ }
+ }
+
+ public void set(Position another) {
+ x = another.x;
+ y = another.y;
+ z = another.z;
+ theta = another.theta;
+ alpha = another.alpha;
+ }
+
+ public void set(float x, float y, float z, float ftheta, float alpha) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.theta = ftheta;
+ this.alpha = alpha;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof Position)) return false;
+ Position position = (Position) object;
+ return x == position.x && y == position.y && z == position.z
+ && theta == position.theta
+ && alpha == position.alpha;
+ }
+
+ public static void interpolate(
+ Position source, Position target, Position output, float progress) {
+ if (progress < 1f) {
+ output.set(
+ Utils.interpolateScale(source.x, target.x, progress),
+ Utils.interpolateScale(source.y, target.y, progress),
+ Utils.interpolateScale(source.z, target.z, progress),
+ Utils.interpolateAngle(source.theta, target.theta, progress),
+ Utils.interpolateScale(source.alpha, target.alpha, progress));
+ } else {
+ output.set(target);
+ }
+ }
+ }
+
+ public static PositionRepository getInstance(GalleryActivity activity) {
+ PositionRepository repository = sMap.get(activity);
+ if (repository == null) {
+ repository = new PositionRepository();
+ sMap.put(activity, repository);
+ }
+ return repository;
+ }
+
+ private HashMap<Long, Position> mData = new HashMap<Long, Position>();
+ private int mOffsetX;
+ private int mOffsetY;
+ private Position mTempPosition = new Position();
+
+ public Position get(Long identity) {
+ Position position = mData.get(identity);
+ if (position == null) return null;
+ mTempPosition.set(position);
+ position = mTempPosition;
+ position.x -= mOffsetX;
+ position.y -= mOffsetY;
+ return position;
+ }
+
+ public void setOffset(int offsetX, int offsetY) {
+ mOffsetX = offsetX;
+ mOffsetY = offsetY;
+ }
+
+ public void putPosition(Long identity, Position position) {
+ Position clone = position.clone();
+ clone.x += mOffsetX;
+ clone.y += mOffsetY;
+ mData.put(identity, clone);
+ }
+
+ public void clear() {
+ mData.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressBar.java b/src/com/android/gallery3d/ui/ProgressBar.java
new file mode 100644
index 000000000..c62fa9a62
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressBar.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class ProgressBar extends GLView {
+ private final int MAX_PROGRESS = 10000;
+ private int mProgress;
+ private int mSecondaryProgress;
+ private BasicTexture mProgressTexture;
+ private BasicTexture mSecondaryProgressTexture;
+ private BasicTexture mBackgrondTexture;
+
+
+ public ProgressBar(Context context, int resProgress,
+ int resSecondaryProgress, int resBackground) {
+ mProgressTexture = new NinePatchTexture(context, resProgress);
+ mSecondaryProgressTexture = new NinePatchTexture(
+ context, resSecondaryProgress);
+ mBackgrondTexture = new NinePatchTexture(context, resBackground);
+
+ }
+
+ // The progress value is between 0 (empty) and MAX_PROGRESS (full).
+ public void setProgress(int progress) {
+ mProgress = progress;
+ }
+
+ public void setSecondaryProgress(int progress) {
+ mSecondaryProgress = progress;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ int primary = width * mProgress / MAX_PROGRESS;
+ int secondary = width * mSecondaryProgress / MAX_PROGRESS;
+ int x = p.left;
+ int y = p.top;
+
+ canvas.drawTexture(mBackgrondTexture, x, y, width, height);
+ canvas.drawTexture(mProgressTexture, x, y, primary, height);
+ canvas.drawTexture(mSecondaryProgressTexture, x, y, secondary, height);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
new file mode 100644
index 000000000..e4d60242b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+
+public class ProgressSpinner {
+ private static float ROTATE_SPEED_OUTER = 1080f / 3500f;
+ private static float ROTATE_SPEED_INNER = -720f / 3500f;
+ private final ResourceTexture mOuter;
+ private final ResourceTexture mInner;
+ private final int mWidth;
+ private final int mHeight;
+
+ private float mInnerDegree = 0f;
+ private float mOuterDegree = 0f;
+ private long mAnimationTimestamp = -1;
+
+ public ProgressSpinner(Context context) {
+ mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo);
+ mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo);
+
+ mWidth = Math.max(mOuter.getWidth(), mInner.getWidth());
+ mHeight = Math.max(mOuter.getHeight(), mInner.getHeight());
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void startAnimation() {
+ mAnimationTimestamp = -1;
+ mOuterDegree = 0;
+ mInnerDegree = 0;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ long now = canvas.currentAnimationTimeMillis();
+ if (mAnimationTimestamp == -1) mAnimationTimestamp = now;
+ mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER;
+ mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER;
+
+ mAnimationTimestamp = now;
+
+ // just preventing overflow
+ if (mOuterDegree > 360) mOuterDegree -= 360f;
+ if (mInnerDegree < 0) mInnerDegree += 360f;
+
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+ canvas.translate(x + mWidth / 2, y + mHeight / 2, 0);
+ canvas.rotate(mInnerDegree, 0, 0, 1);
+ mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2);
+ canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1);
+ mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2);
+ canvas.restore();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java
new file mode 100644
index 000000000..c1be435d1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RawTexture.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.ui;
+
+import javax.microedition.khronos.opengles.GL11;
+
+// RawTexture is used for texture created by glCopyTexImage2D.
+//
+// It will throw RuntimeException in onBind() if used with a different GL
+// context. It is only used internally by copyTexture() in GLCanvas.
+class RawTexture extends BasicTexture {
+
+ private RawTexture(GLCanvas canvas, int id) {
+ super(canvas, id, STATE_LOADED);
+ }
+
+ public static RawTexture newInstance(GLCanvas canvas) {
+ int[] textureId = new int[1];
+ GL11 gl = canvas.getGLInstance();
+ gl.glGenTextures(1, textureId, 0);
+ return new RawTexture(canvas, textureId[0]);
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ if (mCanvasRef.get() != canvas) {
+ throw new RuntimeException("cannot bind to different canvas");
+ }
+ return true;
+ }
+
+ public boolean isOpaque() {
+ return true;
+ }
+
+ @Override
+ public void yield() {
+ // we cannot free the texture because we have no backup.
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ResourceTexture.java b/src/com/android/gallery3d/ui/ResourceTexture.java
new file mode 100644
index 000000000..08fb89187
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ResourceTexture.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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+// ResourceTexture is a texture whose Bitmap is decoded from a resource.
+// By default ResourceTexture is not opaque.
+public class ResourceTexture extends UploadedTexture {
+
+ protected final Context mContext;
+ protected final int mResId;
+
+ public ResourceTexture(Context context, int resId) {
+ mContext = Utils.checkNotNull(context);
+ mResId = resId;
+ setOpaque(false);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ if (!inFinalizer()) {
+ bitmap.recycle();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
new file mode 100644
index 000000000..7e375c9f7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class ScrollBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ScrollBarView";
+
+ public interface Listener {
+ void onScrollBarPositionChanged(int position);
+ }
+
+ private int mBarHeight;
+
+ private int mGripHeight;
+ private int mGripPosition; // left side of the grip
+ private int mGripWidth; // zero if the grip is disabled
+ private int mGivenGripWidth;
+
+ private int mContentPosition;
+ private int mContentTotal;
+
+ private Listener mListener;
+ private NinePatchTexture mScrollBarTexture;
+
+ public ScrollBarView(Context context, int gripHeight, int gripWidth) {
+ mScrollBarTexture = new NinePatchTexture(
+ context, R.drawable.scrollbar_handle_holo_dark);
+ mGripPosition = 0;
+ mGripWidth = 0;
+ mGivenGripWidth = gripWidth;
+ mGripHeight = gripHeight;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) return;
+ mBarHeight = bottom - top;
+ }
+
+ // The content position is between 0 to "total". The current position is
+ // in "position".
+ public void setContentPosition(int position, int total) {
+ if (position == mContentPosition && total == mContentTotal) {
+ return;
+ }
+
+ invalidate();
+
+ mContentPosition = position;
+ mContentTotal = total;
+
+ // If the grip cannot move, don't draw it.
+ if (mContentTotal <= 0) {
+ mGripPosition = 0;
+ mGripWidth = 0;
+ return;
+ }
+
+ // Map from the content range to scroll bar range.
+ //
+ // mContentTotal --> getWidth() - mGripWidth
+ // mContentPosition --> mGripPosition
+ mGripWidth = mGivenGripWidth;
+ float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+ mGripPosition = Math.round(r * mContentPosition);
+ }
+
+ private void notifyContentPositionFromGrip() {
+ if (mContentTotal <= 0) return;
+ float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+ int newContentPosition = Math.round(mGripPosition / r);
+ mListener.onScrollBarPositionChanged(newContentPosition);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ if (mGripWidth == 0) return;
+ Rect b = bounds();
+ int y = (mBarHeight - mGripHeight) / 2;
+ mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight);
+ }
+
+ // The onTouch() handler is disabled because now we don't want the user
+ // to drag the bar (it's an indicator only).
+ /*
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ int x = (int) event.getX();
+ return (x >= mGripPosition && x < mGripPosition + mGripWidth);
+ }
+ case MotionEvent.ACTION_MOVE: {
+ // Adjust x by mGripWidth / 2 so the center of the grip
+ // matches the touch position.
+ int x = (int) event.getX() - mGripWidth / 2;
+ x = Utils.clamp(x, 0, getWidth() - mGripWidth);
+ if (mGripPosition != x) {
+ mGripPosition = x;
+ notifyContentPositionFromGrip();
+ invalidate();
+ }
+ break;
+ }
+ }
+ return true;
+ }
+ */
+}
diff --git a/src/com/android/gallery3d/ui/ScrollView.java b/src/com/android/gallery3d/ui/ScrollView.java
new file mode 100644
index 000000000..f7628335c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollView.java
@@ -0,0 +1,99 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+// The current implementation can only scroll vertically.
+public class ScrollView extends GLView {
+
+ private static final int MIN_SCROLLER_HEIGHT = 20;
+
+ private NinePatchTexture mScroller;
+ private int mScrollLimit = 0;
+ private int mScrollerHeight = MIN_SCROLLER_HEIGHT;
+ private GestureDetector mGestureDetector;
+
+ public ScrollView(Context context) {
+ mScroller = new NinePatchTexture(context, R.drawable.scrollbar_handle_holo_dark);
+ mGestureDetector = new GestureDetector(context, new MyGestureListener());
+ }
+
+ private GLView getContentView() {
+ return getComponentCount() == 0 ? null : getComponent(0);
+ }
+
+ @Override
+ public void onLayout(boolean sizeChange, int l, int t, int r, int b) {
+ GLView content = getContentView();
+ int width = getWidth();
+ int height = getHeight();
+ content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ int contentHeight = content.getMeasuredHeight();
+ content.layout(0, 0, width, contentHeight);
+ if (height < contentHeight) {
+ mScrollLimit = contentHeight - height;
+ mScrollerHeight = Math.max(MIN_SCROLLER_HEIGHT,
+ height * height / contentHeight);
+ } else {
+ mScrollLimit = 0;
+ }
+ mScrollY = Utils.clamp(mScrollY, 0, mScrollLimit);
+ }
+
+ @Override
+ public void render(GLCanvas canvas) {
+ GLView content = getContentView();
+ if (content == null) return;
+ int width = getWidth();
+ int height = getHeight();
+
+ canvas.save(GLCanvas.SAVE_FLAG_CLIP);
+ canvas.clipRect(0, 0, width, height);
+ super.render(canvas);
+ if (mScrollLimit > 0) {
+ int x = getWidth() - mScroller.getWidth();
+ int y = (height - mScrollerHeight) * mScrollY / mScrollLimit;
+ mScroller.draw(canvas, x, y, mScroller.getWidth(), mScrollerHeight);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public boolean onTouch(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ return true;
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onScroll(MotionEvent e1,
+ MotionEvent e2, float distanceX, float distanceY) {
+ mScrollY = Utils.clamp(mScrollY + (int) distanceY, 0, mScrollLimit);
+ invalidate();
+ return true;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 000000000..9f19cec96
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.content.Context;
+import android.view.ViewConfiguration;
+import android.widget.OverScroller;
+
+public class ScrollerHelper {
+ private OverScroller mScroller;
+ private int mOverflingDistance;
+ private boolean mOverflingEnabled;
+
+ public ScrollerHelper(Context context) {
+ mScroller = new OverScroller(context);
+ ViewConfiguration configuration = ViewConfiguration.get(context);
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ }
+
+ public void setOverfling(boolean enabled) {
+ mOverflingEnabled = enabled;
+ }
+
+ /**
+ * Call this when you want to know the new location. The position will be
+ * updated and can be obtained by getPosition(). Returns true if the
+ * animation is not yet finished.
+ */
+ public boolean advanceAnimation(long currentTimeMillis) {
+ return mScroller.computeScrollOffset();
+ }
+
+ public boolean isFinished() {
+ return mScroller.isFinished();
+ }
+
+ public void forceFinished() {
+ mScroller.forceFinished(true);
+ }
+
+ public int getPosition() {
+ return mScroller.getCurrX();
+ }
+
+ public void setPosition(int position) {
+ mScroller.startScroll(
+ position, 0, // startX, startY
+ 0, 0, 0); // dx, dy, duration
+
+ // This forces the scroller to reach the final position.
+ mScroller.abortAnimation();
+ }
+
+ public void fling(int velocity, int min, int max) {
+ int currX = getPosition();
+ mScroller.fling(
+ currX, 0, // startX, startY
+ velocity, 0, // velocityX, velocityY
+ min, max, // minX, maxX
+ 0, 0, // minY, maxY
+ mOverflingEnabled ? mOverflingDistance : 0, 0);
+ }
+
+ public boolean startScroll(int distance, int min, int max) {
+ int currPosition = mScroller.getCurrX();
+ int finalPosition = mScroller.getFinalX();
+ int newPosition = Utils.clamp(finalPosition + distance, min, max);
+ if (newPosition != currPosition) {
+ mScroller.startScroll(
+ currPosition, 0, // startX, startY
+ newPosition - currPosition, 0, 0); // dx, dy, duration
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java
new file mode 100644
index 000000000..2655a221c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionDrawer.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.ui;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.Path;
+
+import android.graphics.Rect;
+
+/**
+ * Drawer class responsible for drawing selectable frame.
+ */
+public abstract class SelectionDrawer {
+ public static final int DATASOURCE_TYPE_NOT_CATEGORIZED = 0;
+ public static final int DATASOURCE_TYPE_LOCAL = 1;
+ public static final int DATASOURCE_TYPE_PICASA = 2;
+ public static final int DATASOURCE_TYPE_MTP = 3;
+ public static final int DATASOURCE_TYPE_CAMERA = 4;
+
+ public abstract void prepareDrawing();
+ public abstract void draw(GLCanvas canvas, Texture content,
+ int width, int height, int rotation, Path path,
+ int topIndex, int dataSourceType, int mediaType,
+ boolean wantCache, boolean isCaching);
+ public abstract void drawFocus(GLCanvas canvas, int width, int height);
+
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int mediaType) {
+ draw(canvas, content, width, height, rotation, path, 0,
+ DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType,
+ false, false);
+ }
+
+ public static void drawWithRotation(GLCanvas canvas, Texture content,
+ int x, int y, int width, int height, int rotation) {
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.rotate(rotation, 0, 0, 1);
+ }
+
+ content.draw(canvas, x, y, width, height);
+
+ if (rotation != 0) {
+ canvas.restore();
+ }
+ }
+
+ public static void drawWithRotationAndGray(GLCanvas canvas, Texture content,
+ int x, int y, int width, int height, int rotation,
+ int topIndex) {
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.rotate(rotation, 0, 0, 1);
+ }
+
+ if (topIndex > 0 && (content instanceof BasicTexture)) {
+ float ratio = Utils.clamp(0.3f + 0.2f * topIndex, 0f, 1f);
+ canvas.drawMixed((BasicTexture) content, 0xFF222222, ratio,
+ x, y, width, height);
+ } else {
+ content.draw(canvas, x, y, width, height);
+ }
+
+ if (rotation != 0) {
+ canvas.restore();
+ }
+ }
+
+ public static void drawFrame(GLCanvas canvas, NinePatchTexture frame,
+ int x, int y, int width, int height) {
+ Rect p = frame.getPaddings();
+ frame.draw(canvas, x - p.left, y - p.top, width + p.left + p.right,
+ height + p.top + p.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
new file mode 100644
index 000000000..b85ca7a41
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -0,0 +1,221 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryContext;
+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 android.content.Context;
+import android.os.Vibrator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectionManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SelectionManager";
+
+ public static final int ENTER_SELECTION_MODE = 1;
+ public static final int LEAVE_SELECTION_MODE = 2;
+ public static final int SELECT_ALL_MODE = 3;
+
+ private Set<Path> mClickedSet;
+ private MediaSet mSourceMediaSet;
+ private final Vibrator mVibrator;
+ private SelectionListener mListener;
+ private DataManager mDataManager;
+ private boolean mInverseSelection;
+ private boolean mIsAlbumSet;
+ private boolean mInSelectionMode;
+ private boolean mAutoLeave = true;
+ private int mTotal;
+
+ public interface SelectionListener {
+ public void onSelectionModeChange(int mode);
+ public void onSelectionChange(Path path, boolean selected);
+ }
+
+ public SelectionManager(GalleryContext galleryContext, boolean isAlbumSet) {
+ Context context = galleryContext.getAndroidContext();
+ mDataManager = galleryContext.getDataManager();
+ mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ mClickedSet = new HashSet<Path>();
+ mIsAlbumSet = isAlbumSet;
+ mTotal = -1;
+ }
+
+ // Whether we will leave selection mode automatically once the number of
+ // selected items is down to zero.
+ public void setAutoLeaveSelectionMode(boolean enable) {
+ mAutoLeave = enable;
+ }
+
+ public void setSelectionListener(SelectionListener listener) {
+ mListener = listener;
+ }
+
+ public void selectAll() {
+ enterSelectionMode();
+ mInverseSelection = true;
+ mClickedSet.clear();
+ if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE);
+ }
+
+ public void deSelectAll() {
+ leaveSelectionMode();
+ mInverseSelection = false;
+ mClickedSet.clear();
+ }
+
+ public boolean inSelectAllMode() {
+ return mInverseSelection;
+ }
+
+ public boolean inSelectionMode() {
+ return mInSelectionMode;
+ }
+
+ public void enterSelectionMode() {
+ if (mInSelectionMode) return;
+
+ mInSelectionMode = true;
+ mVibrator.vibrate(100);
+ if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE);
+ }
+
+ public void leaveSelectionMode() {
+ if (!mInSelectionMode) return;
+
+ mInSelectionMode = false;
+ mInverseSelection = false;
+ mClickedSet.clear();
+ if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE);
+ }
+
+ public boolean isItemSelected(Path itemId) {
+ return mInverseSelection ^ mClickedSet.contains(itemId);
+ }
+
+ public int getSelectedCount() {
+ int count = mClickedSet.size();
+ if (mInverseSelection) {
+ if (mTotal < 0) {
+ mTotal = mIsAlbumSet
+ ? mSourceMediaSet.getSubMediaSetCount()
+ : mSourceMediaSet.getMediaItemCount();
+ }
+ count = mTotal - count;
+ }
+ return count;
+ }
+
+ public void toggle(Path path) {
+ if (mClickedSet.contains(path)) {
+ mClickedSet.remove(path);
+ } else {
+ enterSelectionMode();
+ mClickedSet.add(path);
+ }
+
+ if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
+ if (getSelectedCount() == 0 && mAutoLeave) {
+ leaveSelectionMode();
+ }
+ }
+
+ private static void expandMediaSet(ArrayList<Path> items, MediaSet set) {
+ int subCount = set.getSubMediaSetCount();
+ for (int i = 0; i < subCount; i++) {
+ expandMediaSet(items, set.getSubMediaSet(i));
+ }
+ int total = set.getMediaItemCount();
+ int batch = 50;
+ int index = 0;
+
+ while (index < total) {
+ int count = index + batch < total
+ ? batch
+ : total - index;
+ ArrayList<MediaItem> list = set.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ items.add(item.getPath());
+ }
+ index += batch;
+ }
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet) {
+ ArrayList<Path> selected = new ArrayList<Path>();
+ if (mIsAlbumSet) {
+ if (mInverseSelection) {
+ int max = mSourceMediaSet.getSubMediaSetCount();
+ for (int i = 0; i < max; i++) {
+ MediaSet set = mSourceMediaSet.getSubMediaSet(i);
+ Path id = set.getPath();
+ if (!mClickedSet.contains(id)) {
+ if (expandSet) {
+ expandMediaSet(selected, set);
+ } else {
+ selected.add(id);
+ }
+ }
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ if (expandSet) {
+ expandMediaSet(selected, mDataManager.getMediaSet(id));
+ } else {
+ selected.add(id);
+ }
+ }
+ }
+ } else {
+ if (mInverseSelection) {
+
+ int total = mSourceMediaSet.getMediaItemCount();
+ int index = 0;
+ while (index < total) {
+ int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
+ ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ Path id = item.getPath();
+ if (!mClickedSet.contains(id)) selected.add(id);
+ }
+ index += count;
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ selected.add(id);
+ }
+ }
+ }
+ return selected;
+ }
+
+ public void setSourceMediaSet(MediaSet set) {
+ mSourceMediaSet = set;
+ mTotal = -1;
+ }
+
+ public MediaSet getSourceMediaSet() {
+ return mSourceMediaSet;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
new file mode 100644
index 000000000..79a6bf080
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -0,0 +1,165 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+import java.util.Random;
+import javax.microedition.khronos.opengles.GL11;
+
+public class SlideshowView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlideshowView";
+
+ private static final int SLIDESHOW_DURATION = 3500;
+ private static final int TRANSITION_DURATION = 1000;
+
+ private static final float SCALE_SPEED = 0.20f ;
+ private static final float MOVE_SPEED = SCALE_SPEED;
+
+ private int mCurrentRotation;
+ private BitmapTexture mCurrentTexture;
+ private SlideshowAnimation mCurrentAnimation;
+
+ private int mPrevRotation;
+ private BitmapTexture mPrevTexture;
+ private SlideshowAnimation mPrevAnimation;
+
+ private final FloatAnimation mTransitionAnimation =
+ new FloatAnimation(0, 1, TRANSITION_DURATION);
+
+ private Random mRandom = new Random();
+
+ public void next(Bitmap bitmap, int rotation) {
+
+ mTransitionAnimation.start();
+
+ if (mPrevTexture != null) {
+ mPrevTexture.getBitmap().recycle();
+ mPrevTexture.recycle();
+ }
+
+ mPrevTexture = mCurrentTexture;
+ mPrevAnimation = mCurrentAnimation;
+ mPrevRotation = mCurrentRotation;
+
+ mCurrentRotation = rotation;
+ mCurrentTexture = new BitmapTexture(bitmap);
+ if (((rotation / 90) & 0x01) == 0) {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getWidth(), mCurrentTexture.getHeight(),
+ mRandom);
+ } else {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getHeight(), mCurrentTexture.getWidth(),
+ mRandom);
+ }
+ mCurrentAnimation.start();
+
+ invalidate();
+ }
+
+ public void release() {
+ if (mPrevTexture != null) {
+ mPrevTexture.recycle();
+ mPrevTexture = null;
+ }
+ if (mCurrentTexture != null) {
+ mCurrentTexture.recycle();
+ mCurrentTexture = null;
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ long currentTimeMillis = canvas.currentAnimationTimeMillis();
+ boolean requestRender = mTransitionAnimation.calculate(currentTimeMillis);
+ GL11 gl = canvas.getGLInstance();
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
+ float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
+
+ if (mPrevTexture != null && alpha != 1f) {
+ requestRender |= mPrevAnimation.calculate(currentTimeMillis);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(1f - alpha);
+ mPrevAnimation.apply(canvas);
+ canvas.rotate(mPrevRotation, 0, 0, 1);
+ mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2,
+ -mPrevTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (mCurrentTexture != null) {
+ requestRender |= mCurrentAnimation.calculate(currentTimeMillis);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(alpha);
+ mCurrentAnimation.apply(canvas);
+ canvas.rotate(mCurrentRotation, 0, 0, 1);
+ mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2,
+ -mCurrentTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (requestRender) invalidate();
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ private class SlideshowAnimation extends CanvasAnimation {
+ private final int mWidth;
+ private final int mHeight;
+
+ private final PointF mMovingVector;
+ private float mProgress;
+
+ public SlideshowAnimation(int width, int height, Random random) {
+ mWidth = width;
+ mHeight = height;
+ mMovingVector = new PointF(
+ MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f),
+ MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f));
+ setDuration(SLIDESHOW_DURATION);
+ }
+
+ @Override
+ public void apply(GLCanvas canvas) {
+ int viewWidth = getWidth();
+ int viewHeight = getHeight();
+
+ float initScale = Math.min(2f, Math.min((float)
+ viewWidth / mWidth, (float) viewHeight / mHeight));
+ float scale = initScale * (1 + SCALE_SPEED * mProgress);
+
+ float centerX = viewWidth / 2 + mMovingVector.x * mProgress;
+ float centerY = viewHeight / 2 + mMovingVector.y * mProgress;
+
+ canvas.translate(centerX, centerY, 0);
+ canvas.scale(scale, scale, 0);
+ }
+
+ @Override
+ public int getCanvasSaveFlags() {
+ return GLCanvas.SAVE_FLAG_MATRIX;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
new file mode 100644
index 000000000..a8ca5f290
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -0,0 +1,607 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.util.LinkedNode;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class SlotView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlotView";
+
+ private static final boolean WIDE = true;
+
+ private static final int INDEX_NONE = -1;
+
+ public interface Listener {
+ public void onSingleTapUp(int index);
+ public void onLongTap(int index);
+ public void onScrollPositionChanged(int position, int total);
+ }
+
+ public static class SimpleListener implements Listener {
+ public void onSingleTapUp(int index) {}
+ public void onLongTap(int index) {}
+ public void onScrollPositionChanged(int position, int total) {}
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScrollerHelper mScroller;
+ private final Paper mPaper = new Paper();
+
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+
+ // Use linked hash map to keep the rendering order
+ private HashMap<DisplayItem, ItemEntry> mItems =
+ new HashMap<DisplayItem, ItemEntry>();
+
+ public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList();
+
+ // This is used for multipass rendering
+ private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>();
+ private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>();
+
+ private boolean mMoreAnimation = false;
+ private MyAnimation mAnimation = null;
+ private final Position mTempPosition = new Position();
+ private final Layout mLayout = new Layout();
+ private PositionProvider mPositions;
+ private int mStartIndex = INDEX_NONE;
+
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ private int mOverscrollEffect = OVERSCROLL_3D;
+
+ public static final int OVERSCROLL_3D = 0;
+ public static final int OVERSCROLL_SYSTEM = 1;
+ public static final int OVERSCROLL_NONE = 2;
+
+ public SlotView(Context context) {
+ mGestureDetector =
+ new GestureDetector(context, new MyGestureListener());
+ mScroller = new ScrollerHelper(context);
+ }
+
+ public void setCenterIndex(int index) {
+ int slotCount = mLayout.mSlotCount;
+ if (index < 0 || index >= slotCount) {
+ return;
+ }
+ Rect rect = mLayout.getSlotRect(index);
+ int position = WIDE
+ ? (rect.left + rect.right - getWidth()) / 2
+ : (rect.top + rect.bottom - getHeight()) / 2;
+ setScrollPosition(position);
+ }
+
+ public void makeSlotVisible(int index) {
+ Rect rect = mLayout.getSlotRect(index);
+ int visibleBegin = WIDE ? mScrollX : mScrollY;
+ int visibleLength = WIDE ? getWidth() : getHeight();
+ int visibleEnd = visibleBegin + visibleLength;
+ int slotBegin = WIDE ? rect.left : rect.top;
+ int slotEnd = WIDE ? rect.right : rect.bottom;
+
+ int position = visibleBegin;
+ if (visibleLength < slotEnd - slotBegin) {
+ position = visibleBegin;
+ } else if (slotBegin < visibleBegin) {
+ position = slotBegin;
+ } else if (slotEnd > visibleEnd) {
+ position = slotEnd - visibleLength;
+ }
+
+ setScrollPosition(position);
+ }
+
+ public void setScrollPosition(int position) {
+ position = Utils.clamp(position, 0, mLayout.getScrollLimit());
+ mScroller.setPosition(position);
+ updateScrollPosition(position, false);
+ }
+
+ public void setSlotSize(int slotWidth, int slotHeight) {
+ mLayout.setSlotSize(slotWidth, slotHeight);
+ }
+
+ @Override
+ public void addComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ if (!changeSize) return;
+ mLayout.setSize(r - l, b - t);
+ onLayoutChanged(r - l, b - t);
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ mPaper.setSize(r - l, b - t);
+ }
+ }
+
+ protected void onLayoutChanged(int width, int height) {
+ }
+
+ public void startTransition(PositionProvider position) {
+ mPositions = position;
+ mAnimation = new MyAnimation();
+ mAnimation.start();
+ if (mItems.size() != 0) invalidate();
+ }
+
+ public void savePositions(PositionRepository repository) {
+ repository.clear();
+ LinkedNode.List<ItemEntry> list = mItemList;
+ ItemEntry entry = list.getFirst();
+ Position position = new Position();
+ while (entry != null) {
+ position.set(entry.target);
+ position.x -= mScrollX;
+ position.y -= mScrollY;
+ repository.putPosition(entry.item.getIdentity(), position);
+ entry = list.nextOf(entry);
+ }
+ }
+
+ private void updateScrollPosition(int position, boolean force) {
+ if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
+ if (WIDE) {
+ mScrollX = position;
+ } else {
+ mScrollY = position;
+ }
+ mLayout.setScrollPosition(position);
+ onScrollPositionChanged(position);
+ }
+
+ protected void onScrollPositionChanged(int newPosition) {
+ int limit = mLayout.getScrollLimit();
+ mListener.onScrollPositionChanged(newPosition, limit);
+ }
+
+ public void putDisplayItem(Position target, Position base, DisplayItem item) {
+ ItemEntry entry = new ItemEntry(item, target, base);
+ mItemList.insertLast(entry);
+ mItems.put(item, entry);
+ }
+
+ public void removeDisplayItem(DisplayItem item) {
+ ItemEntry entry = mItems.remove(item);
+ if (entry != null) entry.remove();
+ }
+
+ public Rect getSlotRect(int slotIndex) {
+ return mLayout.getSlotRect(slotIndex);
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ if (mUIListener != null) mUIListener.onUserInteraction();
+ mGestureDetector.onTouchEvent(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownInScrolling = !mScroller.isFinished();
+ mScroller.forceFinished();
+ break;
+ }
+ return true;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setUserInteractionListener(UserInteractionListener listener) {
+ mUIListener = listener;
+ }
+
+ public void setOverscrollEffect(int kind) {
+ mOverscrollEffect = kind;
+ mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_CLIP);
+ canvas.clipRect(0, 0, getWidth(), getHeight());
+ super.render(canvas);
+
+ long currentTimeMillis = canvas.currentAnimationTimeMillis();
+ boolean more = mScroller.advanceAnimation(currentTimeMillis);
+ boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D)
+ && mPaper.advanceAnimation(currentTimeMillis);
+ updateScrollPosition(mScroller.getPosition(), false);
+ float interpolate = 1f;
+ if (mAnimation != null) {
+ more |= mAnimation.calculate(currentTimeMillis);
+ interpolate = mAnimation.value;
+ }
+
+ more |= paperActive;
+
+ if (WIDE) {
+ canvas.translate(-mScrollX, 0, 0);
+ } else {
+ canvas.translate(0, -mScrollY, 0);
+ }
+
+ LinkedNode.List<ItemEntry> list = mItemList;
+ for (ItemEntry entry = list.getLast(); entry != null;) {
+ if (renderItem(canvas, entry, interpolate, 0, paperActive)) {
+ mCurrentItems.add(entry);
+ }
+ entry = list.previousOf(entry);
+ }
+
+ int pass = 1;
+ while (!mCurrentItems.isEmpty()) {
+ for (int i = 0, n = mCurrentItems.size(); i < n; i++) {
+ ItemEntry entry = mCurrentItems.get(i);
+ if (renderItem(canvas, entry, interpolate, pass, paperActive)) {
+ mNextItems.add(entry);
+ }
+ }
+ mCurrentItems.clear();
+ // swap mNextItems with mCurrentItems
+ ArrayList<ItemEntry> tmp = mNextItems;
+ mNextItems = mCurrentItems;
+ mCurrentItems = tmp;
+ pass += 1;
+ }
+
+ if (WIDE) {
+ canvas.translate(mScrollX, 0, 0);
+ } else {
+ canvas.translate(0, mScrollY, 0);
+ }
+
+ if (more) invalidate();
+ if (mMoreAnimation && !more && mUIListener != null) {
+ mUIListener.onUserInteractionEnd();
+ }
+ mMoreAnimation = more;
+ canvas.restore();
+ }
+
+ private boolean renderItem(GLCanvas canvas, ItemEntry entry,
+ float interpolate, int pass, boolean paperActive) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ Position position = entry.target;
+ if (mPositions != null) {
+ position = mTempPosition;
+ position.set(entry.target);
+ position.x -= mScrollX;
+ position.y -= mScrollY;
+ Position source = mPositions
+ .getPosition(entry.item.getIdentity(), position);
+ source.x += mScrollX;
+ source.y += mScrollY;
+ position = mTempPosition;
+ Position.interpolate(
+ source, entry.target, position, interpolate);
+ }
+ canvas.multiplyAlpha(position.alpha);
+ if (paperActive) {
+ canvas.multiplyMatrix(mPaper.getTransform(
+ position, entry.base, mScrollX, mScrollY), 0);
+ } else {
+ canvas.translate(position.x, position.y, position.z);
+ }
+ canvas.rotate(position.theta, 0, 0, 1);
+ boolean more = entry.item.render(canvas, pass);
+ canvas.restore();
+ return more;
+ }
+
+ public static class MyAnimation extends Animation {
+ public float value;
+
+ public MyAnimation() {
+ setInterpolator(new DecelerateInterpolator(4));
+ setDuration(1500);
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ value = progress;
+ }
+ }
+
+ private static class ItemEntry extends LinkedNode {
+ public DisplayItem item;
+ public Position target;
+ public Position base;
+
+ public ItemEntry(DisplayItem item, Position target, Position base) {
+ this.item = item;
+ this.target = target;
+ this.base = base;
+ }
+ }
+
+ public static class Layout {
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private int mSlotCount;
+ private int mSlotWidth;
+ private int mSlotHeight;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int mUnitCount;
+ private int mContentLength;
+ private int mScrollPosition;
+
+ private int mVerticalPadding;
+ private int mHorizontalPadding;
+
+ public void setSlotSize(int slotWidth, int slotHeight) {
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ }
+
+ public boolean setSlotCount(int slotCount) {
+ mSlotCount = slotCount;
+ int hPadding = mHorizontalPadding;
+ int vPadding = mVerticalPadding;
+ initLayoutParameters();
+ return vPadding != mVerticalPadding || hPadding != mHorizontalPadding;
+ }
+
+ public Rect getSlotRect(int index) {
+ int col, row;
+ if (WIDE) {
+ col = index / mUnitCount;
+ row = index - col * mUnitCount;
+ } else {
+ row = index / mUnitCount;
+ col = index - row * mUnitCount;
+ }
+
+ int x = mHorizontalPadding + col * mSlotWidth;
+ int y = mVerticalPadding + row * mSlotHeight;
+ return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
+ }
+
+ public int getContentLength() {
+ return mContentLength;
+ }
+
+ // Calculate
+ // (1) mUnitCount: the number of slots we can fit into one column (or row).
+ // (2) mContentLength: the width (or height) we need to display all the
+ // columns (rows).
+ // (3) padding[]: the vertical and horizontal padding we need in order
+ // to put the slots towards to the center of the display.
+ //
+ // The "major" direction is the direction the user can scroll. The other
+ // direction is the "minor" direction.
+ //
+ // The comments inside this method are the description when the major
+ // directon is horizontal (X), and the minor directon is vertical (Y).
+ private void initLayoutParameters(
+ int majorLength, int minorLength, /* The view width and height */
+ int majorUnitSize, int minorUnitSize, /* The slot width and height */
+ int[] padding) {
+ int unitCount = minorLength / minorUnitSize;
+ if (unitCount == 0) unitCount = 1;
+ mUnitCount = unitCount;
+
+ // We put extra padding above and below the column.
+ int availableUnits = Math.min(mUnitCount, mSlotCount);
+ padding[0] = (minorLength - availableUnits * minorUnitSize) / 2;
+
+ // Then calculate how many columns we need for all slots.
+ int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+ mContentLength = count * majorUnitSize;
+
+ // If the content length is less then the screen width, put
+ // extra padding in left and right.
+ padding[1] = Math.max(0, (majorLength - mContentLength) / 2);
+ }
+
+ private void initLayoutParameters() {
+ int[] padding = new int[2];
+ if (WIDE) {
+ initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
+ mVerticalPadding = padding[0];
+ mHorizontalPadding = padding[1];
+ } else {
+ initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
+ mVerticalPadding = padding[1];
+ mHorizontalPadding = padding[0];
+ }
+ updateVisibleSlotRange();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ initLayoutParameters();
+ }
+
+ private void updateVisibleSlotRange() {
+ int position = mScrollPosition;
+
+ if (WIDE) {
+ int start = Math.max(0, (position / mSlotWidth) * mUnitCount);
+ int end = Math.min(mSlotCount, mUnitCount
+ * (position + mWidth + mSlotWidth - 1) / mSlotWidth);
+ setVisibleRange(start, end);
+ } else {
+ int start = Math.max(0, mUnitCount * (position / mSlotHeight));
+ int end = Math.min(mSlotCount, mUnitCount
+ * (position + mHeight + mSlotHeight - 1) / mSlotHeight);
+ setVisibleRange(start, end);
+ }
+ }
+
+ public void setScrollPosition(int position) {
+ if (mScrollPosition == position) return;
+ mScrollPosition = position;
+ updateVisibleSlotRange();
+ }
+
+ private void setVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) return;
+ if (start < end) {
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ } else {
+ mVisibleStart = mVisibleEnd = 0;
+ }
+ }
+
+ public int getVisibleStart() {
+ return mVisibleStart;
+ }
+
+ public int getVisibleEnd() {
+ return mVisibleEnd;
+ }
+
+ public int getSlotIndexByPosition(float x, float y) {
+ float absoluteX = x + (WIDE ? mScrollPosition : 0);
+ absoluteX -= mHorizontalPadding;
+ int columnIdx = (int) (absoluteX + 0.5) / mSlotWidth;
+ if ((absoluteX - mSlotWidth * columnIdx) < 0
+ || (!WIDE && columnIdx >= mUnitCount)) {
+ return INDEX_NONE;
+ }
+
+ float absoluteY = y + (WIDE ? 0 : mScrollPosition);
+ absoluteY -= mVerticalPadding;
+ int rowIdx = (int) (absoluteY + 0.5) / mSlotHeight;
+ if (((absoluteY - mSlotHeight * rowIdx) < 0)
+ || (WIDE && rowIdx >= mUnitCount)) {
+ return INDEX_NONE;
+ }
+ int index = WIDE
+ ? (columnIdx * mUnitCount + rowIdx)
+ : (rowIdx * mUnitCount + columnIdx);
+
+ return index >= mSlotCount ? INDEX_NONE : index;
+ }
+
+ public int getScrollLimit() {
+ int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
+ return limit <= 0 ? 0 : limit;
+ }
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+
+ @Override
+ public boolean onFling(MotionEvent e1,
+ MotionEvent e2, float velocityX, float velocityY) {
+ int scrollLimit = mLayout.getScrollLimit();
+ if (scrollLimit == 0) return false;
+ float velocity = WIDE ? velocityX : velocityY;
+ mScroller.fling((int) -velocity, 0, scrollLimit);
+ if (mUIListener != null) mUIListener.onUserInteractionBegin();
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1,
+ MotionEvent e2, float distanceX, float distanceY) {
+ float distance = WIDE ? distanceX : distanceY;
+ boolean canMove = mScroller.startScroll(
+ Math.round(distance), 0, mLayout.getScrollLimit());
+ if (mOverscrollEffect == OVERSCROLL_3D && !canMove) {
+ mPaper.overScroll(distance);
+ }
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (mDownInScrolling) return true;
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) mListener.onSingleTapUp(index);
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mDownInScrolling) return;
+ lockRendering();
+ try {
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) mListener.onLongTap(index);
+ } finally {
+ unlockRendering();
+ }
+ }
+ }
+
+ public void setStartIndex(int index) {
+ mStartIndex = index;
+ }
+
+ // Return true if the layout parameters have been changed
+ public boolean setSlotCount(int slotCount) {
+ boolean changed = mLayout.setSlotCount(slotCount);
+
+ // mStartIndex is applied the first time setSlotCount is called.
+ if (mStartIndex != INDEX_NONE) {
+ setCenterIndex(mStartIndex);
+ mStartIndex = INDEX_NONE;
+ }
+ updateScrollPosition(WIDE ? mScrollX : mScrollY, true);
+ return changed;
+ }
+
+ public int getVisibleStart() {
+ return mLayout.getVisibleStart();
+ }
+
+ public int getVisibleEnd() {
+ return mLayout.getVisibleEnd();
+ }
+
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ public int getScrollY() {
+ return mScrollY;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StaticBackground.java b/src/com/android/gallery3d/ui/StaticBackground.java
new file mode 100644
index 000000000..08c55c378
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StaticBackground.java
@@ -0,0 +1,62 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+
+public class StaticBackground extends GLView {
+
+ private Context mContext;
+ private int mLandscapeResource;
+ private int mPortraitResource;
+
+ private BasicTexture mBackground;
+ private boolean mIsLandscape = false;
+
+ public StaticBackground(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ setOrientation(getWidth() >= getHeight());
+ }
+
+ private void setOrientation(boolean isLandscape) {
+ if (mIsLandscape == isLandscape) return;
+ mIsLandscape = isLandscape;
+ if (mBackground != null) mBackground.recycle();
+ mBackground = new ResourceTexture(
+ mContext, mIsLandscape ? mLandscapeResource : mPortraitResource);
+ invalidate();
+ }
+
+ public void setImage(int landscapeId, int portraitId) {
+ mLandscapeResource = landscapeId;
+ mPortraitResource = portraitId;
+ if (mBackground != null) mBackground.recycle();
+ mBackground = new ResourceTexture(
+ mContext, mIsLandscape ? landscapeId : portraitId);
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ //mBackground.draw(canvas, 0, 0, getWidth(), getHeight());
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0xFF000000);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java
new file mode 100644
index 000000000..71ab9b351
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StringTexture.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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+// StringTexture is a texture shows the content of a specified String.
+//
+// To create a StringTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+class StringTexture extends CanvasTexture {
+ private final String mText;
+ private final TextPaint mPaint;
+ private final FontMetricsInt mMetrics;
+
+ private StringTexture(String text, TextPaint paint,
+ FontMetricsInt metrics, int width, int height) {
+ super(width, height);
+ mText = text;
+ mPaint = paint;
+ mMetrics = metrics;
+ }
+
+ public static TextPaint getDefaultPaint(float textSize, int color) {
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
+ return paint;
+ }
+
+ public static StringTexture newInstance(
+ String text, float textSize, int color) {
+ return newInstance(text, getDefaultPaint(textSize, color));
+ }
+
+ public static StringTexture newInstance(
+ String text, String postfix, float textSize, int color,
+ float lengthLimit, boolean isBold) {
+ TextPaint paint = getDefaultPaint(textSize, color);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ if (postfix != null) {
+ lengthLimit = Math.max(0,
+ lengthLimit - paint.measureText(postfix));
+ text = TextUtils.ellipsize(text, paint, lengthLimit,
+ TextUtils.TruncateAt.END).toString() + postfix;
+ } else {
+ text = TextUtils.ellipsize(
+ text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
+ }
+ return newInstance(text, paint);
+ }
+
+ private static StringTexture newInstance(String text, TextPaint paint) {
+ FontMetricsInt metrics = paint.getFontMetricsInt();
+ int width = (int) Math.ceil(paint.measureText(text));
+ int height = metrics.bottom - metrics.top;
+ // The texture size needs to be at least 1x1.
+ if (width <= 0) width = 1;
+ if (height <= 0) height = 1;
+ return new StringTexture(text, paint, metrics, width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ canvas.translate(0, -mMetrics.ascent);
+ canvas.drawText(mText, 0, 0, mPaint);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java
new file mode 100644
index 000000000..09106128f
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StripDrawer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class StripDrawer extends SelectionDrawer {
+ private NinePatchTexture mFocusBox;
+ private Rect mFocusBoxPadding;
+
+ public StripDrawer(Context context) {
+ mFocusBox = new NinePatchTexture(context, R.drawable.focus_box);
+ mFocusBoxPadding = mFocusBox.getPaddings();
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotation(canvas, content, x, y, width, height, rotation);
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ int x = -width / 2;
+ int y = -height / 2;
+ Rect p = mFocusBoxPadding;
+ mFocusBox.draw(canvas, x - p.left, y - p.top,
+ width + p.left + p.right, height + p.top + p.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 000000000..bd494a331
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -0,0 +1,41 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.os.Handler;
+import android.os.Message;
+
+public class SynchronizedHandler extends Handler {
+
+ private final GLRoot mRoot;
+
+ public SynchronizedHandler(GLRoot root) {
+ mRoot = Utils.checkNotNull(root);
+ }
+
+ @Override
+ public void dispatchMessage(Message message) {
+ mRoot.lockRenderThread();
+ try {
+ super.dispatchMessage(message);
+ } finally {
+ mRoot.unlockRenderThread();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TextButton.java b/src/com/android/gallery3d/ui/TextButton.java
new file mode 100644
index 000000000..c6b85bf55
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TextButton.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ui;
+
+import static com.android.gallery3d.ui.TextButtonConfig.*;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+public class TextButton extends Label {
+ private static final String TAG = "TextButton";
+ private boolean mPressed;
+ private Texture mPressedBackground;
+ private Texture mNormalBackground;
+ private OnClickedListener mOnClickListener;
+
+ public interface OnClickedListener {
+ public void onClicked(GLView source);
+ }
+
+ public TextButton(Context context, int label) {
+ super(context, label);
+ setPaddings(HORIZONTAL_PADDINGS, VERTICAL_PADDINGS,
+ HORIZONTAL_PADDINGS, VERTICAL_PADDINGS);
+ }
+
+ public void setOnClickListener(OnClickedListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void setPressedBackground(Texture texture) {
+ mPressedBackground = texture;
+ }
+
+ public void setNormalBackground(Texture texture) {
+ mNormalBackground = texture;
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressed = true;
+ invalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mOnClickListener != null) {
+ mOnClickListener.onClicked(this);
+ }
+ // fall-through
+ case MotionEvent.ACTION_CANCEL:
+ mPressed = false;
+ invalidate();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Texture bg = mPressed ? mPressedBackground : mNormalBackground;
+ if (bg != null) {
+ int width = getWidth();
+ int height = getHeight();
+ if (bg instanceof NinePatchTexture) {
+ Rect p = ((NinePatchTexture) bg).getPaddings();
+ bg.draw(canvas, -p.left, -p.top,
+ width + p.left + p.right, height + p.top + p.bottom);
+ } else {
+ bg.draw(canvas, 0, 0, width, height);
+ }
+ }
+ super.render(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Texture.java b/src/com/android/gallery3d/ui/Texture.java
new file mode 100644
index 000000000..feb7b0ab7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Texture.java
@@ -0,0 +1,44 @@
+/*
+ * 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.ui;
+
+// Texture is a rectangular image which can be drawn on GLCanvas.
+// The isOpaque() function gives a hint about whether the texture is opaque,
+// so the drawing can be done faster.
+//
+// This is the current texture hierarchy:
+//
+// Texture
+// -- ColorTexture
+// -- BasicTexture
+// -- RawTexture
+// -- UploadedTexture
+// -- BitmapTexture
+// -- Tile
+// -- ResourceTexture
+// -- NinePatchTexture
+// -- CanvasTexture
+// -- DrawableTexture
+// -- StringTexture
+//
+public interface Texture {
+ public int getWidth();
+ public int getHeight();
+ public void draw(GLCanvas canvas, int x, int y);
+ public void draw(GLCanvas canvas, int x, int y, int w, int h);
+ public boolean isOpaque();
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
new file mode 100644
index 000000000..cf0685191
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -0,0 +1,693 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class TileImageView extends GLView {
+ public static final int SIZE_UNKNOWN = -1;
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "TileImageView";
+
+ // TILE_SIZE must be 2^N - 2. We put one pixel border in each side of the
+ // texture to avoid seams between tiles.
+ private static final int TILE_SIZE = 254;
+ private static final int TILE_BORDER = 1;
+ private static final int UPLOAD_LIMIT = 1;
+
+ /*
+ * This is the tile state in the CPU side.
+ * Life of a Tile:
+ * ACTIVATED (initial state)
+ * --> IN_QUEUE - by queueForDecode()
+ * --> RECYCLED - by recycleTile()
+ * IN_QUEUE --> DECODING - by decodeTile()
+ * --> RECYCLED - by recycleTile)
+ * DECODING --> RECYCLING - by recycleTile()
+ * --> DECODED - by decodeTile()
+ * RECYCLING --> RECYCLED - by decodeTile()
+ * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+ * DECODED --> RECYCLED - by recycleTile()
+ * RECYCLED --> ACTIVATED - by obtainTile()
+ */
+ private static final int STATE_ACTIVATED = 0x01;
+ private static final int STATE_IN_QUEUE = 0x02;
+ private static final int STATE_DECODING = 0x04;
+ private static final int STATE_DECODED = 0x08;
+ private static final int STATE_RECYCLING = 0x10;
+ private static final int STATE_RECYCLED = 0x20;
+
+ private Model mModel;
+ protected BitmapTexture mBackupImage;
+ protected int mLevelCount; // cache the value of mScaledBitmaps.length
+
+ // The mLevel variable indicates which level of bitmap we should use.
+ // Level 0 means the original full-sized bitmap, and a larger value means
+ // a smaller scaled bitmap (The width and height of each scaled bitmap is
+ // half size of the previous one). If the value is in [0, mLevelCount), we
+ // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
+ // is mLevelCount, and that means we use mBackupTexture for display.
+ private int mLevel = 0;
+
+ // The offsets of the (left, top) of the upper-left tile to the (left, top)
+ // of the view.
+ private int mOffsetX;
+ private int mOffsetY;
+
+ private int mUploadQuota;
+ private boolean mRenderComplete;
+
+ private final RectF mSourceRect = new RectF();
+ private final RectF mTargetRect = new RectF();
+
+ private final HashMap<Long, Tile> mActiveTiles = new HashMap<Long, Tile>();
+
+ // The following three queue is guarded by TileImageView.this
+ private TileQueue mRecycledQueue = new TileQueue();
+ private TileQueue mUploadQueue = new TileQueue();
+ private TileQueue mDecodeQueue = new TileQueue();
+
+ // The width and height of the full-sized bitmap
+ protected int mImageWidth = SIZE_UNKNOWN;
+ protected int mImageHeight = SIZE_UNKNOWN;
+
+ protected int mCenterX;
+ protected int mCenterY;
+ protected float mScale;
+ protected int mRotation;
+
+ // Temp variables to avoid memory allocation
+ private final Rect mTileRange = new Rect();
+ private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+ private final TileUploader mTileUploader = new TileUploader();
+ private boolean mIsTextureFreed;
+ private Future<Void> mTileDecoder;
+ private ThreadPool mThreadPool;
+ private boolean mBackgroundTileUploaded;
+
+ public static interface Model {
+ public int getLevelCount();
+ public Bitmap getBackupImage();
+ public int getImageWidth();
+ public int getImageHeight();
+
+ // The method would be called in another thread
+ public Bitmap getTile(int level, int x, int y, int tileSize);
+ public boolean isFailedToLoad();
+ }
+
+ public TileImageView(GalleryContext context) {
+ mThreadPool = context.getThreadPool();
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+
+ public void setModel(Model model) {
+ mModel = model;
+ if (model != null) notifyModelInvalidated();
+ }
+
+ private void updateBackupTexture(Bitmap backup) {
+ if (backup == null) {
+ if (mBackupImage != null) mBackupImage.recycle();
+ mBackupImage = null;
+ } else {
+ if (mBackupImage != null) {
+ if (mBackupImage.getBitmap() != backup) {
+ mBackupImage.recycle();
+ mBackupImage = new BitmapTexture(backup);
+ }
+ } else {
+ mBackupImage = new BitmapTexture(backup);
+ }
+ }
+ }
+
+ public void notifyModelInvalidated() {
+ invalidateTiles();
+ if (mModel == null) {
+ mBackupImage = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ } else {
+ updateBackupTexture(mModel.getBackupImage());
+ mImageWidth = mModel.getImageWidth();
+ mImageHeight = mModel.getImageHeight();
+ mLevelCount = mModel.getLevelCount();
+ }
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ super.onLayout(changeSize, left, top, right, bottom);
+ if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ }
+
+ // Prepare the tiles we want to use for display.
+ //
+ // 1. Decide the tile level we want to use for display.
+ // 2. Decide the tile levels we want to keep as texture (in addition to
+ // the one we use for display).
+ // 3. Recycle unused tiles.
+ // 4. Activate the tiles we want.
+ private void layoutTiles(int centerX, int centerY, float scale, int rotation) {
+ // The width and height of this view.
+ int width = getWidth();
+ int height = getHeight();
+
+ // The tile levels we want to keep as texture is in the range
+ // [fromLevel, endLevel).
+ int fromLevel;
+ int endLevel;
+
+ // We want to use a texture larger than or equal to the display size.
+ mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount);
+
+ // We want to keep one more tile level as texture in addition to what
+ // we use for display. So it can be faster when the scale moves to the
+ // next level. We choose a level closer to the current scale.
+ if (mLevel != mLevelCount) {
+ Rect range = mTileRange;
+ getRange(range, centerX, centerY, mLevel, scale, rotation);
+ mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale);
+ mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale);
+ fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+ } else {
+ // Activate the tiles of the smallest two levels.
+ fromLevel = mLevel - 2;
+ mOffsetX = Math.round(width / 2f - centerX * scale);
+ mOffsetY = Math.round(height / 2f - centerY * scale);
+ }
+
+ fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
+ endLevel = Math.min(fromLevel + 2, mLevelCount);
+
+ Rect range[] = mActiveRange;
+ for (int i = fromLevel; i < endLevel; ++i) {
+ getRange(range[i - fromLevel], centerX, centerY, i, rotation);
+ }
+
+ // If rotation is transient, don't update the tile.
+ if (rotation % 90 != 0) return;
+
+ synchronized (this) {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+ mBackgroundTileUploaded = false;
+ }
+
+ // Recycle unused tiles: if the level of the active tile is outside the
+ // range [fromLevel, endLevel) or not in the visible range.
+ Iterator<Map.Entry<Long, Tile>>
+ iter = mActiveTiles.entrySet().iterator();
+ while (iter.hasNext()) {
+ Tile tile = iter.next().getValue();
+ int level = tile.mTileLevel;
+ if (level < fromLevel || level >= endLevel
+ || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+ iter.remove();
+ recycleTile(tile);
+ }
+ }
+
+ for (int i = fromLevel; i < endLevel; ++i) {
+ int size = TILE_SIZE << i;
+ Rect r = range[i - fromLevel];
+ for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
+ for (int x = r.left, right = r.right; x < right; x += size) {
+ activateTile(x, y, i);
+ }
+ }
+ }
+ invalidate();
+ }
+
+ protected synchronized void invalidateTiles() {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+ // TODO disable decoder
+ for (Tile tile : mActiveTiles.values()) {
+ recycleTile(tile);
+ }
+ mActiveTiles.clear();
+ }
+
+ private void getRange(Rect out, int cX, int cY, int level, int rotation) {
+ getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
+ }
+
+ // If the bitmap is scaled by the given factor "scale", return the
+ // rectangle containing visible range. The left-top coordinate returned is
+ // aligned to the tile boundary.
+ //
+ // (cX, cY) is the point on the original bitmap which will be put in the
+ // center of the ImageViewer.
+ private void getRange(Rect out,
+ int cX, int cY, int level, float scale, int rotation) {
+
+ double radians = Math.toRadians(-rotation);
+ double w = getWidth();
+ double h = getHeight();
+
+ double cos = Math.cos(radians);
+ double sin = Math.sin(radians);
+ int width = (int) Math.ceil(Math.max(
+ Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
+ int height = (int) Math.ceil(Math.max(
+ Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
+
+ int left = (int) Math.floor(cX - width / (2f * scale));
+ int top = (int) Math.floor(cY - height / (2f * scale));
+ int right = (int) Math.ceil(left + width / scale);
+ int bottom = (int) Math.ceil(top + height / scale);
+
+ // align the rectangle to tile boundary
+ int size = TILE_SIZE << level;
+ left = Math.max(0, size * (left / size));
+ top = Math.max(0, size * (top / size));
+ right = Math.min(mImageWidth, right);
+ bottom = Math.min(mImageHeight, bottom);
+
+ out.set(left, top, right, bottom);
+ }
+
+ public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
+ if (mCenterX == centerX
+ && mCenterY == centerY && mScale == scale) return false;
+ mCenterX = centerX;
+ mCenterY = centerY;
+ mScale = scale;
+ mRotation = rotation;
+ layoutTiles(centerX, centerY, scale, rotation);
+ invalidate();
+ return true;
+ }
+
+ public void freeTextures() {
+ mIsTextureFreed = true;
+
+ if (mTileDecoder != null) {
+ mTileDecoder.cancel();
+ mTileDecoder.get();
+ mTileDecoder = null;
+ }
+
+ for (Tile texture : mActiveTiles.values()) {
+ texture.recycle();
+ }
+ mTileRange.set(0, 0, 0, 0);
+ mActiveTiles.clear();
+
+ synchronized (this) {
+ mUploadQueue.clean();
+ mDecodeQueue.clean();
+ Tile tile = mRecycledQueue.pop();
+ while (tile != null) {
+ tile.recycle();
+ tile = mRecycledQueue.pop();
+ }
+ }
+ updateBackupTexture(null);
+ }
+
+ public void prepareTextures() {
+ if (mTileDecoder == null) {
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+ if (mIsTextureFreed) {
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ mIsTextureFreed = false;
+ updateBackupTexture(mModel.getBackupImage());
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mUploadQuota = UPLOAD_LIMIT;
+ mRenderComplete = true;
+
+ int level = mLevel;
+ int rotation = mRotation;
+
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ int centerX = getWidth() / 2, centerY = getHeight() / 2;
+ canvas.translate(centerX, centerY, 0);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-centerX, -centerY, 0);
+ }
+ try {
+ if (level != mLevelCount) {
+ int size = (TILE_SIZE << level);
+ float length = size * mScale;
+ Rect r = mTileRange;
+
+ for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
+ float y = mOffsetY + i * length;
+ for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
+ float x = mOffsetX + j * length;
+ drawTile(canvas, tx, ty, level, x, y, length);
+ }
+ }
+ } else if (mBackupImage != null) {
+ mBackupImage.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
+ }
+ } finally {
+ if (rotation != 0) canvas.restore();
+ }
+
+ if (mRenderComplete) {
+ if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
+ } else {
+ invalidate();
+ }
+ }
+
+ private void uploadBackgroundTiles(GLCanvas canvas) {
+ mBackgroundTileUploaded = true;
+ for (Tile tile : mActiveTiles.values()) {
+ if (!tile.isContentValid(canvas)) queueForDecode(tile);
+ }
+ }
+
+ void queueForUpload(Tile tile) {
+ synchronized (this) {
+ mUploadQueue.push(tile);
+ }
+ if (mTileUploader.mActive.compareAndSet(false, true)) {
+ getGLRoot().addOnGLIdleListener(mTileUploader);
+ }
+ }
+
+ synchronized void queueForDecode(Tile tile) {
+ if (tile.mTileState == STATE_ACTIVATED) {
+ tile.mTileState = STATE_IN_QUEUE;
+ if (mDecodeQueue.push(tile)) notifyAll();
+ }
+ }
+
+ boolean decodeTile(Tile tile) {
+ synchronized (this) {
+ if (tile.mTileState != STATE_IN_QUEUE) return false;
+ tile.mTileState = STATE_DECODING;
+ }
+ boolean decodeComplete = tile.decode();
+ synchronized (this) {
+ if (tile.mTileState == STATE_RECYCLING) {
+ tile.mTileState = STATE_RECYCLED;
+ tile.mDecodedTile = null;
+ mRecycledQueue.push(tile);
+ return false;
+ }
+ tile.mTileState = STATE_DECODED;
+ return decodeComplete;
+ }
+ }
+
+ private synchronized Tile obtainTile(int x, int y, int level) {
+ Tile tile = mRecycledQueue.pop();
+ if (tile != null) {
+ tile.mTileState = STATE_ACTIVATED;
+ tile.update(x, y, level);
+ return tile;
+ }
+ return new Tile(x, y, level);
+ }
+
+ synchronized void recycleTile(Tile tile) {
+ if (tile.mTileState == STATE_DECODING) {
+ tile.mTileState = STATE_RECYCLING;
+ return;
+ }
+ tile.mTileState = STATE_RECYCLED;
+ tile.mDecodedTile = null;
+ mRecycledQueue.push(tile);
+ }
+
+ private void activateTile(int x, int y, int level) {
+ Long key = makeTileKey(x, y, level);
+ Tile tile = mActiveTiles.get(key);
+ if (tile != null) {
+ if (tile.mTileState == STATE_IN_QUEUE) {
+ tile.mTileState = STATE_ACTIVATED;
+ }
+ return;
+ }
+ tile = obtainTile(x, y, level);
+ mActiveTiles.put(key, tile);
+ }
+
+ private Tile getTile(int x, int y, int level) {
+ return mActiveTiles.get(makeTileKey(x, y, level));
+ }
+
+ private static Long makeTileKey(int x, int y, int level) {
+ long result = x;
+ result = (result << 16) | y;
+ result = (result << 16) | level;
+ return Long.valueOf(result);
+ }
+
+ private class TileUploader implements GLRoot.OnGLIdleListener {
+ AtomicBoolean mActive = new AtomicBoolean(false);
+
+ @Override
+ public boolean onGLIdle(GLRoot root, GLCanvas canvas) {
+ int quota = UPLOAD_LIMIT;
+ Tile tile;
+ while (true) {
+ synchronized (TileImageView.this) {
+ tile = mUploadQueue.pop();
+ }
+ if (tile == null || quota <= 0) break;
+ if (!tile.isContentValid(canvas)) {
+ Utils.assertTrue(tile.mTileState == STATE_DECODED);
+ tile.updateContent(canvas);
+ --quota;
+ }
+ }
+ mActive.set(tile != null);
+ return tile != null;
+ }
+ }
+
+ // Draw the tile to a square at canvas that locates at (x, y) and
+ // has a side length of length.
+ public void drawTile(GLCanvas canvas,
+ int tx, int ty, int level, float x, float y, float length) {
+ RectF source = mSourceRect;
+ RectF target = mTargetRect;
+ target.set(x, y, x + length, y + length);
+ source.set(0, 0, TILE_SIZE, TILE_SIZE);
+
+ Tile tile = getTile(tx, ty, level);
+ if (tile != null) {
+ if (!tile.isContentValid(canvas)) {
+ if (tile.mTileState == STATE_DECODED) {
+ if (mUploadQuota > 0) {
+ --mUploadQuota;
+ tile.updateContent(canvas);
+ } else {
+ mRenderComplete = false;
+ }
+ } else {
+ mRenderComplete = false;
+ queueForDecode(tile);
+ }
+ }
+ if (drawTile(tile, canvas, source, target)) return;
+ }
+ if (mBackupImage != null) {
+ BasicTexture backup = mBackupImage;
+ int size = TILE_SIZE << level;
+ float scaleX = (float) backup.getWidth() / mImageWidth;
+ float scaleY = (float) backup.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ canvas.drawTexture(backup, source, target);
+ }
+ }
+
+ // TODO: avoid drawing the unused part of the textures.
+ static boolean drawTile(
+ Tile tile, GLCanvas canvas, RectF source, RectF target) {
+ while (true) {
+ if (tile.isContentValid(canvas)) {
+ // offset source rectangle for the texture border.
+ source.offset(TILE_BORDER, TILE_BORDER);
+ canvas.drawTexture(tile, source, target);
+ return true;
+ }
+
+ // Parent can be divided to four quads and tile is one of the four.
+ Tile parent = tile.getParentTile();
+ if (parent == null) return false;
+ if (tile.mX == parent.mX) {
+ source.left /= 2f;
+ source.right /= 2f;
+ } else {
+ source.left = (TILE_SIZE + source.left) / 2f;
+ source.right = (TILE_SIZE + source.right) / 2f;
+ }
+ if (tile.mY == parent.mY) {
+ source.top /= 2f;
+ source.bottom /= 2f;
+ } else {
+ source.top = (TILE_SIZE + source.top) / 2f;
+ source.bottom = (TILE_SIZE + source.bottom) / 2f;
+ }
+ tile = parent;
+ }
+ }
+
+ private class Tile extends UploadedTexture {
+ int mX;
+ int mY;
+ int mTileLevel;
+ Tile mNext;
+ Bitmap mDecodedTile;
+ volatile int mTileState = STATE_ACTIVATED;
+
+ public Tile(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ bitmap.recycle();
+ }
+
+ boolean decode() {
+ // Get a tile from the original image. The tile is down-scaled
+ // by (1 << mTilelevel) from a region in the original image.
+ int tileLength = (TILE_SIZE + 2 * TILE_BORDER);
+ int borderLength = TILE_BORDER << mTileLevel;
+ try {
+ mDecodedTile = mModel.getTile(
+ mTileLevel, mX - borderLength, mY - borderLength, tileLength);
+ return mDecodedTile != null;
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode tile", t);
+ return false;
+ }
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Utils.assertTrue(mTileState == STATE_DECODED);
+ Bitmap bitmap = mDecodedTile;
+ mDecodedTile = null;
+ mTileState = STATE_ACTIVATED;
+ return bitmap;
+ }
+
+ public void update(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ invalidateContent();
+ }
+
+ public Tile getParentTile() {
+ if (mTileLevel + 1 == mLevelCount) return null;
+ int size = TILE_SIZE << (mTileLevel + 1);
+ int x = size * (mX / size);
+ int y = size * (mY / size);
+ return getTile(x, y, mTileLevel + 1);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tile(%s, %s, %s / %s)",
+ mX / TILE_SIZE, mY / TILE_SIZE, mLevel, mLevelCount);
+ }
+ }
+
+ private static class TileQueue {
+ private Tile mHead;
+
+ public Tile pop() {
+ Tile tile = mHead;
+ if (tile != null) mHead = tile.mNext;
+ return tile;
+ }
+
+ public boolean push(Tile tile) {
+ boolean wasEmpty = mHead == null;
+ tile.mNext = mHead;
+ mHead = tile;
+ return wasEmpty;
+ }
+
+ public void clean() {
+ mHead = null;
+ }
+ }
+
+ private class TileDecoder implements ThreadPool.Job<Void> {
+
+ private CancelListener mNotifier = new CancelListener() {
+ @Override
+ public void onCancel() {
+ synchronized (TileImageView.this) {
+ TileImageView.this.notifyAll();
+ }
+ }
+ };
+
+ @Override
+ public Void run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ jc.setCancelListener(mNotifier);
+ while (!jc.isCancelled()) {
+ Tile tile = null;
+ synchronized(TileImageView.this) {
+ tile = mDecodeQueue.pop();
+ if (tile == null && !jc.isCancelled()) {
+ Utils.waitWithoutInterrupt(TileImageView.this);
+ }
+ }
+ if (tile == null) continue;
+ if (decodeTile(tile)) queueForUpload(tile);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
new file mode 100644
index 000000000..65dea0eac
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -0,0 +1,144 @@
+/*
+ * 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.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+public class TileImageViewAdapter implements TileImageView.Model {
+ protected BitmapRegionDecoder mRegionDecoder;
+ protected int mImageWidth;
+ protected int mImageHeight;
+ protected Bitmap mBackupImage;
+ protected int mLevelCount;
+ protected boolean mFailedToLoad;
+
+ private final Rect mIntersectRect = new Rect();
+ private final Rect mRegionRect = new Rect();
+
+ public TileImageViewAdapter() {
+ }
+
+ public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) {
+ mBackupImage = Utils.checkNotNull(backup);
+ mRegionDecoder = regionDecoder;
+ mImageWidth = regionDecoder.getWidth();
+ mImageHeight = regionDecoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ }
+
+ public synchronized void clear() {
+ mBackupImage = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ mRegionDecoder = null;
+ mFailedToLoad = false;
+ }
+
+ public synchronized void setBackupImage(Bitmap backup, int width, int height) {
+ mBackupImage = Utils.checkNotNull(backup);
+ mImageWidth = width;
+ mImageHeight = height;
+ mRegionDecoder = null;
+ mLevelCount = 0;
+ mFailedToLoad = false;
+ }
+
+ public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
+ mRegionDecoder = Utils.checkNotNull(decoder);
+ mImageWidth = decoder.getWidth();
+ mImageHeight = decoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ mFailedToLoad = false;
+ }
+
+ private int calculateLevelCount() {
+ return Math.max(0, Utils.ceilLog2(
+ (float) mImageWidth / mBackupImage.getWidth()));
+ }
+
+ @Override
+ public synchronized Bitmap getTile(int level, int x, int y, int length) {
+ Rect region = mRegionRect;
+ Rect intersectRect = mIntersectRect;
+ region.set(x, y, x + (length << level), y + (length << level));
+ intersectRect.set(0, 0, mImageWidth, mImageHeight);
+
+ // Get the intersected rect of the requested region and the image.
+ Utils.assertTrue(intersectRect.intersect(region));
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+
+ Bitmap bitmap;
+
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (mRegionDecoder) {
+ bitmap = mRegionDecoder.decodeRegion(intersectRect, options);
+ }
+
+ // The returned region may not match with the targetLength.
+ // If so, we fill black pixels on it.
+ if (intersectRect.equals(region)) return bitmap;
+
+ Bitmap tile = Bitmap.createBitmap(length, length, Config.ARGB_8888);
+ Canvas canvas = new Canvas(tile);
+ canvas.drawBitmap(bitmap,
+ (intersectRect.left - region.left) >> level,
+ (intersectRect.top - region.top) >> level, null);
+ bitmap.recycle();
+ return tile;
+ }
+
+ @Override
+ public Bitmap getBackupImage() {
+ return mBackupImage;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mLevelCount;
+ }
+
+ public void setFailedToLoad() {
+ mFailedToLoad = true;
+ }
+
+ @Override
+ public boolean isFailedToLoad() {
+ return mFailedToLoad;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
new file mode 100644
index 000000000..b063824d2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UploadedTexture.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.opengl.GLUtils;
+
+import java.util.HashMap;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+// UploadedTextures use a Bitmap for the content of the texture.
+//
+// Subclasses should implement onGetBitmap() to provide the Bitmap and
+// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
+// is not needed anymore.
+//
+// isContentValid() is meaningful only when the isLoaded() returns true.
+// It means whether the content needs to be updated.
+//
+// The user of this class should call recycle() when the texture is not
+// needed anymore.
+//
+// By default an UploadedTexture is opaque (so it can be drawn faster without
+// blending). The user or subclass can override it using setOpaque().
+abstract class UploadedTexture extends BasicTexture {
+
+ // To prevent keeping allocation the borders, we store those used borders here.
+ // Since the length will be power of two, it won't use too much memory.
+ private static HashMap<BorderKey, Bitmap> sBorderLines =
+ new HashMap<BorderKey, Bitmap>();
+ private static BorderKey sBorderKey = new BorderKey();
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "Texture";
+ private boolean mContentValid = true;
+ private boolean mOpaque = true;
+ private boolean mThrottled = false;
+ private static int sUploadedCount;
+ private static final int UPLOAD_LIMIT = 100;
+
+ protected Bitmap mBitmap;
+
+ protected UploadedTexture() {
+ super(null, 0, STATE_UNLOADED);
+ }
+
+ private static class BorderKey implements Cloneable {
+ public boolean vertical;
+ public Config config;
+ public int length;
+
+ @Override
+ public int hashCode() {
+ int x = config.hashCode() ^ length;
+ return vertical ? x : -x;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof BorderKey)) return false;
+ BorderKey o = (BorderKey) object;
+ return vertical == o.vertical
+ && config == o.config && length == o.length;
+ }
+
+ @Override
+ public BorderKey clone() {
+ try {
+ return (BorderKey) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ protected void setThrottled(boolean throttled) {
+ mThrottled = throttled;
+ }
+
+ private static Bitmap getBorderLine(
+ boolean vertical, Config config, int length) {
+ BorderKey key = sBorderKey;
+ key.vertical = vertical;
+ key.config = config;
+ key.length = length;
+ Bitmap bitmap = sBorderLines.get(key);
+ if (bitmap == null) {
+ bitmap = vertical
+ ? Bitmap.createBitmap(1, length, config)
+ : Bitmap.createBitmap(length, 1, config);
+ sBorderLines.put(key.clone(), bitmap);
+ }
+ return bitmap;
+ }
+
+ private Bitmap getBitmap() {
+ if (mBitmap == null) {
+ mBitmap = onGetBitmap();
+ if (mWidth == UNSPECIFIED) {
+ setSize(mBitmap.getWidth(), mBitmap.getHeight());
+ } else if (mWidth != mBitmap.getWidth()
+ || mHeight != mBitmap.getHeight()) {
+ throw new IllegalStateException(String.format(
+ "cannot change size: this = %s, orig = %sx%s, new = %sx%s",
+ toString(), mWidth, mHeight, mBitmap.getWidth(),
+ mBitmap.getHeight()));
+ }
+ }
+ return mBitmap;
+ }
+
+ private void freeBitmap() {
+ Utils.assertTrue(mBitmap != null);
+ onFreeBitmap(mBitmap);
+ mBitmap = null;
+ }
+
+ @Override
+ public int getWidth() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mHeight;
+ }
+
+ protected abstract Bitmap onGetBitmap();
+
+ protected abstract void onFreeBitmap(Bitmap bitmap);
+
+ protected void invalidateContent() {
+ if (mBitmap != null) freeBitmap();
+ mContentValid = false;
+ }
+
+ /**
+ * Whether the content on GPU is valid.
+ */
+ public boolean isContentValid(GLCanvas canvas) {
+ return isLoaded(canvas) && mContentValid;
+ }
+
+ /**
+ * Updates the content on GPU's memory.
+ * @param canvas
+ */
+ public void updateContent(GLCanvas canvas) {
+ if (!isLoaded(canvas)) {
+ if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
+ return;
+ }
+ uploadToCanvas(canvas);
+ } else if (!mContentValid) {
+ Bitmap bitmap = getBitmap();
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ canvas.getGLInstance().glBindTexture(GL11.GL_TEXTURE_2D, mId);
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, format, type);
+ freeBitmap();
+ mContentValid = true;
+ }
+ }
+
+ public static void resetUploadLimit() {
+ sUploadedCount = 0;
+ }
+
+ public static boolean uploadLimitReached() {
+ return sUploadedCount > UPLOAD_LIMIT;
+ }
+
+ static int[] sTextureId = new int[1];
+ static float[] sCropRect = new float[4];
+
+ private void uploadToCanvas(GLCanvas canvas) {
+ GL11 gl = canvas.getGLInstance();
+
+ Bitmap bitmap = getBitmap();
+ if (bitmap != null) {
+ try {
+ // Define a vertically flipped crop rectangle for
+ // OES_draw_texture.
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ sCropRect[0] = 0;
+ sCropRect[1] = height;
+ sCropRect[2] = width;
+ sCropRect[3] = -height;
+
+ // Upload the bitmap to a new texture.
+ gl.glGenTextures(1, sTextureId, 0);
+ gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]);
+ gl.glTexParameterfv(GL11.GL_TEXTURE_2D,
+ GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+
+ if (width == getTextureWidth() && height == getTextureHeight()) {
+ GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
+ } else {
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ Config config = bitmap.getConfig();
+
+ gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format,
+ getTextureWidth(), getTextureHeight(),
+ 0, format, type, null);
+ GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap,
+ format, type);
+
+ if (width != getTextureWidth()) {
+ Bitmap line = getBorderLine(true, config, getTextureHeight());
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, width, 0, line, format, type);
+ }
+
+ if (height != getTextureHeight()) {
+ Bitmap line = getBorderLine(false, config, getTextureWidth());
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, 0, height, line, format, type);
+ }
+
+ }
+ } finally {
+ freeBitmap();
+ }
+ // Update texture state.
+ setAssociatedCanvas(canvas);
+ mId = sTextureId[0];
+ mState = UploadedTexture.STATE_LOADED;
+ mContentValid = true;
+ } else {
+ mState = STATE_ERROR;
+ throw new RuntimeException("Texture load fail, no bitmap");
+ }
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ updateContent(canvas);
+ return isContentValid(canvas);
+ }
+
+ public void setOpaque(boolean isOpaque) {
+ mOpaque = isOpaque;
+ }
+
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ if (mBitmap != null) freeBitmap();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java
new file mode 100644
index 000000000..bc4a71800
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UserInteractionListener.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.ui;
+
+public interface UserInteractionListener {
+ // Called when a user interaction begins (for example, fling).
+ public void onUserInteractionBegin();
+ // Called when the user interaction ends.
+ public void onUserInteractionEnd();
+ // Other one-shot user interactions.
+ public void onUserInteraction();
+}
diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java
new file mode 100644
index 000000000..fcc444e98
--- /dev/null
+++ b/src/com/android/gallery3d/util/CacheManager.java
@@ -0,0 +1,82 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.BlobCache;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+public class CacheManager {
+ private static final String TAG = "CacheManager";
+ private static final String KEY_CACHE_UP_TO_DATE = "cache-up-to-date";
+ private static HashMap<String, BlobCache> sCacheMap =
+ new HashMap<String, BlobCache>();
+ private static boolean sOldCheckDone = false;
+
+ // Return null when we cannot instantiate a BlobCache, e.g.:
+ // there is no SD card found.
+ // This can only be called from data thread.
+ public static BlobCache getCache(Context context, String filename,
+ int maxEntries, int maxBytes, int version) {
+ synchronized (sCacheMap) {
+ if (!sOldCheckDone) {
+ removeOldFilesIfNecessary(context);
+ sOldCheckDone = true;
+ }
+ BlobCache cache = sCacheMap.get(filename);
+ if (cache == null) {
+ File cacheDir = context.getExternalCacheDir();
+ String path = cacheDir.getAbsolutePath() + "/" + filename;
+ try {
+ cache = new BlobCache(path, maxEntries, maxBytes, false,
+ version);
+ sCacheMap.put(filename, cache);
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot instantiate cache!", e);
+ }
+ }
+ return cache;
+ }
+ }
+
+ // Removes the old files if the data is wiped.
+ private static void removeOldFilesIfNecessary(Context context) {
+ SharedPreferences pref = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ int n = 0;
+ try {
+ n = pref.getInt(KEY_CACHE_UP_TO_DATE, 0);
+ } catch (Throwable t) {
+ // ignore.
+ }
+ if (n != 0) return;
+ pref.edit().putInt(KEY_CACHE_UP_TO_DATE, 1).commit();
+
+ File cacheDir = context.getExternalCacheDir();
+ String prefix = cacheDir.getAbsolutePath() + "/";
+
+ BlobCache.deleteFiles(prefix + "imgcache");
+ BlobCache.deleteFiles(prefix + "rev_geocoding");
+ BlobCache.deleteFiles(prefix + "bookmark");
+ }
+}
diff --git a/src/com/android/gallery3d/util/Future.java b/src/com/android/gallery3d/util/Future.java
new file mode 100644
index 000000000..580a2a120
--- /dev/null
+++ b/src/com/android/gallery3d/util/Future.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util;
+
+// This Future differs from the java.util.concurrent.Future in these aspects:
+//
+// - Once cancel() is called, isCancelled() always returns true. It is a sticky
+// flag used to communicate to the implementation. The implmentation may
+// ignore that flag. Regardless whether the Future is cancelled, a return
+// value will be provided to get(). The implementation may choose to return
+// null if it finds the Future is cancelled.
+//
+// - get() does not throw exceptions.
+//
+public interface Future<T> {
+ public void cancel();
+ public boolean isCancelled();
+ public boolean isDone();
+ public T get();
+ public void waitDone();
+}
diff --git a/src/com/android/gallery3d/util/FutureListener.java b/src/com/android/gallery3d/util/FutureListener.java
new file mode 100644
index 000000000..ed1f820c7
--- /dev/null
+++ b/src/com/android/gallery3d/util/FutureListener.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.util;
+
+public interface FutureListener<T> {
+ public void onFutureDone(Future<T> future);
+}
diff --git a/src/com/android/gallery3d/util/FutureTask.java b/src/com/android/gallery3d/util/FutureTask.java
new file mode 100644
index 000000000..9cfab27cb
--- /dev/null
+++ b/src/com/android/gallery3d/util/FutureTask.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.util;
+
+import java.util.concurrent.Callable;
+
+// NOTE: If the Callable throws any Throwable, the result value will be null.
+public class FutureTask<T> implements Runnable, Future<T> {
+ private static final String TAG = "FutureTask";
+ private Callable<T> mCallable;
+ private FutureListener<T> mListener;
+ private volatile boolean mIsCancelled;
+ private boolean mIsDone;
+ private T mResult;
+
+ public FutureTask(Callable<T> callable, FutureListener<T> listener) {
+ mCallable = callable;
+ mListener = listener;
+ }
+
+ public FutureTask(Callable<T> callable) {
+ this(callable, null);
+ }
+
+ public void cancel() {
+ mIsCancelled = true;
+ }
+
+ public synchronized T get() {
+ while (!mIsDone) {
+ try {
+ wait();
+ } catch (InterruptedException t) {
+ // ignore.
+ }
+ }
+ return mResult;
+ }
+
+ public void waitDone() {
+ get();
+ }
+
+ public synchronized boolean isDone() {
+ return mIsDone;
+ }
+
+ public boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ public void run() {
+ T result = null;
+
+ if (!mIsCancelled) {
+ try {
+ result = mCallable.call();
+ } catch (Throwable ex) {
+ Log.w(TAG, "Exception in running a task", ex);
+ }
+ }
+
+ synchronized(this) {
+ mResult = result;
+ mIsDone = true;
+ if (mListener != null) {
+ mListener.onFutureDone(this);
+ }
+ notifyAll();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
new file mode 100644
index 000000000..2fed46a22
--- /dev/null
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -0,0 +1,327 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Environment;
+import android.os.StatFs;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class GalleryUtils {
+ private static final String TAG = "GalleryUtils";
+ private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
+ private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
+
+ private static final String MIME_TYPE_IMAGE = "image/*";
+ private static final String MIME_TYPE_VIDEO = "video/*";
+ private static final String MIME_TYPE_ALL = "*/*";
+
+ private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-";
+ private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-";
+
+ private static final String KEY_CAMERA_UPDATE = "camera-update";
+ private static final String KEY_HAS_CAMERA = "has-camera";
+
+ private static Context sContext;
+
+
+ static float sPixelDensity = -1f;
+
+ public static void initialize(Context context) {
+ sContext = context;
+ if (sPixelDensity < 0) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sPixelDensity = metrics.density;
+ }
+ }
+
+ public static float dpToPixel(float dp) {
+ return sPixelDensity * dp;
+ }
+
+ public static int dpToPixel(int dp) {
+ return Math.round(dpToPixel((float) dp));
+ }
+
+ public static int meterToPixel(float meter) {
+ // 1 meter = 39.37 inches, 1 inch = 160 dp.
+ return Math.round(dpToPixel(meter * 39.37f * 160));
+ }
+
+ public static byte[] getBytes(String in) {
+ byte[] result = new byte[in.length() * 2];
+ int output = 0;
+ for (char ch : in.toCharArray()) {
+ result[output++] = (byte) (ch & 0xFF);
+ result[output++] = (byte) (ch >> 8);
+ }
+ return result;
+ }
+
+ // Below are used the detect using database in the render thread. It only
+ // works most of the time, but that's ok because it's for debugging only.
+
+ private static volatile Thread sCurrentThread;
+ private static volatile boolean sWarned;
+
+ public static void setRenderThread() {
+ sCurrentThread = Thread.currentThread();
+ }
+
+ public static void assertNotInRenderThread() {
+ if (!sWarned) {
+ if (Thread.currentThread() == sCurrentThread) {
+ sWarned = true;
+ Log.w(TAG, new Throwable("Should not do this in render thread"));
+ }
+ }
+ }
+
+ private static final double RAD_PER_DEG = Math.PI / 180.0;
+ private static final double EARTH_RADIUS_METERS = 6367000.0;
+
+ public static double fastDistanceMeters(double latRad1, double lngRad1,
+ double latRad2, double lngRad2) {
+ if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG)
+ || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) {
+ return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2);
+ }
+ // Approximate sin(x) = x.
+ double sineLat = (latRad1 - latRad2);
+
+ // Approximate sin(x) = x.
+ double sineLng = (lngRad1 - lngRad2);
+
+ // Approximate cos(lat1) * cos(lat2) using
+ // cos((lat1 + lat2)/2) ^ 2
+ double cosTerms = Math.cos((latRad1 + latRad2) / 2.0);
+ cosTerms = cosTerms * cosTerms;
+ double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng;
+ trigTerm = Math.sqrt(trigTerm);
+
+ // Approximate arcsin(x) = x
+ return EARTH_RADIUS_METERS * trigTerm;
+ }
+
+ public static double accurateDistanceMeters(double lat1, double lng1,
+ double lat2, double lng2) {
+ double dlat = Math.sin(0.5 * (lat2 - lat1));
+ double dlng = Math.sin(0.5 * (lng2 - lng1));
+ double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2);
+ return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0,
+ 1.0 - x)))) * EARTH_RADIUS_METERS;
+ }
+
+
+ public static final double toMile(double meter) {
+ return meter / 1609;
+ }
+
+ // For debugging, it will block the caller for timeout millis.
+ public static void fakeBusy(JobContext jc, int timeout) {
+ final ConditionVariable cv = new ConditionVariable();
+ jc.setCancelListener(new CancelListener() {
+ public void onCancel() {
+ cv.open();
+ }
+ });
+ cv.block(timeout);
+ jc.setCancelListener(null);
+ }
+
+ public static boolean isEditorAvailable(Context context, String mimeType) {
+ int version = PackagesMonitor.getPackagesVersion(context);
+
+ String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType;
+ String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (prefs.getInt(updateKey, 0) != version) {
+ PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> infos = packageManager.queryIntentActivities(
+ new Intent(Intent.ACTION_EDIT).setType(mimeType), 0);
+ prefs.edit().putInt(updateKey, version)
+ .putBoolean(hasKey, !infos.isEmpty())
+ .commit();
+ }
+
+ return prefs.getBoolean(hasKey, true);
+ }
+
+ public static boolean isCameraAvailable(Context context) {
+ int version = PackagesMonitor.getPackagesVersion(context);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) {
+ PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> infos = packageManager.queryIntentActivities(
+ new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0);
+ prefs.edit().putInt(KEY_CAMERA_UPDATE, version)
+ .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty())
+ .commit();
+ }
+ return prefs.getBoolean(KEY_HAS_CAMERA, true);
+ }
+
+ public static boolean isValidLocation(double latitude, double longitude) {
+ // TODO: change || to && after we fix the default location issue
+ return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG);
+ }
+ public static void showOnMap(Context context, double latitude, double longitude) {
+ try {
+ // We don't use "geo:latitude,longitude" because it only centers
+ // the MapView to the specified location, but we need a marker
+ // for further operations (routing to/from).
+ // The q=(lat, lng) syntax is suggested by geo-team.
+ String uri = String.format("http://maps.google.com/maps?f=q&q=(%f,%f)",
+ latitude, longitude);
+ ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME,
+ MAPS_CLASS_NAME);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(uri)).setComponent(compName);
+ context.startActivity(mapsIntent);
+ } catch (ActivityNotFoundException e) {
+ // Use the "geo intent" if no GMM is installed
+ Log.e(TAG, "GMM activity not found!", e);
+ String url = String.format("geo:%f,%f", latitude, longitude);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(mapsIntent);
+ }
+ }
+
+ public static void setViewPointMatrix(
+ float matrix[], float x, float y, float z) {
+ // The matrix is
+ // -z, 0, x, 0
+ // 0, -z, y, 0
+ // 0, 0, 1, 0
+ // 0, 0, 1, -z
+ Arrays.fill(matrix, 0, 16, 0);
+ matrix[0] = matrix[5] = matrix[15] = -z;
+ matrix[8] = x;
+ matrix[9] = y;
+ matrix[10] = matrix[11] = 1;
+ }
+
+ public static int getBucketId(String path) {
+ return path.toLowerCase().hashCode();
+ }
+
+ // Returns a (localized) string for the given duration (in seconds).
+ public static String formatDuration(final Context context, int duration) {
+ int h = duration / 3600;
+ int m = (duration - h * 3600) / 60;
+ int s = duration - (h * 3600 + m * 60);
+ String durationValue;
+ if (h == 0) {
+ durationValue = String.format(context.getString(R.string.details_ms), m, s);
+ } else {
+ durationValue = String.format(context.getString(R.string.details_hms), h, m, s);
+ }
+ return durationValue;
+ }
+
+ public static void setSpinnerVisibility(final Activity activity,
+ final boolean visible) {
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ activity.setProgressBarIndeterminateVisibility(visible);
+ }
+ });
+ }
+
+ public static int determineTypeBits(Context context, Intent intent) {
+ int typeBits = 0;
+ String type = intent.resolveType(context);
+ if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
+ if (MIME_TYPE_ALL.equals(type)) {
+ typeBits = DataManager.INCLUDE_LOCAL_ALL_ONLY;
+ } else if (MIME_TYPE_IMAGE.equals(type)) {
+ typeBits = DataManager.INCLUDE_LOCAL_IMAGE_ONLY;
+ } else if (MIME_TYPE_VIDEO.equals(type)) {
+ typeBits = DataManager.INCLUDE_LOCAL_VIDEO_ONLY;
+ }
+ } else {
+ if (MIME_TYPE_ALL.equals(type)) {
+ typeBits = DataManager.INCLUDE_ALL;
+ } else if (MIME_TYPE_IMAGE.equals(type)) {
+ typeBits = DataManager.INCLUDE_IMAGE;
+ } else if (MIME_TYPE_VIDEO.equals(type)) {
+ typeBits = DataManager.INCLUDE_VIDEO;
+ }
+ }
+ if (typeBits == 0) typeBits = DataManager.INCLUDE_ALL;
+
+ return typeBits;
+ }
+
+ public static int getSelectionModePrompt(int typeBits) {
+ if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
+ return (typeBits & DataManager.INCLUDE_IMAGE) == 0
+ ? R.string.select_video
+ : R.string.select_item;
+ }
+ return R.string.select_image;
+ }
+
+ public static boolean hasSpaceForSize(long size) {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return false;
+ }
+
+ String path = Environment.getExternalStorageDirectory().getPath();
+ try {
+ StatFs stat = new StatFs(path);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return false;
+ }
+
+ public static void assertInMainThread() {
+ if (Thread.currentThread() == sContext.getMainLooper().getThread()) {
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java
new file mode 100644
index 000000000..02a46aef7
--- /dev/null
+++ b/src/com/android/gallery3d/util/IdentityCache.java
@@ -0,0 +1,74 @@
+/*
+ * 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.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Set;
+
+public class IdentityCache<K, V> {
+
+ private final HashMap<K, Entry<K, V>> mWeakMap =
+ new HashMap<K, Entry<K, V>>();
+ private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+ public IdentityCache() {
+ }
+
+ private static class Entry<K, V> extends WeakReference<V> {
+ K mKey;
+
+ public Entry(K key, V value, ReferenceQueue<V> queue) {
+ super(value, queue);
+ mKey = key;
+ }
+ }
+
+ private void cleanUpWeakMap() {
+ Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+ while (entry != null) {
+ mWeakMap.remove(entry.mKey);
+ entry = (Entry<K, V>) mQueue.poll();
+ }
+ }
+
+ public synchronized V put(K key, V value) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.put(
+ key, new Entry<K, V>(key, value, mQueue));
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized V get(K key) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.get(key);
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized void clear() {
+ mWeakMap.clear();
+ mQueue = new ReferenceQueue<V>();
+ }
+
+ public synchronized ArrayList<K> keys() {
+ Set<K> set = mWeakMap.keySet();
+ ArrayList<K> result = new ArrayList<K>(set);
+ return result;
+ }
+}
diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java
new file mode 100644
index 000000000..88657bbd6
--- /dev/null
+++ b/src/com/android/gallery3d/util/IntArray.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.util;
+
+public class IntArray {
+ private static final int INIT_CAPACITY = 8;
+
+ private int mData[] = new int[INIT_CAPACITY];
+ private int mSize = 0;
+
+ public void add(int value) {
+ if (mData.length == mSize) {
+ int temp[] = new int[mSize + mSize];
+ System.arraycopy(mData, 0, temp, 0, mSize);
+ mData = temp;
+ }
+ mData[mSize++] = value;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public int[] toArray(int[] result) {
+ if (result == null || result.length < mSize) {
+ result = new int[mSize];
+ }
+ System.arraycopy(mData, 0, result, 0, mSize);
+ return result;
+ }
+
+ public int[] getInternalArray() {
+ return mData;
+ }
+
+ public void clear() {
+ mSize = 0;
+ if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
+ }
+}
diff --git a/src/com/android/gallery3d/util/InterruptableOutputStream.java b/src/com/android/gallery3d/util/InterruptableOutputStream.java
new file mode 100644
index 000000000..1ab62ab98
--- /dev/null
+++ b/src/com/android/gallery3d/util/InterruptableOutputStream.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+public class InterruptableOutputStream extends OutputStream {
+
+ private static final int MAX_WRITE_BYTES = 4096;
+
+ private OutputStream mOutputStream;
+ private volatile boolean mIsInterrupted = false;
+
+ public InterruptableOutputStream(OutputStream outputStream) {
+ mOutputStream = Utils.checkNotNull(outputStream);
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ mOutputStream.write(oneByte);
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int count) throws IOException {
+ int end = offset + count;
+ while (offset < end) {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ int bytesCount = Math.min(MAX_WRITE_BYTES, end - offset);
+ mOutputStream.write(buffer, offset, bytesCount);
+ offset += bytesCount;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ mOutputStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ mOutputStream.flush();
+ }
+
+ public void interrupt() {
+ mIsInterrupted = true;
+ }
+}
diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java
new file mode 100644
index 000000000..8554acd21
--- /dev/null
+++ b/src/com/android/gallery3d/util/LinkedNode.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.util;
+
+
+public class LinkedNode {
+ private LinkedNode mPrev;
+ private LinkedNode mNext;
+
+ public LinkedNode() {
+ mPrev = mNext = this;
+ }
+
+ public void insert(LinkedNode node) {
+ node.mNext = mNext;
+ mNext.mPrev = node;
+ node.mPrev = this;
+ mNext = node;
+ }
+
+ public void remove() {
+ if (mNext == this) throw new IllegalStateException();
+ mPrev.mNext = mNext;
+ mNext.mPrev = mPrev;
+ mPrev = mNext = null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static class List<T extends LinkedNode> {
+ private LinkedNode mHead = new LinkedNode();
+
+ public void insertFirst(T node) {
+ mHead.insert(node);
+ }
+
+ public void insertLast(T node) {
+ mHead.mPrev.insert(node);
+ }
+
+ public T getFirst() {
+ return (T) (mHead.mNext == mHead ? null : mHead.mNext);
+ }
+
+ public T getLast() {
+ return (T) (mHead.mPrev == mHead ? null : mHead.mPrev);
+ }
+
+ public T nextOf(T node) {
+ return (T) (node.mNext == mHead ? null : node.mNext);
+ }
+
+ public T previousOf(T node) {
+ return (T) (node.mPrev == mHead ? null : node.mPrev);
+ }
+
+ }
+
+ public static <T extends LinkedNode> List<T> newList() {
+ return new List<T>();
+ }
+}
diff --git a/src/com/android/gallery3d/util/Log.java b/src/com/android/gallery3d/util/Log.java
new file mode 100644
index 000000000..d7f8e85d0
--- /dev/null
+++ b/src/com/android/gallery3d/util/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.util;
+
+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/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java
new file mode 100644
index 000000000..817ffedcb
--- /dev/null
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MtpContext;
+import com.android.gallery3d.data.Path;
+
+import android.os.Environment;
+
+import java.util.Comparator;
+
+public class MediaSetUtils {
+ public static final Comparator<MediaSet> NAME_COMPARATOR = new NameComparator();
+
+ public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera");
+ public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/download");
+ public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + MtpContext.NAME_IMPORTED_FOLDER);
+
+ private static final Path[] CAMERA_PATHS = {
+ Path.fromString("/local/all/" + CAMERA_BUCKET_ID),
+ Path.fromString("/local/image/" + CAMERA_BUCKET_ID),
+ Path.fromString("/local/video/" + CAMERA_BUCKET_ID)};
+
+ public static boolean isCameraSource(Path path) {
+ return CAMERA_PATHS[0] == path || CAMERA_PATHS[1] == path
+ || CAMERA_PATHS[2] == path;
+ }
+
+ // Sort MediaSets by name
+ public static class NameComparator implements Comparator<MediaSet> {
+ public int compare(MediaSet set1, MediaSet set2) {
+ int result = set1.getName().compareToIgnoreCase(set2.getName());
+ if (result != 0) return result;
+ return set1.getPath().toString().compareTo(set2.getPath().toString());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/util/PriorityThreadFactory.java b/src/com/android/gallery3d/util/PriorityThreadFactory.java
new file mode 100644
index 000000000..67b215274
--- /dev/null
+++ b/src/com/android/gallery3d/util/PriorityThreadFactory.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.util;
+
+
+import android.os.Process;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A thread factory that creates threads with a given thread priority.
+ */
+public class PriorityThreadFactory implements ThreadFactory {
+
+ private final int mPriority;
+ private final AtomicInteger mNumber = new AtomicInteger();
+ private final String mName;
+
+ public PriorityThreadFactory(String name, int priority) {
+ mName = name;
+ mPriority = priority;
+ }
+
+ public Thread newThread(Runnable r) {
+ return new Thread(r, mName + '-' + mNumber.getAndIncrement()) {
+ @Override
+ public void run() {
+ Process.setThreadPriority(mPriority);
+ super.run();
+ }
+ };
+ }
+
+}
diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java
new file mode 100644
index 000000000..d253b4b96
--- /dev/null
+++ b/src/com/android/gallery3d/util/ReverseGeocoder.java
@@ -0,0 +1,417 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.BlobCache;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public class ReverseGeocoder {
+ private static final String TAG = "ReverseGeocoder";
+ public static final int EARTH_RADIUS_METERS = 6378137;
+ public static final int LAT_MIN = -90;
+ public static final int LAT_MAX = 90;
+ public static final int LON_MIN = -180;
+ public static final int LON_MAX = 180;
+ private static final int MAX_COUNTRY_NAME_LENGTH = 8;
+ // If two points are within 20 miles of each other, use
+ // "Around Palo Alto, CA" or "Around Mountain View, CA".
+ // instead of directly jumping to the next level and saying
+ // "California, US".
+ private static final int MAX_LOCALITY_MILE_RANGE = 20;
+
+ private static final String GEO_CACHE_FILE = "rev_geocoding";
+ private static final int GEO_CACHE_MAX_ENTRIES = 1000;
+ private static final int GEO_CACHE_MAX_BYTES = 500 * 1024;
+ private static final int GEO_CACHE_VERSION = 0;
+
+ public static class SetLatLong {
+ // The latitude and longitude of the min latitude point.
+ public double mMinLatLatitude = LAT_MAX;
+ public double mMinLatLongitude;
+ // The latitude and longitude of the max latitude point.
+ public double mMaxLatLatitude = LAT_MIN;
+ public double mMaxLatLongitude;
+ // The latitude and longitude of the min longitude point.
+ public double mMinLonLatitude;
+ public double mMinLonLongitude = LON_MAX;
+ // The latitude and longitude of the max longitude point.
+ public double mMaxLonLatitude;
+ public double mMaxLonLongitude = LON_MIN;
+ }
+
+ private Context mContext;
+ private Geocoder mGeocoder;
+ private BlobCache mGeoCache;
+ private ConnectivityManager mConnectivityManager;
+ private static Address sCurrentAddress; // last known address
+
+ public ReverseGeocoder(Context context) {
+ mContext = context;
+ mGeocoder = new Geocoder(mContext);
+ mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE,
+ GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES,
+ GEO_CACHE_VERSION);
+ mConnectivityManager = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ public String computeAddress(SetLatLong set) {
+ // The overall min and max latitudes and longitudes of the set.
+ double setMinLatitude = set.mMinLatLatitude;
+ double setMinLongitude = set.mMinLatLongitude;
+ double setMaxLatitude = set.mMaxLatLatitude;
+ double setMaxLongitude = set.mMaxLatLongitude;
+ if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude)
+ < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) {
+ setMinLatitude = set.mMinLonLatitude;
+ setMinLongitude = set.mMinLonLongitude;
+ setMaxLatitude = set.mMaxLonLatitude;
+ setMaxLongitude = set.mMaxLonLongitude;
+ }
+ Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true);
+ Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true);
+ if (addr1 == null)
+ addr1 = addr2;
+ if (addr2 == null)
+ addr2 = addr1;
+ if (addr1 == null || addr2 == null) {
+ return null;
+ }
+
+ // Get current location, we decide the granularity of the string based
+ // on this.
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ Location location = null;
+ List<String> providers = locationManager.getAllProviders();
+ for (int i = 0; i < providers.size(); ++i) {
+ String provider = providers.get(i);
+ location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null;
+ if (location != null)
+ break;
+ }
+ String currentCity = "";
+ String currentAdminArea = "";
+ String currentCountry = Locale.getDefault().getCountry();
+ if (location != null) {
+ Address currentAddress = lookupAddress(
+ location.getLatitude(), location.getLongitude(), true);
+ if (currentAddress == null) {
+ currentAddress = sCurrentAddress;
+ } else {
+ sCurrentAddress = currentAddress;
+ }
+ if (currentAddress != null && currentAddress.getCountryCode() != null) {
+ currentCity = checkNull(currentAddress.getLocality());
+ currentCountry = checkNull(currentAddress.getCountryCode());
+ currentAdminArea = checkNull(currentAddress.getAdminArea());
+ }
+ }
+
+ String closestCommonLocation = null;
+ String addr1Locality = checkNull(addr1.getLocality());
+ String addr2Locality = checkNull(addr2.getLocality());
+ String addr1AdminArea = checkNull(addr1.getAdminArea());
+ String addr2AdminArea = checkNull(addr2.getAdminArea());
+ String addr1CountryCode = checkNull(addr1.getCountryCode());
+ String addr2CountryCode = checkNull(addr2.getCountryCode());
+
+ if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) {
+ String otherCity = currentCity;
+ if (currentCity.equals(addr1Locality)) {
+ otherCity = addr2Locality;
+ if (otherCity.length() == 0) {
+ otherCity = addr2AdminArea;
+ if (!currentCountry.equals(addr2CountryCode)) {
+ otherCity += " " + addr2CountryCode;
+ }
+ }
+ addr2Locality = addr1Locality;
+ addr2AdminArea = addr1AdminArea;
+ addr2CountryCode = addr1CountryCode;
+ } else {
+ otherCity = addr1Locality;
+ if (otherCity.length() == 0) {
+ otherCity = addr1AdminArea;
+ if (!currentCountry.equals(addr1CountryCode)) {
+ otherCity += " " + addr1CountryCode;
+ }
+ }
+ addr1Locality = addr2Locality;
+ addr1AdminArea = addr2AdminArea;
+ addr1CountryCode = addr2CountryCode;
+ }
+ closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
+ if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+ if (!currentCity.equals(otherCity)) {
+ closestCommonLocation += " - " + otherCity;
+ }
+ return closestCommonLocation;
+ }
+
+ // Compare thoroughfare (street address) next.
+ closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
+ if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+ return closestCommonLocation;
+ }
+ }
+
+ // Compare the locality.
+ closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ String adminArea = addr1AdminArea;
+ String countryCode = addr1CountryCode;
+ if (adminArea != null && adminArea.length() > 0) {
+ if (!countryCode.equals(currentCountry)) {
+ closestCommonLocation += ", " + adminArea + " " + countryCode;
+ } else {
+ closestCommonLocation += ", " + adminArea;
+ }
+ }
+ return closestCommonLocation;
+ }
+
+ // If the admin area is the same as the current location, we hide it and
+ // instead show the city name.
+ if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) {
+ if ("".equals(addr1Locality)) {
+ addr1Locality = addr2Locality;
+ }
+ if ("".equals(addr2Locality)) {
+ addr2Locality = addr1Locality;
+ }
+ if (!"".equals(addr1Locality)) {
+ if (addr1Locality.equals(addr2Locality)) {
+ closestCommonLocation = addr1Locality + ", " + currentAdminArea;
+ } else {
+ closestCommonLocation = addr1Locality + " - " + addr2Locality;
+ }
+ return closestCommonLocation;
+ }
+ }
+
+ // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE
+ // mile radius.
+ float[] distanceFloat = new float[1];
+ Location.distanceBetween(setMinLatitude, setMinLongitude,
+ setMaxLatitude, setMaxLongitude, distanceFloat);
+ int distance = (int) GalleryUtils.toMile(distanceFloat[0]);
+ if (distance < MAX_LOCALITY_MILE_RANGE) {
+ // Try each of the points and just return the first one to have a
+ // valid address.
+ closestCommonLocation = getLocalityAdminForAddress(addr1, true);
+ if (closestCommonLocation != null) {
+ return closestCommonLocation;
+ }
+ closestCommonLocation = getLocalityAdminForAddress(addr2, true);
+ if (closestCommonLocation != null) {
+ return closestCommonLocation;
+ }
+ }
+
+ // Check the administrative area.
+ closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ String countryCode = addr1CountryCode;
+ if (!countryCode.equals(currentCountry)) {
+ if (countryCode != null && countryCode.length() > 0) {
+ closestCommonLocation += " " + countryCode;
+ }
+ }
+ return closestCommonLocation;
+ }
+
+ // Check the country codes.
+ closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ return closestCommonLocation;
+ }
+ // There is no intersection, let's choose a nicer name.
+ String addr1Country = addr1.getCountryName();
+ String addr2Country = addr2.getCountryName();
+ if (addr1Country == null)
+ addr1Country = addr1CountryCode;
+ if (addr2Country == null)
+ addr2Country = addr2CountryCode;
+ if (addr1Country == null || addr2Country == null)
+ return null;
+ if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
+ closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode;
+ } else {
+ closestCommonLocation = addr1Country + " - " + addr2Country;
+ }
+ return closestCommonLocation;
+ }
+
+ private String checkNull(String locality) {
+ if (locality == null)
+ return "";
+ if (locality.equals("null"))
+ return "";
+ return locality;
+ }
+
+ private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
+ if (addr == null)
+ return "";
+ String localityAdminStr = addr.getLocality();
+ if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
+ if (approxLocation) {
+ // TODO: Uncomment these lines as soon as we may translations
+ // for Res.string.around.
+ // localityAdminStr =
+ // mContext.getResources().getString(Res.string.around) + " " +
+ // localityAdminStr;
+ }
+ String adminArea = addr.getAdminArea();
+ if (adminArea != null && adminArea.length() > 0) {
+ localityAdminStr += ", " + adminArea;
+ }
+ return localityAdminStr;
+ }
+ return null;
+ }
+
+ public Address lookupAddress(final double latitude, final double longitude,
+ boolean useCache) {
+ try {
+ long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX
+ + (longitude + LON_MAX)) * EARTH_RADIUS_METERS);
+ byte[] cachedLocation = null;
+ if (useCache && mGeoCache != null) {
+ cachedLocation = mGeoCache.lookup(locationKey);
+ }
+ Address address = null;
+ NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo();
+ if (cachedLocation == null || cachedLocation.length == 0) {
+ if (networkInfo == null || !networkInfo.isConnected()) {
+ return null;
+ }
+ List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
+ if (!addresses.isEmpty()) {
+ address = addresses.get(0);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ Locale locale = address.getLocale();
+ writeUTF(dos, locale.getLanguage());
+ writeUTF(dos, locale.getCountry());
+ writeUTF(dos, locale.getVariant());
+
+ writeUTF(dos, address.getThoroughfare());
+ int numAddressLines = address.getMaxAddressLineIndex();
+ dos.writeInt(numAddressLines);
+ for (int i = 0; i < numAddressLines; ++i) {
+ writeUTF(dos, address.getAddressLine(i));
+ }
+ writeUTF(dos, address.getFeatureName());
+ writeUTF(dos, address.getLocality());
+ writeUTF(dos, address.getAdminArea());
+ writeUTF(dos, address.getSubAdminArea());
+
+ writeUTF(dos, address.getCountryName());
+ writeUTF(dos, address.getCountryCode());
+ writeUTF(dos, address.getPostalCode());
+ writeUTF(dos, address.getPhone());
+ writeUTF(dos, address.getUrl());
+
+ dos.flush();
+ if (mGeoCache != null) {
+ mGeoCache.insert(locationKey, bos.toByteArray());
+ }
+ dos.close();
+ }
+ } else {
+ // Parsing the address from the byte stream.
+ DataInputStream dis = new DataInputStream(
+ new ByteArrayInputStream(cachedLocation));
+ String language = readUTF(dis);
+ String country = readUTF(dis);
+ String variant = readUTF(dis);
+ Locale locale = null;
+ if (language != null) {
+ if (country == null) {
+ locale = new Locale(language);
+ } else if (variant == null) {
+ locale = new Locale(language, country);
+ } else {
+ locale = new Locale(language, country, variant);
+ }
+ }
+ if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ dis.close();
+ return lookupAddress(latitude, longitude, false);
+ }
+ address = new Address(locale);
+
+ address.setThoroughfare(readUTF(dis));
+ int numAddressLines = dis.readInt();
+ for (int i = 0; i < numAddressLines; ++i) {
+ address.setAddressLine(i, readUTF(dis));
+ }
+ address.setFeatureName(readUTF(dis));
+ address.setLocality(readUTF(dis));
+ address.setAdminArea(readUTF(dis));
+ address.setSubAdminArea(readUTF(dis));
+
+ address.setCountryName(readUTF(dis));
+ address.setCountryCode(readUTF(dis));
+ address.setPostalCode(readUTF(dis));
+ address.setPhone(readUTF(dis));
+ address.setUrl(readUTF(dis));
+ dis.close();
+ }
+ return address;
+ } catch (Exception e) {
+ // Ignore.
+ }
+ return null;
+ }
+
+ private String valueIfEqual(String a, String b) {
+ return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
+ }
+
+ public static final void writeUTF(DataOutputStream dos, String string) throws IOException {
+ if (string == null) {
+ dos.writeUTF("");
+ } else {
+ dos.writeUTF(string);
+ }
+ }
+
+ public static final String readUTF(DataInputStream dis) throws IOException {
+ String retVal = dis.readUTF();
+ if (retVal.length() == 0)
+ return null;
+ return retVal;
+ }
+}
diff --git a/src/com/android/gallery3d/util/ThreadPool.java b/src/com/android/gallery3d/util/ThreadPool.java
new file mode 100644
index 000000000..71bb3c5b7
--- /dev/null
+++ b/src/com/android/gallery3d/util/ThreadPool.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.util;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPool {
+ private static final String TAG = "ThreadPool";
+ private static final int CORE_POOL_SIZE = 4;
+ private static final int MAX_POOL_SIZE = 8;
+ private static final int KEEP_ALIVE_TIME = 10; // 10 seconds
+
+ // Resource type
+ public static final int MODE_NONE = 0;
+ public static final int MODE_CPU = 1;
+ public static final int MODE_NETWORK = 2;
+
+ public static final JobContext JOB_CONTEXT_STUB = new JobContextStub();
+
+ ResourceCounter mCpuCounter = new ResourceCounter(2);
+ ResourceCounter mNetworkCounter = new ResourceCounter(2);
+
+ // A Job is like a Callable, but it has an addition JobContext parameter.
+ public interface Job<T> {
+ public T run(JobContext jc);
+ }
+
+ public interface JobContext {
+ boolean isCancelled();
+ void setCancelListener(CancelListener listener);
+ boolean setMode(int mode);
+ }
+
+ private static class JobContextStub implements JobContext {
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void setCancelListener(CancelListener listener) {
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ return true;
+ }
+ }
+
+ public interface CancelListener {
+ public void onCancel();
+ }
+
+ private static class ResourceCounter {
+ public int value;
+ public ResourceCounter(int v) {
+ value = v;
+ }
+ }
+
+ private final Executor mExecutor;
+
+ public ThreadPool() {
+ mExecutor = new ThreadPoolExecutor(
+ CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
+ TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+ new PriorityThreadFactory("thread-pool",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND));
+ }
+
+ // Submit a job to the thread pool. The listener will be called when the
+ // job is finished (or cancelled).
+ public <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+ Worker<T> w = new Worker<T>(job, listener);
+ mExecutor.execute(w);
+ return w;
+ }
+
+ public <T> Future<T> submit(Job<T> job) {
+ return submit(job, null);
+ }
+
+ private class Worker<T> implements Runnable, Future<T>, JobContext {
+ private static final String TAG = "Worker";
+ private Job<T> mJob;
+ private FutureListener<T> mListener;
+ private CancelListener mCancelListener;
+ private ResourceCounter mWaitOnResource;
+ private volatile boolean mIsCancelled;
+ private boolean mIsDone;
+ private T mResult;
+ private int mMode;
+
+ public Worker(Job<T> job, FutureListener<T> listener) {
+ mJob = job;
+ mListener = listener;
+ }
+
+ // This is called by a thread in the thread pool.
+ public void run() {
+ T result = null;
+
+ // A job is in CPU mode by default. setMode returns false
+ // if the job is cancelled.
+ if (setMode(MODE_CPU)) {
+ try {
+ result = mJob.run(this);
+ } catch (Throwable ex) {
+ Log.w(TAG, "Exception in running a job", ex);
+ }
+ }
+
+ synchronized(this) {
+ setMode(MODE_NONE);
+ mResult = result;
+ mIsDone = true;
+ notifyAll();
+ }
+ if (mListener != null) mListener.onFutureDone(this);
+ }
+
+ // Below are the methods for Future.
+ public synchronized void cancel() {
+ if (mIsCancelled) return;
+ mIsCancelled = true;
+ if (mWaitOnResource != null) {
+ synchronized (mWaitOnResource) {
+ mWaitOnResource.notifyAll();
+ }
+ }
+ if (mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+
+ public boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ public synchronized boolean isDone() {
+ return mIsDone;
+ }
+
+ public synchronized T get() {
+ while (!mIsDone) {
+ try {
+ wait();
+ } catch (Exception ex) {
+ Log.w(TAG, "ingore exception", ex);
+ // ignore.
+ }
+ }
+ return mResult;
+ }
+
+ public void waitDone() {
+ get();
+ }
+
+ // Below are the methods for JobContext (only called from the
+ // thread running the job)
+ public synchronized void setCancelListener(CancelListener listener) {
+ mCancelListener = listener;
+ if (mIsCancelled && mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+
+ public boolean setMode(int mode) {
+ // Release old resource
+ ResourceCounter rc = modeToCounter(mMode);
+ if (rc != null) releaseResource(rc);
+ mMode = MODE_NONE;
+
+ // Acquire new resource
+ rc = modeToCounter(mode);
+ if (rc != null) {
+ if (!acquireResource(rc)) {
+ return false;
+ }
+ mMode = mode;
+ }
+
+ return true;
+ }
+
+ private ResourceCounter modeToCounter(int mode) {
+ if (mode == MODE_CPU) {
+ return mCpuCounter;
+ } else if (mode == MODE_NETWORK) {
+ return mNetworkCounter;
+ } else {
+ return null;
+ }
+ }
+
+ private boolean acquireResource(ResourceCounter counter) {
+ while (true) {
+ synchronized (this) {
+ if (mIsCancelled) {
+ mWaitOnResource = null;
+ return false;
+ }
+ mWaitOnResource = counter;
+ }
+
+ synchronized (counter) {
+ if (counter.value > 0) {
+ counter.value--;
+ break;
+ } else {
+ try {
+ counter.wait();
+ } catch (InterruptedException ex) {
+ // ignore.
+ }
+ }
+ }
+ }
+
+ synchronized (this) {
+ mWaitOnResource = null;
+ }
+
+ return true;
+ }
+
+ private void releaseResource(ResourceCounter counter) {
+ synchronized (counter) {
+ counter.value++;
+ counter.notifyAll();
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java
new file mode 100644
index 000000000..9fdade683
--- /dev/null
+++ b/src/com/android/gallery3d/util/UpdateHelper.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.Utils;
+
+public class UpdateHelper {
+
+ private boolean mUpdated = false;
+
+ public int update(int original, int update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public long update(long original, long update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public double update(double original, double update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public double update(float original, float update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public <T> T update(T original, T update) {
+ if (!Utils.equals(original, update)) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public boolean isUpdated() {
+ return mUpdated;
+ }
+}
diff --git a/src/com/android/gallery3d/widget/LocalPhotoSource.java b/src/com/android/gallery3d/widget/LocalPhotoSource.java
new file mode 100644
index 000000000..de16a7129
--- /dev/null
+++ b/src/com/android/gallery3d/widget/LocalPhotoSource.java
@@ -0,0 +1,202 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.app.GalleryApp;
+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.Path;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.provider.MediaStore.Images.Media;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Random;
+
+public class LocalPhotoSource implements WidgetSource {
+
+ private static final String TAG = "LocalPhotoSource";
+
+ private static final int MAX_PHOTO_COUNT = 128;
+
+ /* Static fields used to query for the correct set of images */
+ private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI;
+ private static final String DATE_TAKEN = Media.DATE_TAKEN;
+ private static final String[] PROJECTION = {Media._ID};
+ private static final String[] COUNT_PROJECTION = {"count(*)"};
+ /* We don't want to include the download directory */
+ private static final String SELECTION =
+ String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId());
+ private static final String ORDER = String.format("%s DESC", DATE_TAKEN);
+
+ private Context mContext;
+ private ArrayList<Long> mPhotos = new ArrayList<Long>();
+ private ContentListener mContentListener;
+ private ContentObserver mContentObserver;
+ private boolean mContentDirty = true;
+ private DataManager mDataManager;
+ private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item");
+
+ public LocalPhotoSource(Context context) {
+ mContext = context;
+ mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager();
+ mContentObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ mContentDirty = true;
+ if (mContentListener != null) mContentListener.onContentDirty();
+ }
+ };
+ mContext.getContentResolver()
+ .registerContentObserver(CONTENT_URI, true, mContentObserver);
+ }
+
+ public void close() {
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+ }
+
+ @Override
+ public Uri getContentUri(int index) {
+ if (index < mPhotos.size()) {
+ return CONTENT_URI.buildUpon()
+ .appendPath(String.valueOf(mPhotos.get(index)))
+ .build();
+ }
+ return null;
+ }
+
+ @Override
+ public Bitmap getImage(int index) {
+ if (index >= mPhotos.size()) return null;
+ long id = mPhotos.get(index);
+ MediaItem image = (MediaItem)
+ mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id));
+ if (image == null) return null;
+
+ return WidgetUtils.createWidgetBitmap(image);
+ }
+
+ private int[] getExponentialIndice(int total, int count) {
+ Random random = new Random();
+ if (count > total) count = total;
+ HashSet<Integer> selected = new HashSet<Integer>(count);
+ while (selected.size() < count) {
+ int row = (int)(-Math.log(random.nextDouble()) * total / 2);
+ if (row < total) selected.add(row);
+ }
+ int values[] = new int[count];
+ int index = 0;
+ for (int value : selected) {
+ values[index++] = value;
+ }
+ return values;
+ }
+
+ private int getPhotoCount(ContentResolver resolver) {
+ Cursor cursor = resolver.query(
+ CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null);
+ if (cursor == null) return 0;
+ try {
+ Utils.assertTrue(cursor.moveToNext());
+ return cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private boolean isContentSound(int totalCount) {
+ if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false;
+ if (mPhotos.size() == 0) return true; // totalCount is also 0
+
+ StringBuilder builder = new StringBuilder();
+ for (Long imageId : mPhotos) {
+ if (builder.length() > 0) builder.append(",");
+ builder.append(imageId);
+ }
+ Cursor cursor = mContext.getContentResolver().query(
+ CONTENT_URI, COUNT_PROJECTION,
+ String.format("%s in (%s)", Media._ID, builder.toString()),
+ null, null);
+ if (cursor == null) return false;
+ try {
+ Utils.assertTrue(cursor.moveToNext());
+ return cursor.getInt(0) == mPhotos.size();
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public void reload() {
+ if (!mContentDirty) return;
+ mContentDirty = false;
+
+ ContentResolver resolver = mContext.getContentResolver();
+ int photoCount = getPhotoCount(resolver);
+ if (isContentSound(photoCount)) return;
+
+ int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT);
+ Arrays.sort(choosedIds);
+
+ mPhotos.clear();
+ Cursor cursor = mContext.getContentResolver().query(
+ CONTENT_URI, PROJECTION, SELECTION, null, ORDER);
+ if (cursor == null) return;
+ try {
+ for (int index : choosedIds) {
+ if (cursor.moveToPosition(index)) {
+ mPhotos.add(cursor.getLong(0));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public int size() {
+ reload();
+ return mPhotos.size();
+ }
+
+ /**
+ * Builds the bucket ID for the public external storage Downloads directory
+ * @return the bucket ID
+ */
+ private static int getDownloadBucketId() {
+ String downloadsPath = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ .getAbsolutePath();
+ return GalleryUtils.getBucketId(downloadsPath);
+ }
+
+ @Override
+ public void setContentListener(ContentListener listener) {
+ mContentListener = listener;
+ }
+}
diff --git a/src/com/android/gallery3d/widget/MediaSetSource.java b/src/com/android/gallery3d/widget/MediaSetSource.java
new file mode 100644
index 000000000..1677f69f1
--- /dev/null
+++ b/src/com/android/gallery3d/widget/MediaSetSource.java
@@ -0,0 +1,113 @@
+/*
+ * 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.widget;
+
+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 android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class MediaSetSource implements WidgetSource, ContentListener {
+ private static final int CACHE_SIZE = 32;
+
+ private static final String TAG = "MediaSetSource";
+
+ private MediaSet mSource;
+ private MediaItem mCache[] = new MediaItem[CACHE_SIZE];
+ private int mCacheStart;
+ private int mCacheEnd;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+ private ContentListener mContentListener;
+
+ public MediaSetSource(MediaSet source) {
+ mSource = Utils.checkNotNull(source);
+ mSource.addContentListener(this);
+ }
+
+ @Override
+ public void close() {
+ mSource.removeContentListener(this);
+ }
+
+ private void ensureCacheRange(int index) {
+ if (index >= mCacheStart && index < mCacheEnd) return;
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ mCacheStart = index;
+ ArrayList<MediaItem> items = mSource.getMediaItem(mCacheStart, CACHE_SIZE);
+ mCacheEnd = mCacheStart + items.size();
+ items.toArray(mCache);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public synchronized Uri getContentUri(int index) {
+ ensureCacheRange(index);
+ if (index < mCacheStart || index >= mCacheEnd) return null;
+ return mCache[index - mCacheStart].getContentUri();
+ }
+
+ @Override
+ public synchronized Bitmap getImage(int index) {
+ ensureCacheRange(index);
+ if (index < mCacheStart || index >= mCacheEnd) return null;
+ return WidgetUtils.createWidgetBitmap(mCache[index - mCacheStart]);
+ }
+
+ @Override
+ public void reload() {
+ long version = mSource.reload();
+ if (mSourceVersion != version) {
+ mSourceVersion = version;
+ mCacheStart = 0;
+ mCacheEnd = 0;
+ Arrays.fill(mCache, null);
+ }
+ }
+
+ @Override
+ public void setContentListener(ContentListener listener) {
+ mContentListener = listener;
+ }
+
+ @Override
+ public int size() {
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mSource.getMediaItemCount();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onContentDirty() {
+ if (mContentListener != null) mContentListener.onContentDirty();
+ }
+}
diff --git a/src/com/android/gallery3d/widget/WidgetClickHandler.java b/src/com/android/gallery3d/widget/WidgetClickHandler.java
new file mode 100644
index 000000000..362e4d20c
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetClickHandler.java
@@ -0,0 +1,59 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+public class WidgetClickHandler extends Activity {
+ private static final String TAG = "PhotoAppWidgetClickHandler";
+
+ private boolean isValidDataUri(Uri dataUri) {
+ if (dataUri == null) return false;
+ try {
+ AssetFileDescriptor f = getContentResolver()
+ .openAssetFileDescriptor(dataUri, "r");
+ f.close();
+ return true;
+ } catch (Throwable e) {
+ Log.w(TAG, "cannot open uri: " + dataUri, e);
+ return false;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ Intent intent = getIntent();
+ if (isValidDataUri(intent.getData())) {
+ startActivity(new Intent(Intent.ACTION_VIEW, intent.getData()));
+ } else {
+ Toast.makeText(this,
+ R.string.no_such_item, Toast.LENGTH_LONG).show();
+ startActivity(new Intent(this, Gallery.class));
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/widget/WidgetConfigure.java b/src/com/android/gallery3d/widget/WidgetConfigure.java
new file mode 100644
index 000000000..3bcd9c46e
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetConfigure.java
@@ -0,0 +1,167 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPicker;
+import com.android.gallery3d.app.CropImage;
+import com.android.gallery3d.app.DialogPicker;
+
+import android.app.Activity;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+public class WidgetConfigure extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "WidgetConfigure";
+
+ public static final String KEY_WIDGET_TYPE = "widget-type";
+
+ private static final int REQUEST_WIDGET_TYPE = 1;
+ private static final int REQUEST_CHOOSE_ALBUM = 2;
+ private static final int REQUEST_CROP_IMAGE = 3;
+ private static final int REQUEST_GET_PHOTO = 4;
+
+ public static final int RESULT_ERROR = RESULT_FIRST_USER;
+
+ // Scale up the widget size since we only specified the minimized
+ // size of the gadget. The real size could be larger.
+ // Note: There is also a limit on the size of data that can be
+ // passed in Binder's transaction.
+ private static float WIDGET_SCALE_FACTOR = 1.5f;
+
+ private int mAppWidgetId = -1;
+ private int mWidgetType = 0;
+ private Uri mPickedItem;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+
+ if (mAppWidgetId == -1) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ if (mWidgetType == 0) {
+ Intent intent = new Intent(this, WidgetTypeChooser.class);
+ startActivityForResult(intent, REQUEST_WIDGET_TYPE);
+ }
+ }
+
+ private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(this);
+ RemoteViews views = WidgetProvider.buildWidget(this, mAppWidgetId, entry);
+ manager.updateAppWidget(mAppWidgetId, views);
+ setResult(RESULT_OK, new Intent().putExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+ finish();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ setResult(resultCode, new Intent().putExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+ finish();
+ return;
+ }
+
+ if (requestCode == REQUEST_WIDGET_TYPE) {
+ setWidgetType(data);
+ } else if (requestCode == REQUEST_CHOOSE_ALBUM) {
+ setChoosenAlbum(data);
+ } else if (requestCode == REQUEST_GET_PHOTO) {
+ setChoosenPhoto(data);
+ } else if (requestCode == REQUEST_CROP_IMAGE) {
+ setPhotoWidget(data);
+ } else {
+ throw new AssertionError("unknown request: " + requestCode);
+ }
+ }
+
+ private void setPhotoWidget(Intent data) {
+ // Store the cropped photo in our database
+ Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+ try {
+ helper.setPhoto(mAppWidgetId, mPickedItem, bitmap);
+ updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+ } finally {
+ helper.close();
+ }
+ }
+
+ private void setChoosenPhoto(Intent data) {
+ Resources res = getResources();
+ int widgetWidth = Math.round(WIDGET_SCALE_FACTOR
+ * res.getDimension(R.dimen.appwidget_width));
+ int widgetHeight = Math.round(WIDGET_SCALE_FACTOR
+ * res.getDimension(R.dimen.appwidget_height));
+ mPickedItem = data.getData();
+ Intent request = new Intent(CropImage.ACTION_CROP, mPickedItem)
+ .putExtra(CropImage.KEY_OUTPUT_X, widgetWidth)
+ .putExtra(CropImage.KEY_OUTPUT_Y, widgetHeight)
+ .putExtra(CropImage.KEY_ASPECT_X, widgetWidth)
+ .putExtra(CropImage.KEY_ASPECT_Y, widgetHeight)
+ .putExtra(CropImage.KEY_SCALE_UP_IF_NEEDED, true)
+ .putExtra(CropImage.KEY_SCALE, true)
+ .putExtra(CropImage.KEY_RETURN_DATA, true);
+ startActivityForResult(request, REQUEST_CROP_IMAGE);
+ }
+
+ private void setChoosenAlbum(Intent data) {
+ String albumPath = data.getStringExtra(AlbumPicker.KEY_ALBUM_PATH);
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+ try {
+ helper.setWidget(mAppWidgetId,
+ WidgetDatabaseHelper.TYPE_ALBUM, albumPath);
+ updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+ } finally {
+ helper.close();
+ }
+ }
+
+ private void setWidgetType(Intent data) {
+ mWidgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle);
+ if (mWidgetType == R.id.widget_type_album) {
+ Intent intent = new Intent(this, AlbumPicker.class);
+ startActivityForResult(intent, REQUEST_CHOOSE_ALBUM);
+ } else if (mWidgetType == R.id.widget_type_shuffle) {
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+ try {
+ helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null);
+ updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+ } finally {
+ helper.close();
+ }
+ } else {
+ // Explicitly send the intent to the DialogPhotoPicker
+ Intent request = new Intent(this, DialogPicker.class)
+ .setAction(Intent.ACTION_GET_CONTENT)
+ .setType("image/*");
+ startActivityForResult(request, REQUEST_GET_PHOTO);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java
new file mode 100644
index 000000000..d5bf22e18
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.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.widget;
+
+import com.android.gallery3d.common.Utils;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+
+public class WidgetDatabaseHelper extends SQLiteOpenHelper {
+ private static final String TAG = "PhotoDatabaseHelper";
+ private static final String DATABASE_NAME = "launcher.db";
+
+ private static final int DATABASE_VERSION = 4;
+
+ private static final String TABLE_WIDGETS = "widgets";
+
+ private static final String FIELD_APPWIDGET_ID = "appWidgetId";
+ private static final String FIELD_IMAGE_URI = "imageUri";
+ private static final String FIELD_PHOTO_BLOB = "photoBlob";
+ private static final String FIELD_WIDGET_TYPE = "widgetType";
+ private static final String FIELD_ALBUM_PATH = "albumPath";
+
+ public static final int TYPE_SINGLE_PHOTO = 0;
+ public static final int TYPE_SHUFFLE = 1;
+ public static final int TYPE_ALBUM = 2;
+
+ private static final String[] PROJECTION = {
+ FIELD_WIDGET_TYPE, FIELD_IMAGE_URI, FIELD_PHOTO_BLOB, FIELD_ALBUM_PATH};
+ private static final int INDEX_WIDGET_TYPE = 0;
+ private static final int INDEX_IMAGE_URI = 1;
+ private static final int INDEX_PHOTO_BLOB = 2;
+ private static final int INDEX_ALBUM_PATH = 3;
+ private static final String WHERE_CLAUSE = FIELD_APPWIDGET_ID + " = ?";
+
+ public static class Entry {
+ public int widgetId;
+ public int type;
+ public Uri imageUri;
+ public Bitmap image;
+ public String albumPath;
+
+ private Entry(int id, Cursor cursor) {
+ widgetId = id;
+ type = cursor.getInt(INDEX_WIDGET_TYPE);
+
+ if (type == TYPE_SINGLE_PHOTO) {
+ imageUri = Uri.parse(cursor.getString(INDEX_IMAGE_URI));
+ image = loadBitmap(cursor, INDEX_PHOTO_BLOB);
+ } else if (type == TYPE_ALBUM) {
+ albumPath = cursor.getString(INDEX_ALBUM_PATH);
+ }
+ }
+ }
+
+ public WidgetDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_WIDGETS + " ("
+ + FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY, "
+ + FIELD_WIDGET_TYPE + " INTEGER DEFAULT 0, "
+ + FIELD_IMAGE_URI + " TEXT, "
+ + FIELD_ALBUM_PATH + " TEXT, "
+ + FIELD_PHOTO_BLOB + " BLOB)");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ int version = oldVersion;
+
+ if (version != DATABASE_VERSION) {
+ Log.w(TAG, "destroying all old data.");
+ // Table "photos" is renamed to "widget" in version 4
+ db.execSQL("DROP TABLE IF EXISTS photos");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS);
+ onCreate(db);
+ }
+ }
+
+ /**
+ * Store the given bitmap in this database for the given appWidgetId.
+ */
+ public boolean setPhoto(int appWidgetId, Uri imageUri, Bitmap bitmap) {
+ try {
+ // Try go guesstimate how much space the icon will take when
+ // serialized to avoid unnecessary allocations/copies during
+ // the write.
+ int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+ ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.close();
+
+ ContentValues values = new ContentValues();
+ values.put(FIELD_APPWIDGET_ID, appWidgetId);
+ values.put(FIELD_WIDGET_TYPE, TYPE_SINGLE_PHOTO);
+ values.put(FIELD_IMAGE_URI, imageUri.toString());
+ values.put(FIELD_PHOTO_BLOB, out.toByteArray());
+
+ SQLiteDatabase db = getWritableDatabase();
+ db.replaceOrThrow(TABLE_WIDGETS, null, values);
+ return true;
+ } catch (Throwable e) {
+ Log.e(TAG, "set widget photo fail", e);
+ return false;
+ }
+ }
+
+ public boolean setWidget(int id, int type, String albumPath) {
+ try {
+ ContentValues values = new ContentValues();
+ values.put(FIELD_APPWIDGET_ID, id);
+ values.put(FIELD_WIDGET_TYPE, type);
+ values.put(FIELD_ALBUM_PATH, Utils.ensureNotNull(albumPath));
+ getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values);
+ return true;
+ } catch (Throwable e) {
+ Log.e(TAG, "set widget fail", e);
+ return false;
+ }
+ }
+
+ private static Bitmap loadBitmap(Cursor cursor, int columnIndex) {
+ byte[] data = cursor.getBlob(columnIndex);
+ if (data == null) return null;
+ return BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+
+ public Entry getEntry(int appWidgetId) {
+ Cursor cursor = null;
+ try {
+ SQLiteDatabase db = getReadableDatabase();
+ cursor = db.query(TABLE_WIDGETS, PROJECTION,
+ WHERE_CLAUSE, new String[] {String.valueOf(appWidgetId)},
+ null, null, null);
+ if (cursor == null || !cursor.moveToNext()) {
+ Log.e(TAG, "query fail: empty cursor: " + cursor);
+ return null;
+ }
+ return new Entry(appWidgetId, cursor);
+ } catch (Throwable e) {
+ Log.e(TAG, "Could not load photo from database", e);
+ return null;
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+
+ /**
+ * Remove any bitmap associated with the given appWidgetId.
+ */
+ public void deleteEntry(int appWidgetId) {
+ try {
+ SQLiteDatabase db = getWritableDatabase();
+ db.delete(TABLE_WIDGETS, WHERE_CLAUSE,
+ new String[] {String.valueOf(appWidgetId)});
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not delete photo from database", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/widget/WidgetProvider.java b/src/com/android/gallery3d/widget/WidgetProvider.java
new file mode 100644
index 000000000..0a2fbfbe0
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetProvider.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.widget;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.widget.WidgetDatabaseHelper.Entry;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+public class WidgetProvider extends AppWidgetProvider {
+
+ private static final String TAG = "WidgetProvider";
+
+ static RemoteViews buildWidget(Context context, int id, Entry entry) {
+
+ switch (entry.type) {
+ case WidgetDatabaseHelper.TYPE_ALBUM:
+ case WidgetDatabaseHelper.TYPE_SHUFFLE:
+ return buildStackWidget(context, id, entry);
+ case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO:
+ return buildFrameWidget(context, id, entry);
+ }
+ throw new RuntimeException("invalid type - " + entry.type);
+ }
+
+ @Override
+ public void onUpdate(Context context,
+ AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
+ try {
+ for (int id : appWidgetIds) {
+ Entry entry = helper.getEntry(id);
+ if (entry != null) {
+ RemoteViews views = buildWidget(context, id, entry);
+ appWidgetManager.updateAppWidget(id, views);
+ } else {
+ Log.e(TAG, "cannot load widget: " + id);
+ }
+ }
+ } finally {
+ helper.close();
+ }
+ super.onUpdate(context, appWidgetManager, appWidgetIds);
+ }
+
+ private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) {
+ RemoteViews views = new RemoteViews(
+ context.getPackageName(), R.layout.appwidget_main);
+
+ Intent intent = new Intent(context, WidgetService.class);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
+ intent.putExtra(WidgetService.EXTRA_WIDGET_TYPE, entry.type);
+ intent.putExtra(WidgetService.EXTRA_ALBUM_PATH, entry.albumPath);
+ intent.setData(Uri.parse("widget://gallery/" + widgetId));
+
+ views.setRemoteAdapter(R.id.appwidget_stack_view, intent);
+ views.setEmptyView(R.id.appwidget_stack_view, R.id.appwidget_empty_view);
+
+ Intent clickIntent = new Intent(context, WidgetClickHandler.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setPendingIntentTemplate(R.id.appwidget_stack_view, pendingIntent);
+
+ return views;
+ }
+
+ static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) {
+ RemoteViews views = new RemoteViews(
+ context.getPackageName(), R.layout.photo_frame);
+ views.setImageViewBitmap(R.id.photo, entry.image);
+ Intent clickIntent = new Intent(context,
+ WidgetClickHandler.class).setData(entry.imageUri);
+ PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0,
+ clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ views.setOnClickPendingIntent(R.id.photo, pendingClickIntent);
+ return views;
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+ // Clean deleted photos out of our database
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
+ for (int appWidgetId : appWidgetIds) {
+ helper.deleteEntry(appWidgetId);
+ }
+ helper.close();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/widget/WidgetService.java b/src/com/android/gallery3d/widget/WidgetService.java
new file mode 100644
index 000000000..aa167c768
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetService.java
@@ -0,0 +1,169 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+public class WidgetService extends RemoteViewsService {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "GalleryAppWidgetService";
+
+ public static final String EXTRA_WIDGET_TYPE = "widget-type";
+ public static final String EXTRA_ALBUM_PATH = "album-path";
+
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ int id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ int type = intent.getIntExtra(EXTRA_WIDGET_TYPE, 0);
+ String albumPath = intent.getStringExtra(EXTRA_ALBUM_PATH);
+
+ return new PhotoRVFactory((GalleryApp) getApplicationContext(),
+ id, type, albumPath);
+ }
+
+ private static class EmptySource implements WidgetSource {
+
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ @Override
+ public Bitmap getImage(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri getContentUri(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentListener(ContentListener listener) {}
+
+ @Override
+ public void reload() {}
+
+ @Override
+ public void close() {}
+ }
+
+ private static class PhotoRVFactory implements
+ RemoteViewsService.RemoteViewsFactory, ContentListener {
+
+ private final int mAppWidgetId;
+ private final int mType;
+ private final String mAlbumPath;
+ private final GalleryApp mApp;
+
+ private WidgetSource mSource;
+
+ public PhotoRVFactory(GalleryApp app, int id, int type, String albumPath) {
+ mApp = app;
+ mAppWidgetId = id;
+ mType = type;
+ mAlbumPath = albumPath;
+ }
+
+ @Override
+ public void onCreate() {
+ if (mType == WidgetDatabaseHelper.TYPE_ALBUM) {
+ Path path = Path.fromString(mAlbumPath);
+ DataManager manager = mApp.getDataManager();
+ MediaSet mediaSet = (MediaSet) manager.getMediaObject(path);
+ mSource = mediaSet == null
+ ? new EmptySource()
+ : new MediaSetSource(mediaSet);
+ } else {
+ mSource = new LocalPhotoSource(mApp.getAndroidContext());
+ }
+ mSource.setContentListener(this);
+ AppWidgetManager.getInstance(mApp.getAndroidContext())
+ .notifyAppWidgetViewDataChanged(
+ mAppWidgetId, R.id.appwidget_stack_view);
+ }
+
+ @Override
+ public void onDestroy() {
+ mSource.close();
+ mSource = null;
+ }
+
+ public int getCount() {
+ return mSource.size();
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public RemoteViews getLoadingView() {
+ RemoteViews rv = new RemoteViews(
+ mApp.getAndroidContext().getPackageName(),
+ R.layout.appwidget_loading_item);
+ rv.setProgressBar(R.id.appwidget_loading_item, 0, 0, true);
+ return rv;
+ }
+
+ public RemoteViews getViewAt(int position) {
+ Bitmap bitmap = mSource.getImage(position);
+ if (bitmap == null) return getLoadingView();
+ RemoteViews views = new RemoteViews(
+ mApp.getAndroidContext().getPackageName(),
+ R.layout.appwidget_photo_item);
+ views.setImageViewBitmap(R.id.appwidget_photo_item, bitmap);
+ views.setOnClickFillInIntent(R.id.appwidget_photo_item, new Intent()
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ .setData(mSource.getContentUri(position)));
+ return views;
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ mSource.reload();
+ }
+
+ @Override
+ public void onContentDirty() {
+ AppWidgetManager.getInstance(mApp.getAndroidContext())
+ .notifyAppWidgetViewDataChanged(
+ mAppWidgetId, R.id.appwidget_stack_view);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/widget/WidgetSource.java b/src/com/android/gallery3d/widget/WidgetSource.java
new file mode 100644
index 000000000..3c73e882f
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetSource.java
@@ -0,0 +1,31 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.data.ContentListener;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+public interface WidgetSource {
+ public int size();
+ public Bitmap getImage(int index);
+ public Uri getContentUri(int index);
+ public void setContentListener(ContentListener listener);
+ public void reload();
+ public void close();
+}
diff --git a/src/com/android/gallery3d/widget/WidgetTypeChooser.java b/src/com/android/gallery3d/widget/WidgetTypeChooser.java
new file mode 100644
index 000000000..9718e0cb2
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetTypeChooser.java
@@ -0,0 +1,59 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RadioGroup;
+import android.widget.RadioGroup.OnCheckedChangeListener;
+
+public class WidgetTypeChooser extends Activity {
+
+ private OnCheckedChangeListener mListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ Intent data = new Intent()
+ .putExtra(WidgetConfigure.KEY_WIDGET_TYPE, checkedId);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.widget_type);
+ setContentView(R.layout.choose_widget_type);
+ RadioGroup rg = (RadioGroup) findViewById(R.id.widget_type);
+ rg.setOnCheckedChangeListener(mListener);
+
+ Button cancel = (Button) findViewById(R.id.cancel);
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ }
+}
diff --git a/src/com/android/gallery3d/widget/WidgetUtils.java b/src/com/android/gallery3d/widget/WidgetUtils.java
new file mode 100644
index 000000000..481bbddbc
--- /dev/null
+++ b/src/com/android/gallery3d/widget/WidgetUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.widget;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Bitmap.Config;
+import android.util.Log;
+
+public class WidgetUtils {
+
+ private static final String TAG = "WidgetUtils";
+
+ private static int sStackPhotoWidth = 220;
+ private static int sStackPhotoHeight = 170;
+
+ private WidgetUtils() {
+ }
+
+ public static void initialize(Context context) {
+ Resources r = context.getResources();
+ sStackPhotoWidth = r.getDimensionPixelSize(R.dimen.stack_photo_width);
+ sStackPhotoHeight = r.getDimensionPixelSize(R.dimen.stack_photo_height);
+ }
+
+ public static Bitmap createWidgetBitmap(MediaItem image) {
+ Bitmap bitmap = image.requestImage(MediaItem.TYPE_THUMBNAIL)
+ .run(ThreadPool.JOB_CONTEXT_STUB);
+ if (bitmap == null) {
+ Log.w(TAG, "fail to get image of " + image.toString());
+ return null;
+ }
+ return createWidgetBitmap(bitmap, image.getRotation());
+ }
+
+ public static Bitmap createWidgetBitmap(Bitmap bitmap, int rotation) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ float scale;
+ if (((rotation / 90) & 1) == 0) {
+ scale = Math.max((float) sStackPhotoWidth / w,
+ (float) sStackPhotoHeight / h);
+ } else {
+ scale = Math.max((float) sStackPhotoWidth / h,
+ (float) sStackPhotoHeight / w);
+ }
+
+ Bitmap target = Bitmap.createBitmap(
+ sStackPhotoWidth, sStackPhotoHeight, Config.ARGB_8888);
+ Canvas canvas = new Canvas(target);
+ canvas.translate(sStackPhotoWidth / 2, sStackPhotoHeight / 2);
+ canvas.rotate(rotation);
+ canvas.scale(scale, scale);
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ canvas.drawBitmap(bitmap, -w / 2, -h / 2, paint);
+ return target;
+ }
+}