summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOwen Lin <owenlin@google.com>2011-08-30 10:38:59 +0800
committerOwen Lin <owenlin@google.com>2011-09-05 11:28:18 +0800
commitace280a70fd1ead67039b9667d3905c85703c094 (patch)
treeadd5ae4895e61f13a9b55c91b9890bb6ecc9a58e
parent9c12d7512df688131384328fb6ffb89b36cc4393 (diff)
downloadandroid_packages_apps_Snap-ace280a70fd1ead67039b9667d3905c85703c094.tar.gz
android_packages_apps_Snap-ace280a70fd1ead67039b9667d3905c85703c094.tar.bz2
android_packages_apps_Snap-ace280a70fd1ead67039b9667d3905c85703c094.zip
Improve the performance of Reviewing a photo.
fix: 5144370 There is two componenet in the photo page. One is the large photo and the other is the thumbnail strip. They idenpendently load their own data and images. This change fixes several issues here: 1. Prevent sending to many jobs to ThreadPool and block others. In a worse case, if the thumbnail strip send image requests first, it may block the ThreadPool very long. 2. Improve the performance of extracting thumbnails from local files. Now we try to extract the thumbnails from EXIF data first. Change-Id: I45100d4daa025efb479f47c4f105de2b4731b498
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java3
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java8
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java39
-rw-r--r--src/com/android/gallery3d/data/DecodeUtils.java25
-rw-r--r--src/com/android/gallery3d/data/LocalImage.java22
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlidingWindow.java7
-rw-r--r--src/com/android/gallery3d/ui/FilmStripView.java59
-rw-r--r--src/com/android/gallery3d/ui/GLView.java7
-rw-r--r--src/com/android/gallery3d/util/JobLimiter.java163
9 files changed, 264 insertions, 69 deletions
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
index 29f86e056..ea98c6c19 100644
--- a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -298,8 +298,5 @@ public class BitmapUtils {
ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
bitmap.compress(CompressFormat.JPEG, quality, baos);
return baos.toByteArray();
-
}
-
-
}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index c05c89a0d..78afe7ee7 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -142,6 +142,7 @@ public class PhotoDataAdapter implements PhotoPage.Model {
private boolean mIsActive;
public interface DataListener extends LoadingListener {
+ public void onPhotoAvailable(long version, boolean fullImage);
public void onPhotoChanged(int index, Path item);
}
@@ -224,6 +225,9 @@ public class PhotoDataAdapter implements PhotoPage.Model {
if (entry.screenNail == null) {
entry.failToLoad = true;
} else {
+ if (mDataListener != null) {
+ mDataListener.onPhotoAvailable(version, false);
+ }
for (int i = -1; i <=1; ++i) {
if (version == getVersion(mCurrentIndex + i)) {
if (i == 0) updateTileProvider(entry);
@@ -244,6 +248,9 @@ public class PhotoDataAdapter implements PhotoPage.Model {
entry.fullImageTask = null;
entry.fullImage = future.get();
if (entry.fullImage != null) {
+ if (mDataListener != null) {
+ mDataListener.onPhotoAvailable(version, true);
+ }
if (version == getVersion(mCurrentIndex)) {
updateTileProvider(entry);
mPhotoView.notifyImageInvalidated(0);
@@ -644,6 +651,7 @@ public class PhotoDataAdapter implements PhotoPage.Model {
@Override
public UpdateInfo call() throws Exception {
+ // TODO: Try to load some data in first update
UpdateInfo info = new UpdateInfo();
info.version = mSourceVersion;
info.reloadContent = needContentReload();
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 44f299ef5..3a0bcd23f 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -94,7 +94,7 @@ public class PhotoPage extends ActivityState
private Intent mResultIntent = new Intent();
private int mCurrentIndex = 0;
private Handler mHandler;
- private boolean mShowBars;
+ private boolean mShowBars = true;
private ActionBar mActionBar;
private MyMenuVisibilityListener mMenuVisibilityListener;
private boolean mIsMenuVisible;
@@ -148,6 +148,22 @@ public class PhotoPage extends ActivityState
}
};
+ private void initFilmStripView() {
+ 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);
+ mRootPane.requestLayout();
+ if (mIsActive) mFilmStripView.resume();
+ if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE);
+ }
+
@Override
public void onCreate(Bundle data, Bundle restoreState) {
mActionBar = ((Activity) mActivity).getActionBar();
@@ -175,25 +191,14 @@ public class PhotoPage extends ActivityState
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() {
+ @Override
public void onPhotoChanged(int index, Path item) {
- mFilmStripView.setFocusIndex(index);
+ if (mFilmStripView != null) mFilmStripView.setFocusIndex(index);
mCurrentIndex = index;
mResultIntent.putExtra(KEY_INDEX_HINT, index);
if (item != null) {
@@ -217,11 +222,15 @@ public class PhotoPage extends ActivityState
}
}
-
@Override
public void onLoadingStarted() {
GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
}
+
+ @Override
+ public void onPhotoAvailable(long version, boolean fullImage) {
+ if (mFilmStripView == null) initFilmStripView();
+ }
});
} else {
// Get default media set by the URI
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
index da2d3e0ee..d2b4ebc4e 100644
--- a/src/com/android/gallery3d/data/DecodeUtils.java
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -106,6 +106,31 @@ public class DecodeUtils {
BitmapFactory.decodeFileDescriptor(fd, null, options));
}
+ /**
+ * Decodes the bitmap from the given byte array if the image size is larger than the given
+ * requirement.
+ *
+ * Note: The returned image may be resized down. However, both width and heigh must be
+ * larger than the <code>targetSize</code>.
+ */
+ public static Bitmap requestDecodeIfBigEnough(JobContext jc, byte[] data,
+ Options options, int targetSize) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ if (jc.isCancelled()) return null;
+ if (options.outWidth < targetSize || options.outHeight < targetSize) {
+ return null;
+ }
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ options.outWidth, options.outHeight, targetSize);
+ options.inJustDecodeBounds = false;
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(data, 0, data.length, options));
+ }
+
public static Bitmap requestDecode(JobContext jc,
FileDescriptor fileDescriptor, Rect paddings, Options options) {
if (options == null) options = new Options();
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index f3dedf037..7ab04c568 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -18,10 +18,10 @@ 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 com.android.gallery3d.util.UpdateHelper;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -33,6 +33,7 @@ import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
import java.io.File;
import java.io.IOException;
@@ -158,6 +159,25 @@ public class LocalImage extends LocalMediaItem {
public Bitmap onDecodeOriginal(JobContext jc, int type) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ // try to decode from JPEG EXIF
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ ExifInterface exif = null;
+ byte [] thumbData = null;
+ try {
+ exif = new ExifInterface(mLocalFilePath);
+ if (exif != null) {
+ thumbData = exif.getThumbnail();
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to get exif thumb", t);
+ }
+ if (thumbData != null) {
+ Bitmap bitmap = DecodeUtils.requestDecodeIfBigEnough(
+ jc, thumbData, options, getTargetSize(type));
+ if (bitmap != null) return bitmap;
+ }
+ }
return DecodeUtils.requestDecode(
jc, mLocalFilePath, options, getTargetSize(type));
}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
index 9e44bd1d2..9b410e9a8 100644
--- a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -24,7 +24,7 @@ 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.JobLimiter;
import com.android.gallery3d.util.ThreadPool.Job;
import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -39,6 +39,7 @@ public class AlbumSlidingWindow implements AlbumView.ModelListener {
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;
+ private static final int JOB_LIMIT = 2;
public static interface Listener {
public void onSizeChanged(int size);
@@ -64,7 +65,7 @@ public class AlbumSlidingWindow implements AlbumView.ModelListener {
private SelectionDrawer mSelectionDrawer;
private SynchronizedHandler mHandler;
- private ThreadPool mThreadPool;
+ private JobLimiter mThreadPool;
private int mSlotWidth, mSlotHeight;
private int mActiveRequestCount = 0;
@@ -103,7 +104,7 @@ public class AlbumSlidingWindow implements AlbumView.ModelListener {
}
};
- mThreadPool = activity.getThreadPool();
+ mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
}
public void setSelectionDrawer(SelectionDrawer drawer) {
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
index 8d28f2c7b..c53e1aedf 100644
--- a/src/com/android/gallery3d/ui/FilmStripView.java
+++ b/src/com/android/gallery3d/ui/FilmStripView.java
@@ -18,7 +18,6 @@ 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;
@@ -46,8 +45,6 @@ public class FilmStripView extends GLView implements SlotView.Listener,
private StripDrawer mStripDrawer;
private Listener mListener;
private UserInteractionListener mUIListener;
- private boolean mFilmStripVisible;
- private CanvasAnimation mFilmStripAnimation;
private NinePatchTexture mBackgroundTexture;
// The layout of FileStripView is
@@ -90,7 +87,6 @@ public class FilmStripView extends GLView implements SlotView.Listener,
mAlbumView.setModel(mAlbumDataAdapter);
mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(),
R.drawable.navstrip_translucent);
- mFilmStripVisible = true;
}
public void setListener(Listener listener) {
@@ -101,25 +97,18 @@ public class FilmStripView extends GLView implements SlotView.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);
+ if (getVisibility() == GLView.VISIBLE) return;
+ startAnimation(null);
+ setVisibility(GLView.VISIBLE);
}
public void hide() {
- setFilmStripVisible(false);
+ if (getVisibility() == GLView.INVISIBLE) return;
+ AlphaAnimation animation = new AlphaAnimation(1, 0);
+ animation.setDuration(HIDE_ANIMATION_DURATION);
+ startAnimation(animation);
+ setVisibility(GLView.INVISIBLE);
}
@Override
@@ -159,10 +148,6 @@ public class FilmStripView extends GLView implements SlotView.Listener,
@Override
protected boolean dispatchTouchEvent(MotionEvent event) {
- if (!mFilmStripVisible && mFilmStripAnimation == null) {
- return false;
- }
-
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
@@ -179,63 +164,49 @@ public class FilmStripView extends GLView implements SlotView.Listener,
@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
+ @Override
public void onSingleTapUp(int slotIndex) {
mAlbumView.setFocusIndex(slotIndex);
mListener.onSlotSelected(slotIndex);
}
// Called by AlbumView
+ @Override
public void onLongTap(int slotIndex) {
onSingleTapUp(slotIndex);
}
// Called by AlbumView
+ @Override
public void onUserInteractionBegin() {
mUIListener.onUserInteractionBegin();
}
// Called by AlbumView
+ @Override
public void onUserInteractionEnd() {
mUIListener.onUserInteractionEnd();
}
// Called by AlbumView
+ @Override
public void onUserInteraction() {
mUIListener.onUserInteraction();
}
// Called by AlbumView
+ @Override
public void onScrollPositionChanged(int position, int total) {
mScrollBarView.setContentPosition(position, total);
}
// Called by ScrollBarView
+ @Override
public void onScrollBarPositionChanged(int position) {
mAlbumView.setScrollPosition(position);
}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
index c59327831..7491a6ffb 100644
--- a/src/com/android/gallery3d/ui/GLView.java
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -76,10 +76,11 @@ public class GLView {
public void startAnimation(CanvasAnimation animation) {
GLRoot root = getGLRoot();
if (root == null) throw new IllegalStateException();
-
mAnimation = animation;
- mAnimation.start();
- root.registerLaunchedAnimation(mAnimation);
+ if (mAnimation != null) {
+ mAnimation.start();
+ root.registerLaunchedAnimation(mAnimation);
+ }
invalidate();
}
diff --git a/src/com/android/gallery3d/util/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java
new file mode 100644
index 000000000..5abdfd80b
--- /dev/null
+++ b/src/com/android/gallery3d/util/JobLimiter.java
@@ -0,0 +1,163 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+
+// Limit the number of concurrent jobs that has been submitted into a ThreadPool
+@SuppressWarnings("rawtypes")
+public class JobLimiter implements FutureListener {
+ private static final String TAG = "JobLimiter";
+
+ // State Transition:
+ // INIT -> DONE, CANCELLED
+ // DONE -> CANCELLED
+ private static final int STATE_INIT = 0;
+ private static final int STATE_DONE = 1;
+ private static final int STATE_CANCELLED = 2;
+
+ private final LinkedList<JobWrapper<?>> mJobs = new LinkedList<JobWrapper<?>>();
+ private final ThreadPool mPool;
+ private int mLimit;
+
+ private static class JobWrapper<T> implements Future<T>, Job<T> {
+ private int mState = STATE_INIT;
+ private Job<T> mJob;
+ private Future<T> mDelegate;
+ private FutureListener<T> mListener;
+ private T mResult;
+
+ public JobWrapper(Job<T> job, FutureListener<T> listener) {
+ mJob = job;
+ mListener = listener;
+ }
+
+ public synchronized void setFuture(Future<T> future) {
+ if (mState != STATE_INIT) return;
+ mDelegate = future;
+ }
+
+ @Override
+ public void cancel() {
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState != STATE_DONE) {
+ listener = mListener;
+ mJob = null;
+ mListener = null;
+ if (mDelegate != null) {
+ mDelegate.cancel();
+ mDelegate = null;
+ }
+ }
+ mState = STATE_CANCELLED;
+ mResult = null;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mState == STATE_CANCELLED;
+ }
+
+ @Override
+ public boolean isDone() {
+ // Both CANCELLED AND DONE is considered as done
+ return mState != STATE_INIT;
+ }
+
+ @Override
+ public synchronized T get() {
+ while (mState == STATE_INIT) {
+ // handle the interrupted exception of wait()
+ Utils.waitWithoutInterrupt(this);
+ }
+ return mResult;
+ }
+
+ @Override
+ public void waitDone() {
+ get();
+ }
+
+ @Override
+ public T run(JobContext jc) {
+ Job<T> job = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ job = mJob;
+ }
+ T result = null;
+ try {
+ result = job.run(jc);
+ } catch (Throwable t) {
+ Log.w(TAG, "error executing job: " + job, t);
+ }
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ mState = STATE_DONE;
+ listener = mListener;
+ mListener = null;
+ mJob = null;
+ mResult = result;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ return result;
+ }
+ }
+
+ public JobLimiter(ThreadPool pool, int limit) {
+ mPool = Utils.checkNotNull(pool);
+ mLimit = limit;
+ }
+
+ public synchronized <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+ JobWrapper<T> future = new JobWrapper<T>(Utils.checkNotNull(job), listener);
+ mJobs.addLast(future);
+ submitTasksIfAllowed();
+ return future;
+ }
+
+ public <T> Future<T> submit(Job<T> job) {
+ return submit(job, null);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void submitTasksIfAllowed() {
+ while (mLimit > 0 && !mJobs.isEmpty()) {
+ JobWrapper wrapper = mJobs.removeFirst();
+ if (!wrapper.isCancelled()) {
+ --mLimit;
+ wrapper.setFuture(mPool.submit(wrapper, this));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFutureDone(Future future) {
+ ++mLimit;
+ submitTasksIfAllowed();
+ }
+}