From 232aa1369beb0a26a35b7792b8a927e09023961f Mon Sep 17 00:00:00 2001 From: Bobby Georgescu Date: Fri, 1 Mar 2013 14:09:51 -0800 Subject: Refactor AutoThumbnailDrawable, fix race conditions Change-Id: I80243ee177387e0d9b309acb5a7c1d364cc73530 --- src/com/android/photos/PhotoSetFragment.java | 6 +- .../photos/drawables/AutoThumbnailDrawable.java | 141 +++++++++++++-------- .../photos/drawables/DataUriThumbnailDrawable.java | 54 ++++++++ .../photos/drawables/MtpThumbnailDrawable.java | 61 +++++++++ 4 files changed, 208 insertions(+), 54 deletions(-) create mode 100644 src/com/android/photos/drawables/DataUriThumbnailDrawable.java create mode 100644 src/com/android/photos/drawables/MtpThumbnailDrawable.java (limited to 'src') diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java index a3406bea4..d7265a27b 100644 --- a/src/com/android/photos/PhotoSetFragment.java +++ b/src/com/android/photos/PhotoSetFragment.java @@ -32,7 +32,7 @@ import android.widget.ImageView.ScaleType; import com.android.gallery3d.R; import com.android.photos.data.PhotoSetLoader; -import com.android.photos.drawables.AutoThumbnailDrawable; +import com.android.photos.drawables.DataUriThumbnailDrawable; import com.android.photos.views.GalleryThumbnailView; import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter; @@ -90,7 +90,7 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks extends Drawable { - private static final String TAG = "AutoMipMapDrawable"; + private static final String TAG = "AutoThumbnailDrawable"; private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor(); + private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance(); private static byte[] sTempStorage = new byte[64 * 1024]; // UI thread only private Paint mPaint = new Paint(); private Matrix mDrawMatrix = new Matrix(); - private int mSampleSize = 1; // Decoder thread only private BitmapFactory.Options mOptions = new BitmapFactory.Options(); @@ -52,10 +51,16 @@ public class AutoThumbnailDrawable extends Drawable { // Shared, guarded by mLock private Object mLock = new Object(); private Bitmap mBitmap; - private String mDataUri; + protected T mData; private boolean mIsQueued; private int mImageWidth, mImageHeight; private Rect mBounds = new Rect(); + private int mSampleSize = 1; + // mSampleSize is the target sample size for the full-size dimensions + // of the image (so if a preferred, smaller image is used, it might + // not reflect the sample size used when decoding that image). This + // value is used in refreshSampleSizeLocked to determine whether the + // image needs to be refreshed. public AutoThumbnailDrawable() { mPaint.setAntiAlias(true); @@ -64,18 +69,35 @@ public class AutoThumbnailDrawable extends Drawable { mOptions.inTempStorage = sTempStorage; } - public void setImage(String dataUri, int width, int height) { - if (TextUtils.equals(mDataUri, dataUri)) return; + protected abstract byte[] getPreferredImageBytes(T data); + protected abstract InputStream getFallbackImageStream(T data); + + // Must hold mLock when calling on different thread from setImage + protected abstract boolean dataChangedLocked(T data); + + // Must only be called from one thread (usually the UI thread) + public void setImage(T data, int width, int height) { + if (!dataChangedLocked(data)) return; synchronized (mLock) { mImageWidth = width; mImageHeight = height; - mDataUri = dataUri; - mBitmap = null; + mData = data; + setBitmapLocked(null); refreshSampleSizeLocked(); } invalidateSelf(); } + private void setBitmapLocked(Bitmap b) { + if (b == mBitmap) { + return; + } + if (mBitmap != null) { + sBitmapPool.put(mBitmap); + } + mBitmap = b; + } + @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); @@ -148,7 +170,8 @@ public class AutoThumbnailDrawable extends Drawable { } else { scale = (float) dwidth / (float) vwidth; } - return (int) (scale + .5f); + int result = Math.round(scale); + return result > 0 ? result : 1; } private void refreshSampleSizeLocked() { @@ -208,62 +231,78 @@ public class AutoThumbnailDrawable extends Drawable { private final Runnable mLoadBitmap = new Runnable() { @Override public void run() { - // TODO: Use bitmap pool - String data; - int sampleSize; + T data; + synchronized (mLock) { + data = mData; + } + int preferredSampleSize = 1; + byte[] preferred = getPreferredImageBytes(data); + boolean hasPreferred = (preferred != null && preferred.length > 0); + if (hasPreferred) { + mOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); + mOptions.inJustDecodeBounds = false; + } + int sampleSize, width, height; synchronized (mLock) { - data = mDataUri; - sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); - mSampleSize = sampleSize; + if (dataChangedLocked(data)) { + return; + } + width = mImageWidth; + height = mImageHeight; + if (hasPreferred) { + preferredSampleSize = calculateSampleSizeLocked( + mOptions.outWidth, mOptions.outHeight); + } + sampleSize = calculateSampleSizeLocked(width, height); mIsQueued = false; } - FileInputStream fis = null; + Bitmap b = null; + InputStream is = null; try { - ExifInterface exif = new ExifInterface(data); - if (exif.hasThumbnail()) { - byte[] thumbnail = exif.getThumbnail(); - mOptions.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(thumbnail, 0, - thumbnail.length, mOptions); - int exifThumbSampleSize = calculateSampleSizeLocked( - mOptions.outWidth, mOptions.outHeight); - mOptions.inJustDecodeBounds = false; - mOptions.inSampleSize = exifThumbSampleSize; - mBitmap = BitmapFactory.decodeByteArray(thumbnail, 0, - thumbnail.length, mOptions); - if (mBitmap != null) { - synchronized (mLock) { - if (TextUtils.equals(data, mDataUri)) { - scheduleSelf(mUpdateBitmap, 0); - } - } - return; + if (hasPreferred) { + mOptions.inSampleSize = preferredSampleSize; + mOptions.inBitmap = sBitmapPool.get( + mOptions.outWidth / preferredSampleSize, + mOptions.outHeight / preferredSampleSize); + b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); + if (mOptions.inBitmap != null && b != mOptions.inBitmap) { + sBitmapPool.put(mOptions.inBitmap); + mOptions.inBitmap = null; + } + } + if (b == null) { + is = getFallbackImageStream(data); + mOptions.inSampleSize = sampleSize; + mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize); + b = BitmapFactory.decodeStream(is, null, mOptions); + if (mOptions.inBitmap != null && b != mOptions.inBitmap) { + sBitmapPool.put(mOptions.inBitmap); + mOptions.inBitmap = null; } } - fis = new FileInputStream(data); - FileDescriptor fd = fis.getFD(); - mOptions.inSampleSize = sampleSize; - mBitmap = BitmapFactory.decodeFileDescriptor(fd, null, mOptions); } catch (Exception e) { - Log.d("AsyncBitmap", "Failed to fetch bitmap", e); + Log.d(TAG, "Failed to fetch bitmap", e); return; } finally { try { - if (fis != null) { - fis.close(); + if (is != null) { + is.close(); } } catch (Exception e) {} - } - synchronized (mLock) { - if (TextUtils.equals(data, mDataUri)) { - scheduleSelf(mUpdateBitmap, 0); + if (b != null) { + synchronized (mLock) { + if (!dataChangedLocked(data)) { + setBitmapLocked(b); + scheduleSelf(mUpdateBitmap, 0); + } + } } } } }; private final Runnable mUpdateBitmap = new Runnable() { - @Override public void run() { synchronized (AutoThumbnailDrawable.this) { diff --git a/src/com/android/photos/drawables/DataUriThumbnailDrawable.java b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java new file mode 100644 index 000000000..c83b0c8fa --- /dev/null +++ b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photos.drawables; + +import android.media.ExifInterface; +import android.text.TextUtils; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class DataUriThumbnailDrawable extends AutoThumbnailDrawable { + + @Override + protected byte[] getPreferredImageBytes(String data) { + byte[] thumbnail = null; + try { + ExifInterface exif = new ExifInterface(data); + if (exif.hasThumbnail()) { + thumbnail = exif.getThumbnail(); + } + } catch (IOException e) { } + return thumbnail; + } + + @Override + protected InputStream getFallbackImageStream(String data) { + try { + return new FileInputStream(data); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected boolean dataChangedLocked(String data) { + return !TextUtils.equals(mData, data); + } +} diff --git a/src/com/android/photos/drawables/MtpThumbnailDrawable.java b/src/com/android/photos/drawables/MtpThumbnailDrawable.java new file mode 100644 index 000000000..e35e06943 --- /dev/null +++ b/src/com/android/photos/drawables/MtpThumbnailDrawable.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photos.drawables; + +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; + +import com.android.gallery3d.ingest.MtpDeviceIndex; + +import java.io.InputStream; + +public class MtpThumbnailDrawable extends AutoThumbnailDrawable { + public void setImage(MtpObjectInfo data) { + if (data == null) { + setImage(null, 0, 0); + } else { + setImage(data, data.getImagePixWidth(), data.getImagePixHeight()); + } + } + + @Override + protected byte[] getPreferredImageBytes(MtpObjectInfo data) { + if (data == null) { + return null; + } + MtpDevice device = MtpDeviceIndex.getInstance().getDevice(); + if (device != null) { + return device.getThumbnail(data.getObjectHandle()); + } else { + return null; + } + } + + @Override + protected InputStream getFallbackImageStream(MtpObjectInfo data) { + // No fallback + return null; + } + + @Override + protected boolean dataChangedLocked(MtpObjectInfo data) { + // We only fetch the MtpObjectInfo once when creating + // the index so checking the reference is enough + return mData == data; + } + +} -- cgit v1.2.3