Merge "Make wallpaper cropper more robust " into klp-dev
diff --git a/packages/WallpaperCropper/res/values/strings.xml b/packages/WallpaperCropper/res/values/strings.xml
index 2b8111d..091869a 100644
--- a/packages/WallpaperCropper/res/values/strings.xml
+++ b/packages/WallpaperCropper/res/values/strings.xml
@@ -17,4 +17,9 @@
     <string name="crop_wallpaper">Crop wallpaper</string>
     <!-- Button label on Wallpaper picker screen; user selects this button to set a specific wallpaper -->
     <string name="wallpaper_instructions">Set wallpaper</string>
+    <!-- Error message when an image is selected as a wallpaper,
+         but the wallpaper cropper cannot load it. The user will
+         usually see this when using another app and trying to set
+         an image as the wallpaper -->
+    <string name="wallpaper_load_fail">Couldn\'t load image as wallpaper</string>
 </resources>
diff --git a/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java b/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java
index b5774f4..e14f89a 100644
--- a/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java
+++ b/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java
@@ -31,6 +31,7 @@
 import android.util.Log;
 
 import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.BitmapTexture;
@@ -41,6 +42,85 @@
 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(String pathName, boolean isShareable) {
+        try {
+            BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
+            if (d != null) {
+                return new SimpleBitmapRegionDecoderWrapper(d);
+            }
+        } catch (IOException e) {
+            Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
+            return null;
+        }
+        return null;
+    }
+    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 {
+    //byte[] streamCopy;
+    Bitmap mBuffer;
+    private DumbBitmapRegionDecoder(Bitmap b) {
+        mBuffer = b;
+    }
+    public static DumbBitmapRegionDecoder newInstance(String pathName) {
+        Bitmap b = BitmapFactory.decodeFile(pathName);
+        if (b != null) {
+            return new DumbBitmapRegionDecoder(b);
+        }
+        return null;
+    }
+    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) {
+        System.out.println("DECODING WITH SAMPLE LEVEL OF " + options.inSampleSize);
+        return Bitmap.createBitmap(
+                mBuffer, wantRegion.left, wantRegion.top, wantRegion.width(), wantRegion.height());
+    }
+}
+
 /**
  * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
  * {@link BitmapRegionDecoder} to wrap a local file
@@ -58,14 +138,16 @@
     public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
 
     public static abstract class BitmapSource {
-        private BitmapRegionDecoder mDecoder;
+        private SimpleBitmapRegionDecoder mDecoder;
         private Bitmap mPreview;
         private int mPreviewSize;
         private int mRotation;
+        public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
+        private State mState = State.NOT_LOADED;
         public BitmapSource(int previewSize) {
             mPreviewSize = previewSize;
         }
-        public void loadInBackground() {
+        public boolean loadInBackground() {
             ExifInterface ei = new ExifInterface();
             if (readExif(ei)) {
                 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
@@ -74,22 +156,33 @@
                 }
             }
             mDecoder = loadBitmapRegionDecoder();
-            int width = mDecoder.getWidth();
-            int height = mDecoder.getHeight();
-            if (mPreviewSize != 0) {
-                int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
-                BitmapFactory.Options opts = new BitmapFactory.Options();
-                opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
-                opts.inPreferQualityOverSpeed = true;
+            if (mDecoder == null) {
+                mState = State.ERROR_LOADING;
+                return false;
+            } else {
+                int width = mDecoder.getWidth();
+                int height = mDecoder.getHeight();
+                if (mPreviewSize != 0) {
+                    int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
+                    BitmapFactory.Options opts = new BitmapFactory.Options();
+                    opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                    opts.inPreferQualityOverSpeed = true;
 
-                float scale = (float) previewSize / Math.max(width, height);
-                opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
-                opts.inJustDecodeBounds = false;
-                mPreview = loadPreviewBitmap(opts);
+                    float scale = (float) previewSize / Math.max(width, height);
+                    opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+                    opts.inJustDecodeBounds = false;
+                    mPreview = loadPreviewBitmap(opts);
+                }
+                mState = State.LOADED;
+                return true;
             }
         }
 
-        public BitmapRegionDecoder getBitmapRegionDecoder() {
+        public State getLoadingState() {
+            return mState;
+        }
+
+        public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
             return mDecoder;
         }
 
@@ -106,7 +199,7 @@
         }
 
         public abstract boolean readExif(ExifInterface ei);
-        public abstract BitmapRegionDecoder loadBitmapRegionDecoder();
+        public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
         public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
     }
 
@@ -117,13 +210,13 @@
             mPath = path;
         }
         @Override
-        public BitmapRegionDecoder loadBitmapRegionDecoder() {
-            try {
-                return BitmapRegionDecoder.newInstance(mPath, true);
-            } catch (IOException e) {
-                Log.w("BitmapRegionTileSource", "getting decoder failed", e);
-                return null;
+        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+            SimpleBitmapRegionDecoder d;
+            d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
+            if (d == null) {
+                d = DumbBitmapRegionDecoder.newInstance(mPath);
             }
+            return d;
         }
         @Override
         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
@@ -154,9 +247,17 @@
             return new BufferedInputStream(is);
         }
         @Override
-        public BitmapRegionDecoder loadBitmapRegionDecoder() {
+        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
             try {
-                return BitmapRegionDecoder.newInstance(regenerateInputStream(), true);
+                InputStream is = regenerateInputStream();
+                SimpleBitmapRegionDecoder regionDecoder =
+                        SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
+                Utils.closeSilently(is);
+                if (regionDecoder == null) {
+                    is = regenerateInputStream();
+                    regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
+                }
+                return regionDecoder;
             } catch (FileNotFoundException e) {
                 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return null;
@@ -168,7 +269,10 @@
         @Override
         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
             try {
-                return BitmapFactory.decodeStream(regenerateInputStream(), null, options);
+                InputStream is = regenerateInputStream();
+                Bitmap b = BitmapFactory.decodeStream(is, null, options);
+                Utils.closeSilently(is);
+                return b;
             } catch (FileNotFoundException e) {
                 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return null;
@@ -177,13 +281,15 @@
         @Override
         public boolean readExif(ExifInterface ei) {
             try {
-                ei.readExif(regenerateInputStream());
+                InputStream is = regenerateInputStream();
+                ei.readExif(is);
+                Utils.closeSilently(is);
                 return true;
             } catch (FileNotFoundException e) {
                 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return false;
             } catch (IOException e) {
-                Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
+                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return false;
             }
         }
@@ -202,13 +308,16 @@
             return new BufferedInputStream(is);
         }
         @Override
-        public BitmapRegionDecoder loadBitmapRegionDecoder() {
-            try {
-                return BitmapRegionDecoder.newInstance(regenerateInputStream(), true);
-            } catch (IOException e) {
-                Log.e("BitmapRegionTileSource", "Error reading resource", e);
-                return null;
+        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+            InputStream is = regenerateInputStream();
+            SimpleBitmapRegionDecoder regionDecoder =
+                    SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
+            Utils.closeSilently(is);
+            if (regionDecoder == null) {
+                is = regenerateInputStream();
+                regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
             }
+            return regionDecoder;
         }
         @Override
         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
@@ -217,7 +326,9 @@
         @Override
         public boolean readExif(ExifInterface ei) {
             try {
-                ei.readExif(regenerateInputStream());
+                InputStream is = regenerateInputStream();
+                ei.readExif(is);
+                Utils.closeSilently(is);
                 return true;
             } catch (IOException e) {
                 Log.e("BitmapRegionTileSource", "Error reading resource", e);
@@ -226,7 +337,7 @@
         }
     }
 
-    BitmapRegionDecoder mDecoder;
+    SimpleBitmapRegionDecoder mDecoder;
     int mWidth;
     int mHeight;
     int mTileSize;
@@ -243,27 +354,29 @@
         mTileSize = TiledImageRenderer.suggestedTileSize(context);
         mRotation = source.getRotation();
         mDecoder = source.getBitmapRegionDecoder();
-        mWidth = mDecoder.getWidth();
-        mHeight = mDecoder.getHeight();
-        mOptions = new BitmapFactory.Options();
-        mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
-        mOptions.inPreferQualityOverSpeed = true;
-        mOptions.inTempStorage = new byte[16 * 1024];
-        int previewSize = source.getPreviewSize();
-        if (previewSize != 0) {
-            previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
-            // Although this is the same size as the Bitmap that is likely already
-            // loaded, the lifecycle is different and interactions are on a different
-            // thread. Thus to simplify, this source will decode its own bitmap.
-            Bitmap preview = decodePreview(source, previewSize);
-            if (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.getWidth(), preview.getHeight()));
+        if (mDecoder != null) {
+            mWidth = mDecoder.getWidth();
+            mHeight = mDecoder.getHeight();
+            mOptions = new BitmapFactory.Options();
+            mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            mOptions.inPreferQualityOverSpeed = true;
+            mOptions.inTempStorage = new byte[16 * 1024];
+            int previewSize = source.getPreviewSize();
+            if (previewSize != 0) {
+                previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+                // Although this is the same size as the Bitmap that is likely already
+                // loaded, the lifecycle is different and interactions are on a different
+                // thread. Thus to simplify, this source will decode its own bitmap.
+                Bitmap preview = decodePreview(source, previewSize);
+                if (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.getWidth(), preview.getHeight()));
+                }
             }
         }
     }
diff --git a/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java b/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java
index 220f567..d12140d 100644
--- a/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java
+++ b/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java
@@ -41,10 +41,12 @@
 import android.view.Display;
 import android.view.View;
 import android.view.WindowManager;
+import android.widget.Toast;
 
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.photos.BitmapRegionTileSource;
+import com.android.photos.BitmapRegionTileSource.BitmapSource;
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
@@ -109,12 +111,24 @@
                 });
 
         // Load image in background
-        setCropViewTileSource(
-                new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024), true, false);
+        final BitmapRegionTileSource.UriBitmapSource bitmapSource =
+                new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
+        Runnable onLoad = new Runnable() {
+            public void run() {
+                if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
+                    Toast.makeText(WallpaperCropActivity.this,
+                            getString(R.string.wallpaper_load_fail),
+                            Toast.LENGTH_LONG).show();
+                    finish();
+                }
+            }
+        };
+        setCropViewTileSource(bitmapSource, true, false, onLoad);
     }
 
-    public void setCropViewTileSource(final BitmapRegionTileSource.BitmapSource bitmapSource,
-            final boolean touchEnabled, final boolean moveToLeft) {
+    public void setCropViewTileSource(
+            final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
+            final boolean moveToLeft, final Runnable postExecute) {
         final Context context = WallpaperCropActivity.this;
         final View progressView = findViewById(R.id.loading);
         final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
@@ -127,13 +141,18 @@
             protected void onPostExecute(Void arg) {
                 if (!isCancelled()) {
                     progressView.setVisibility(View.INVISIBLE);
-                    mCropView.setTileSource(
-                            new BitmapRegionTileSource(context, bitmapSource), null);
-                    mCropView.setTouchEnabled(touchEnabled);
-                    if (moveToLeft) {
-                        mCropView.moveToLeft();
+                    if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
+                        mCropView.setTileSource(
+                                new BitmapRegionTileSource(context, bitmapSource), null);
+                        mCropView.setTouchEnabled(touchEnabled);
+                        if (moveToLeft) {
+                            mCropView.moveToLeft();
+                        }
                     }
                 }
+                if (postExecute != null) {
+                    postExecute.run();
+                }
             }
         };
         // We don't want to show the spinner every time we load an image, because that would be
@@ -235,10 +254,12 @@
                 InputStream is = context.getContentResolver().openInputStream(uri);
                 BufferedInputStream bis = new BufferedInputStream(is);
                 ei.readExif(bis);
+                bis.close();
             } else {
                 InputStream is = res.openRawResource(resId);
                 BufferedInputStream bis = new BufferedInputStream(is);
                 ei.readExif(bis);
+                bis.close();
             }
             Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
             if (ori != null) {
@@ -408,7 +429,6 @@
         String mInFilePath;
         byte[] mInImageBytes;
         int mInResId = 0;
-        InputStream mInStream;
         RectF mCropBounds = null;
         int mOutWidth, mOutHeight;
         int mRotation;
@@ -481,37 +501,36 @@
         }
 
         // Helper to setup input stream
-        private void regenerateInputStream() {
+        private InputStream regenerateInputStream() {
             if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
                 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
                         "image byte array given");
             } else {
-                Utils.closeSilently(mInStream);
                 try {
                     if (mInUri != null) {
-                        mInStream = new BufferedInputStream(
+                        return new BufferedInputStream(
                                 mContext.getContentResolver().openInputStream(mInUri));
                     } else if (mInFilePath != null) {
-                        mInStream = mContext.openFileInput(mInFilePath);
+                        return mContext.openFileInput(mInFilePath);
                     } else if (mInImageBytes != null) {
-                        mInStream = new BufferedInputStream(
-                                new ByteArrayInputStream(mInImageBytes));
+                        return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
                     } else {
-                        mInStream = new BufferedInputStream(
-                                mResources.openRawResource(mInResId));
+                        return new BufferedInputStream(mResources.openRawResource(mInResId));
                     }
                 } catch (FileNotFoundException e) {
                     Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
                 }
             }
+            return null;
         }
 
         public Point getImageBounds() {
-            regenerateInputStream();
-            if (mInStream != null) {
+            InputStream is = regenerateInputStream();
+            if (is != null) {
                 BitmapFactory.Options options = new BitmapFactory.Options();
                 options.inJustDecodeBounds = true;
-                BitmapFactory.decodeStream(mInStream, null, options);
+                BitmapFactory.decodeStream(is, null, options);
+                Utils.closeSilently(is);
                 if (options.outWidth != 0 && options.outHeight != 0) {
                     return new Point(options.outWidth, options.outHeight);
                 }
@@ -529,22 +548,26 @@
         public boolean cropBitmap() {
             boolean failure = false;
 
-            regenerateInputStream();
 
             WallpaperManager wallpaperManager = null;
             if (mSetWallpaper) {
                 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
             }
-            if (mSetWallpaper && mNoCrop && mInStream != null) {
+
+
+            if (mSetWallpaper && mNoCrop) {
                 try {
-                    wallpaperManager.setStream(mInStream);
+                    InputStream is = regenerateInputStream();
+                    if (is != null) {
+                        wallpaperManager.setStream(is);
+                        Utils.closeSilently(is);
+                    }
                 } catch (IOException e) {
                     Log.w(LOGTAG, "cannot write stream to wallpaper", e);
                     failure = true;
                 }
                 return !failure;
-            }
-            if (mInStream != null) {
+            } else {
                 // Find crop bounds (scaled to original image size)
                 Rect roundedTrueCrop = new Rect();
                 Matrix rotateMatrix = new Matrix();
@@ -557,6 +580,11 @@
                     mCropBounds = new RectF(roundedTrueCrop);
 
                     Point bounds = getImageBounds();
+                    if (bounds == null) {
+                        Log.w(LOGTAG, "cannot get bounds for image");
+                        failure = true;
+                        return false;
+                    }
 
                     float[] rotatedBounds = new float[] { bounds.x, bounds.y };
                     rotateMatrix.mapPoints(rotatedBounds);
@@ -567,7 +595,6 @@
                     inverseRotateMatrix.mapRect(mCropBounds);
                     mCropBounds.offset(bounds.x/2, bounds.y/2);
 
-                    regenerateInputStream();
                 }
 
                 mCropBounds.roundOut(roundedTrueCrop);
@@ -585,7 +612,14 @@
                 // Attempt to open a region decoder
                 BitmapRegionDecoder decoder = null;
                 try {
-                    decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+                    InputStream is = regenerateInputStream();
+                    if (is == null) {
+                        Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
+                        failure = true;
+                        return false;
+                    }
+                    decoder = BitmapRegionDecoder.newInstance(is, false);
+                    Utils.closeSilently(is);
                 } catch (IOException e) {
                     Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
                 }
@@ -603,14 +637,15 @@
 
                 if (crop == null) {
                     // BitmapRegionDecoder has failed, try to crop in-memory
-                    regenerateInputStream();
+                    InputStream is = regenerateInputStream();
                     Bitmap fullSize = null;
-                    if (mInStream != null) {
+                    if (is != null) {
                         BitmapFactory.Options options = new BitmapFactory.Options();
                         if (scaleDownSampleSize > 1) {
                             options.inSampleSize = scaleDownSampleSize;
                         }
-                        fullSize = BitmapFactory.decodeStream(mInStream, null, options);
+                        fullSize = BitmapFactory.decodeStream(is, null, options);
+                        Utils.closeSilently(is);
                     }
                     if (fullSize != null) {
                         mCropBounds.left /= scaleDownSampleSize;