| /* |
| * 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; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.BitmapRegionDecoder; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.opengl.GLUtils; |
| import android.os.Build; |
| import android.util.Log; |
| |
| import com.android.gallery3d.common.ExifOrientation; |
| import com.android.gallery3d.common.Utils; |
| import com.android.gallery3d.glrenderer.BasicTexture; |
| import com.android.gallery3d.glrenderer.BitmapTexture; |
| import com.android.photos.views.TiledImageRenderer; |
| import com.android.wallpaperpicker.common.InputStreamProvider; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| interface SimpleBitmapRegionDecoder { |
| int getWidth(); |
| int getHeight(); |
| Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); |
| } |
| |
| class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder { |
| BitmapRegionDecoder mDecoder; |
| private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) { |
| mDecoder = decoder; |
| } |
| |
| public static SimpleBitmapRegionDecoderWrapper newInstance( |
| InputStream is, boolean isShareable) { |
| try { |
| BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); |
| if (d != null) { |
| return new SimpleBitmapRegionDecoderWrapper(d); |
| } |
| } catch (IOException e) { |
| Log.w("BitmapRegionTileSource", "getting decoder failed", e); |
| return null; |
| } |
| return null; |
| } |
| public int getWidth() { |
| return mDecoder.getWidth(); |
| } |
| public int getHeight() { |
| return mDecoder.getHeight(); |
| } |
| public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { |
| return mDecoder.decodeRegion(wantRegion, options); |
| } |
| } |
| |
| class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { |
| Bitmap mBuffer; |
| Canvas mTempCanvas; |
| Paint mTempPaint; |
| private DumbBitmapRegionDecoder(Bitmap b) { |
| mBuffer = b; |
| } |
| public static DumbBitmapRegionDecoder newInstance(InputStream is) { |
| Bitmap b = BitmapFactory.decodeStream(is); |
| if (b != null) { |
| return new DumbBitmapRegionDecoder(b); |
| } |
| return null; |
| } |
| public int getWidth() { |
| return mBuffer.getWidth(); |
| } |
| public int getHeight() { |
| return mBuffer.getHeight(); |
| } |
| public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { |
| if (mTempCanvas == null) { |
| mTempCanvas = new Canvas(); |
| mTempPaint = new Paint(); |
| mTempPaint.setFilterBitmap(true); |
| } |
| int sampleSize = Math.max(options.inSampleSize, 1); |
| Bitmap newBitmap = Bitmap.createBitmap( |
| wantRegion.width() / sampleSize, |
| wantRegion.height() / sampleSize, |
| Bitmap.Config.ARGB_8888); |
| mTempCanvas.setBitmap(newBitmap); |
| mTempCanvas.save(); |
| mTempCanvas.scale(1f / sampleSize, 1f / sampleSize); |
| mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint); |
| mTempCanvas.restore(); |
| mTempCanvas.setBitmap(null); |
| return newBitmap; |
| } |
| } |
| |
| /** |
| * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using |
| * {@link BitmapRegionDecoder} to wrap a local file |
| */ |
| @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) |
| public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { |
| |
| private static final String TAG = "BitmapRegionTileSource"; |
| |
| private static final int GL_SIZE_LIMIT = 2048; |
| // This must be no larger than half the size of the GL_SIZE_LIMIT |
| // due to decodePreview being allowed to be up to 2x the size of the target |
| private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; |
| |
| public static abstract class BitmapSource { |
| private SimpleBitmapRegionDecoder mDecoder; |
| private Bitmap mPreview; |
| private int mRotation; |
| public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; |
| private State mState = State.NOT_LOADED; |
| |
| /** Returns whether loading was successful. */ |
| public boolean loadInBackground(InBitmapProvider bitmapProvider) { |
| mRotation = getExifRotation(); |
| mDecoder = loadBitmapRegionDecoder(); |
| if (mDecoder == null) { |
| mState = State.ERROR_LOADING; |
| return false; |
| } else { |
| int width = mDecoder.getWidth(); |
| int height = mDecoder.getHeight(); |
| |
| BitmapFactory.Options opts = new BitmapFactory.Options(); |
| opts.inPreferredConfig = Bitmap.Config.ARGB_8888; |
| opts.inPreferQualityOverSpeed = true; |
| |
| float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height); |
| opts.inSampleSize = Utils.computeSampleSizeLarger(scale); |
| opts.inJustDecodeBounds = false; |
| opts.inMutable = true; |
| |
| if (bitmapProvider != null) { |
| int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); |
| Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); |
| if (reusableBitmap != null) { |
| // Try loading with reusable bitmap |
| opts.inBitmap = reusableBitmap; |
| try { |
| mPreview = loadPreviewBitmap(opts); |
| } catch (IllegalArgumentException e) { |
| Log.d(TAG, "Unable to reuse bitmap", e); |
| opts.inBitmap = null; |
| mPreview = null; |
| } |
| } |
| } |
| if (mPreview == null) { |
| mPreview = loadPreviewBitmap(opts); |
| } |
| if (mPreview == null) { |
| mState = State.ERROR_LOADING; |
| return false; |
| } |
| |
| // Verify that the bitmap can be used on GL surface |
| try { |
| GLUtils.getInternalFormat(mPreview); |
| GLUtils.getType(mPreview); |
| mState = State.LOADED; |
| } catch (IllegalArgumentException e) { |
| Log.d(TAG, "Image cannot be rendered on a GL surface", e); |
| mState = State.ERROR_LOADING; |
| } |
| return mState == State.LOADED; |
| } |
| } |
| |
| public State getLoadingState() { |
| return mState; |
| } |
| |
| public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { |
| return mDecoder; |
| } |
| |
| public Bitmap getPreviewBitmap() { |
| return mPreview; |
| } |
| |
| public int getRotation() { |
| return mRotation; |
| } |
| |
| public abstract int getExifRotation(); |
| public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); |
| public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); |
| |
| public interface InBitmapProvider { |
| Bitmap forPixelCount(int count); |
| } |
| } |
| |
| public static class InputStreamSource extends BitmapSource { |
| private final InputStreamProvider mStreamProvider; |
| private final Context mContext; |
| |
| public InputStreamSource(Context context, Uri uri) { |
| this(InputStreamProvider.fromUri(context, uri), context); |
| } |
| |
| public InputStreamSource(Resources res, int resId, Context context) { |
| this(InputStreamProvider.fromResource(res, resId), context); |
| } |
| |
| public InputStreamSource(InputStreamProvider streamProvider, Context context) { |
| mStreamProvider = streamProvider; |
| mContext = context; |
| } |
| |
| @Override |
| public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { |
| try { |
| InputStream is = mStreamProvider.newStreamNotNull(); |
| SimpleBitmapRegionDecoder regionDecoder = |
| SimpleBitmapRegionDecoderWrapper.newInstance(is, false); |
| Utils.closeSilently(is); |
| if (regionDecoder == null) { |
| is = mStreamProvider.newStreamNotNull(); |
| regionDecoder = DumbBitmapRegionDecoder.newInstance(is); |
| Utils.closeSilently(is); |
| } |
| return regionDecoder; |
| } catch (IOException e) { |
| Log.e("InputStreamSource", "Failed to load stream", e); |
| return null; |
| } |
| } |
| |
| @Override |
| public int getExifRotation() { |
| return mStreamProvider.getRotationFromExif(mContext); |
| } |
| |
| @Override |
| public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { |
| try { |
| InputStream is = mStreamProvider.newStreamNotNull(); |
| Bitmap b = BitmapFactory.decodeStream(is, null, options); |
| Utils.closeSilently(is); |
| return b; |
| } catch (IOException | OutOfMemoryError e) { |
| Log.e("InputStreamSource", "Failed to load stream", e); |
| return null; |
| } |
| } |
| } |
| |
| public static class FilePathBitmapSource extends InputStreamSource { |
| private String mPath; |
| public FilePathBitmapSource(File file, Context context) { |
| super(context, Uri.fromFile(file)); |
| mPath = file.getAbsolutePath(); |
| } |
| |
| @Override |
| public int getExifRotation() { |
| return ExifOrientation.readRotation(mPath); |
| } |
| } |
| |
| SimpleBitmapRegionDecoder mDecoder; |
| int mWidth; |
| int mHeight; |
| int mTileSize; |
| private BasicTexture mPreview; |
| private final int mRotation; |
| |
| // For use only by getTile |
| private Rect mWantRegion = new Rect(); |
| private BitmapFactory.Options mOptions; |
| |
| public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { |
| mTileSize = TiledImageRenderer.suggestedTileSize(context); |
| mRotation = source.getRotation(); |
| mDecoder = source.getBitmapRegionDecoder(); |
| if (mDecoder != null) { |
| mWidth = mDecoder.getWidth(); |
| mHeight = mDecoder.getHeight(); |
| mOptions = new BitmapFactory.Options(); |
| mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; |
| mOptions.inPreferQualityOverSpeed = true; |
| mOptions.inTempStorage = tempStorage; |
| |
| Bitmap preview = source.getPreviewBitmap(); |
| if (preview != null && |
| preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { |
| mPreview = new BitmapTexture(preview); |
| } else { |
| Log.w(TAG, String.format( |
| "Failed to create preview of apropriate size! " |
| + " in: %dx%d, out: %dx%d", |
| mWidth, mHeight, |
| preview == null ? -1 : preview.getWidth(), |
| preview == null ? -1 : preview.getHeight())); |
| } |
| } |
| } |
| |
| public Bitmap getBitmap() { |
| return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; |
| } |
| |
| @Override |
| public int getTileSize() { |
| return mTileSize; |
| } |
| |
| @Override |
| public int getImageWidth() { |
| return mWidth; |
| } |
| |
| @Override |
| public int getImageHeight() { |
| return mHeight; |
| } |
| |
| @Override |
| public BasicTexture getPreview() { |
| return mPreview; |
| } |
| |
| @Override |
| public int getRotation() { |
| return mRotation; |
| } |
| |
| @Override |
| public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { |
| int tileSize = getTileSize(); |
| int t = tileSize << level; |
| mWantRegion.set(x, y, x + t, y + t); |
| |
| if (bitmap == null) { |
| bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); |
| } |
| |
| mOptions.inSampleSize = (1 << level); |
| mOptions.inBitmap = bitmap; |
| |
| try { |
| bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); |
| } finally { |
| if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { |
| mOptions.inBitmap = null; |
| } |
| } |
| |
| if (bitmap == null) { |
| Log.w("BitmapRegionTileSource", "fail in decoding region"); |
| } |
| return bitmap; |
| } |
| } |