am 29c041a3: am 88d18b9f: am 4054bd6d: Use gesture detector from support lib

* commit '29c041a3e41b93c7ffd8f29550df501f0b45ea02':
  Use gesture detector from support lib
diff --git a/carousel/test/res/values-in/strings.xml b/carousel/test/res/values-in/strings.xml
index 40000e8..06c8812 100644
--- a/carousel/test/res/values-in/strings.xml
+++ b/carousel/test/res/values-in/strings.xml
@@ -21,7 +21,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="music_demo_activity_label" msgid="4382090808250495841">"KaruselMusik"</string>
     <string name="carousel_test_activity_label" msgid="6014624482213318747">"UjiKarusel"</string>
-    <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Karusel"</string>
+    <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Korsel"</string>
     <string name="task_switcher_activity_label" msgid="714620143340933546">"PengubahTugas"</string>
     <string name="recent_tasks_title" msgid="1030287226205477117">"Aplikasi Terbaru"</string>
     <string name="no_recent_tasks" msgid="6884096266670555780">"Tidak ada tugas terbaru"</string>
diff --git a/chips/res/values-ko/strings.xml b/chips/res/values-ko/strings.xml
index 7423ce5..f7884bd 100644
--- a/chips/res/values-ko/strings.xml
+++ b/chips/res/values-ko/strings.xml
@@ -19,5 +19,5 @@
     <string name="more_string" msgid="8495478259330621990">"<xliff:g id="COUNT">%1$s</xliff:g>명 이상"</string>
     <string name="copy_email" msgid="7869435992461603532">"이메일 주소 복사"</string>
     <string name="copy_number" msgid="530057841276106843">"전화번호 복사"</string>
-    <string name="done" msgid="2356320650733788862">"Enter 키"</string>
+    <string name="done" msgid="2356320650733788862">"입력"</string>
 </resources>
diff --git a/photoviewer/.gitignore b/photoviewer/.gitignore
new file mode 100644
index 0000000..ff7ef7d
--- /dev/null
+++ b/photoviewer/.gitignore
@@ -0,0 +1,8 @@
+*~
+*.bak
+*.class
+bin/
+gen/
+*.properties
+.classpath
+.project
diff --git a/photoviewer/AndroidManifest.xml b/photoviewer/AndroidManifest.xml
index 485e044..e4e9101 100644
--- a/photoviewer/AndroidManifest.xml
+++ b/photoviewer/AndroidManifest.xml
@@ -18,4 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ex.photo"
     android:versionCode="1">
+
+    <uses-sdk
+        android:minSdkVersion="11"/>
 </manifest>
\ No newline at end of file
diff --git a/photoviewer/src/com/android/ex/photo/Intents.java b/photoviewer/src/com/android/ex/photo/Intents.java
index 0e64730..e1e77d3 100644
--- a/photoviewer/src/com/android/ex/photo/Intents.java
+++ b/photoviewer/src/com/android/ex/photo/Intents.java
@@ -34,6 +34,7 @@
     public static final String EXTRA_RESOLVED_PHOTO_URI = "resolved_photo_uri";
     public static final String EXTRA_PROJECTION = "projection";
     public static final String EXTRA_THUMBNAIL_URI = "thumbnail_uri";
+    public static final String EXTRA_MAX_INITIAL_SCALE = "max_scale";
 
     /**
      * Gets a photo view intent builder to display the photos from phone activity.
@@ -75,6 +76,8 @@
         private String[] mProjection;
         /** The URI of a thumbnail of the photo to display */
         private String mThumbnailUri;
+        /** The maximum scale to display images at before  */
+        private Float mMaxInitialScale;
 
         private PhotoViewIntentBuilder(Context context, Class<?> cls) {
             mIntent = new Intent(context, cls);
@@ -116,6 +119,14 @@
             return this;
         }
 
+        /**
+         * Sets the maximum scale which an image is initially displayed at
+         */
+        public PhotoViewIntentBuilder setMaxInitialScale(float maxScale) {
+            mMaxInitialScale = maxScale;
+            return this;
+        }
+
         /** Build the intent */
         public Intent build() {
             mIntent.setAction(Intent.ACTION_VIEW);
@@ -142,6 +153,10 @@
                 mIntent.putExtra(EXTRA_THUMBNAIL_URI, mThumbnailUri);
             }
 
+            if (mMaxInitialScale != null) {
+                mIntent.putExtra(EXTRA_MAX_INITIAL_SCALE, mMaxInitialScale);
+            }
+
             return mIntent;
         }
     }
diff --git a/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java b/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
index 5231ca1..5e45f5e 100644
--- a/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
+++ b/photoviewer/src/com/android/ex/photo/PhotoViewActivity.java
@@ -31,6 +31,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.view.MenuItem;
 import android.view.View;
 
 import com.android.ex.photo.PhotoViewPager.InterceptType;
@@ -133,6 +134,8 @@
     private boolean mRestartLoader;
     /** Whether or not this activity is paused */
     private boolean mIsPaused = true;
+    /** The maximum scale factor applied to images when they are initially displayed */
+    private float mMaxInitialScale;
     private final Handler mHandler = new Handler();
     // TODO Find a better way to do this. We basically want the activity to display the
     // "loading..." progress until the fragment takes over and shows it's own "loading..."
@@ -176,12 +179,16 @@
         if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) {
             currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
         }
+
+        // Set the max initial scale, defaulting to 1x
+        mMaxInitialScale = mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f);
+
         mPhotoIndex = currentItem;
 
         setContentView(R.layout.photo_activity_view);
 
         // Create the adapter and add the view pager
-        mAdapter = new PhotoPagerAdapter(this, getFragmentManager(), null);
+        mAdapter = new PhotoPagerAdapter(this, getFragmentManager(), null, mMaxInitialScale);
 
         mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
         mViewPager.setAdapter(mAdapter);
@@ -236,6 +243,16 @@
         outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
     }
 
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+       switch (item.getItemId()) {
+          case android.R.id.home:
+             finish();
+          default:
+             return super.onOptionsItemSelected(item);
+       }
+    }
+
     public void addScreenListener(OnScreenListener listener) {
         mScreenListeners.add(listener);
     }
@@ -306,6 +323,9 @@
                 }
                 mIsEmpty = false;
 
+                mAdapter.swapCursor(data);
+                notifyCursorListeners(data);
+
                 // set the selected photo
                 int itemIndex = mPhotoIndex;
 
@@ -314,9 +334,6 @@
                     itemIndex = 0;
                 }
 
-                mAdapter.swapCursor(data);
-                notifyCursorListeners(data);
-
                 mViewPager.setCurrentItem(itemIndex, false);
                 setViewActivated();
             }
@@ -398,7 +415,7 @@
     /**
      * Updates the title bar according to the value of {@link #mFullScreen}.
      */
-    private void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
+    protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
         final boolean fullScreenChanged = (fullScreen != mFullScreen);
         mFullScreen = fullScreen;
 
@@ -428,7 +445,7 @@
         mHandler.removeCallbacks(mActionBarHideRunnable);
     }
 
-    private void setLightsOutMode(boolean enabled) {
+    protected void setLightsOutMode(boolean enabled) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
             int flags = enabled
                     ? View.SYSTEM_UI_FLAG_LOW_PROFILE
@@ -457,7 +474,7 @@
     private Runnable mActionBarHideRunnable = new Runnable() {
         @Override
         public void run() {
-            PhotoViewActivity.this.setLightsOutMode(true);
+            setFullScreen(true, true);
         }
     };
 
@@ -532,4 +549,12 @@
             postActionBarHideRunnableWithDelay();
         }
     }
+
+    protected boolean isFullScreen() {
+        return mFullScreen;
+    }
+
+    protected void setPhotoIndex(int index) {
+        mPhotoIndex = index;
+    }
 }
diff --git a/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
index 848d79a..ae2c92e 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/BaseCursorPagerAdapter.java
@@ -38,13 +38,12 @@
     private static final String TAG = "BaseCursorPagerAdapter";
 
     Context mContext;
-    private boolean mDataValid;
     private Cursor mCursor;
     private int mRowIDColumn;
     /** Mapping of row ID to cursor position */
     private SparseIntArray mItemPosition;
     /** Mapping of instantiated object to row ID */
-    private HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
+    private final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
 
     /**
      * Constructor that always enables auto-requery.
@@ -67,9 +66,11 @@
      */
     public abstract Fragment getItem(Context context, Cursor cursor, int position);
 
+    // TODO: This shouldn't just return null - maybe it needs to wait for a cursor to be supplied?
+    //       See b/7103023
     @Override
     public Fragment getItem(int position) {
-        if (mDataValid && moveCursorTo(position)) {
+        if (mCursor != null && moveCursorTo(position)) {
             return getItem(mContext, mCursor, position);
         }
         return null;
@@ -77,7 +78,7 @@
 
     @Override
     public int getCount() {
-        if (mDataValid && mCursor != null) {
+        if (mCursor != null) {
             return mCursor.getCount();
         } else {
             return 0;
@@ -86,7 +87,7 @@
 
     @Override
     public Object instantiateItem(View container, int position) {
-        if (!mDataValid) {
+        if (mCursor == null) {
             throw new IllegalStateException("this should only be called when the cursor is valid");
         }
 
@@ -127,7 +128,7 @@
      * @return true if data is valid
      */
     public boolean isDataValid() {
-        return mDataValid;
+        return mCursor != null;
     }
 
     /**
@@ -141,7 +142,7 @@
      * Returns the data item associated with the specified position in the data set.
      */
     public Object getDataItem(int position) {
-        if (mDataValid && moveCursorTo(position)) {
+        if (mCursor != null && moveCursorTo(position)) {
             return mCursor;
         } else {
             return null;
@@ -152,7 +153,7 @@
      * Returns the row id associated with the specified position in the list.
      */
     public long getItemId(int position) {
-        if (mDataValid && moveCursorTo(position)) {
+        if (mCursor != null && moveCursorTo(position)) {
             return mCursor.getString(mRowIDColumn).hashCode();
         } else {
             return 0;
@@ -180,10 +181,8 @@
         mCursor = newCursor;
         if (newCursor != null) {
             mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
-            mDataValid = true;
         } else {
             mRowIDColumn = -1;
-            mDataValid = false;
         }
 
         setItemPosition();
@@ -231,7 +230,6 @@
     private void init(Context context, Cursor c) {
         boolean cursorPresent = c != null;
         mCursor = c;
-        mDataValid = cursorPresent;
         mContext = context;
         mRowIDColumn = cursorPresent
                 ? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
@@ -242,7 +240,7 @@
      * row id to cursor position.
      */
     private void setItemPosition() {
-        if (!mDataValid || mCursor == null || mCursor.isClosed()) {
+        if (mCursor == null || mCursor.isClosed()) {
             mItemPosition = null;
             return;
         }
diff --git a/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
index 9c24575..2065b2a 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/BaseFragmentPagerAdapter.java
@@ -86,6 +86,10 @@
             mCurTransaction.attach(fragment);
         } else {
             fragment = getItem(position);
+            if(fragment == null) {
+                if (DEBUG) Log.e(TAG, "NPE workaround for getItem(). See b/7103023");
+                return null;
+            }
             if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
             mCurTransaction.add(container.getId(), fragment,
                     makeFragmentName(container.getId(), position));
diff --git a/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java b/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
index bf75ecb..4536432 100644
--- a/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
+++ b/photoviewer/src/com/android/ex/photo/adapters/PhotoPagerAdapter.java
@@ -33,24 +33,38 @@
 public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
     private int mContentUriIndex;
     private int mThumbnailUriIndex;
+    private int mLoadingIndex;
+    private final float mMaxScale;
 
-    public PhotoPagerAdapter(Context context, FragmentManager fm, Cursor c) {
+    public PhotoPagerAdapter(Context context, FragmentManager fm, Cursor c, float maxScale) {
         super(context, fm, c);
+        mMaxScale = maxScale;
     }
 
     @Override
     public Fragment getItem(Context context, Cursor cursor, int position) {
         final String photoUri = cursor.getString(mContentUriIndex);
         final String thumbnailUri = cursor.getString(mThumbnailUriIndex);
+        boolean loading;
+        if(mLoadingIndex != -1) {
+            loading = Boolean.valueOf(cursor.getString(mLoadingIndex));
+        } else {
+            loading = false;
+        }
+        boolean onlyShowSpinner = false;
+        if(photoUri == null && loading) {
+            onlyShowSpinner = true;
+        }
 
         // create new PhotoViewFragment
         final PhotoViewIntentBuilder builder =
                 Intents.newPhotoViewFragmentIntentBuilder(mContext);
         builder
             .setResolvedPhotoUri(photoUri)
-            .setThumbnailUri(thumbnailUri);
+            .setThumbnailUri(thumbnailUri)
+            .setMaxInitialScale(mMaxScale);
 
-        return new PhotoViewFragment(builder.build(), position, this);
+        return new PhotoViewFragment(builder.build(), position, this, onlyShowSpinner);
     }
 
     @Override
@@ -60,9 +74,12 @@
                     newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.CONTENT_URI);
             mThumbnailUriIndex =
                     newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.THUMBNAIL_URI);
+            mLoadingIndex =
+                    newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.LOADING_INDICATOR);
         } else {
             mContentUriIndex = -1;
             mThumbnailUriIndex = -1;
+            mLoadingIndex = -1;
         }
 
         return super.swapCursor(newCursor);
diff --git a/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java b/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
index 429c403..a721b26 100644
--- a/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
+++ b/photoviewer/src/com/android/ex/photo/fragments/PhotoViewFragment.java
@@ -106,6 +106,9 @@
     /** Whether or not the fragment should make the photo full-screen */
     private boolean mFullScreen;
 
+    /** Whether or not this fragment will only show the loading spinner */
+    private final boolean mOnlyShowSpinner;
+
     /** Whether or not the progress bar is showing valid information about the progress stated */
     private boolean mProgressBarNeeded = true;
 
@@ -113,13 +116,16 @@
 
     public PhotoViewFragment() {
         mPosition = -1;
+        mOnlyShowSpinner = false;
         mProgressBarNeeded = true;
     }
 
-    public PhotoViewFragment(Intent intent, int position, PhotoPagerAdapter adapter) {
+    public PhotoViewFragment(Intent intent, int position, PhotoPagerAdapter adapter,
+            boolean onlyShowSpinner) {
         mIntent = intent;
         mPosition = position;
         mAdapter = adapter;
+        mOnlyShowSpinner = onlyShowSpinner;
         mProgressBarNeeded = true;
     }
 
@@ -184,6 +190,7 @@
         final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
 
         mPhotoView = (PhotoView) view.findViewById(R.id.photo_view);
+        mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1));
         mPhotoView.setOnClickListener(this);
         mPhotoView.setFullScreen(mFullScreen, false);
         mPhotoView.enableImageTransforms(true);
@@ -209,7 +216,7 @@
         mCallback.addScreenListener(this);
         mCallback.addCursorListener(this);
 
-        getLoaderManager().initLoader(LOADER_ID_PHOTO, null, this);
+        getLoaderManager().initLoader(LOADER_ID_THUMBNAIL, null, this);
 
         super.onResume();
     }
@@ -245,6 +252,9 @@
 
     @Override
     public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
+        if(mOnlyShowSpinner) {
+            return null;
+        }
         switch (id) {
             case LOADER_ID_PHOTO:
                 return new PhotoBitmapLoader(getActivity(), mResolvedPhotoUri);
@@ -276,23 +286,19 @@
                 }
                 break;
             case LOADER_ID_THUMBNAIL:
+                mProgressBarNeeded = false;
                 if (isPhotoBound()) {
                     // There is need to do anything with the thumbnail image, as the full size
                     // image is being shown.
                     mPhotoPreviewAndProgress.setVisibility(View.GONE);
-                    mProgressBarNeeded = false;
                     return;
                 } else if (data == null) {
                     // no preview, show default
                     mPhotoPreviewImage.setVisibility(View.VISIBLE);
                     mPhotoPreviewImage.setImageResource(R.drawable.default_image);
-
-                    mProgressBarNeeded = false;
                 } else {
-                    mPhotoPreviewImage.setVisibility(View.VISIBLE);
-                    mPhotoPreviewImage.setImageBitmap(data);
-
-                    mProgressBarNeeded = false;
+                    bindPhoto(data);
+                    getLoaderManager().initLoader(LOADER_ID_PHOTO, null, this);
                 }
                 break;
             default:
diff --git a/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java b/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
index 439b68b..8483620 100644
--- a/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
+++ b/photoviewer/src/com/android/ex/photo/provider/PhotoContract.java
@@ -48,6 +48,11 @@
          * This string column is the MIME type.
          */
         public static final String CONTENT_TYPE = "contentType";
+        /**
+         * This boolean column indicates that a loading indicator should display permenantly
+         * if no image urls are provided.
+         */
+        public static final String LOADING_INDICATOR = "loadingIndicator";
 
     }
 
diff --git a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
index d852d65..a9790eb 100644
--- a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
+++ b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java
@@ -1,175 +1,197 @@
-/*

- * Copyright (C) 2011 Google Inc.

- * Licensed to 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.ex.photo.util;

-

-import android.content.ContentResolver;

-import android.graphics.Bitmap;

-import android.graphics.BitmapFactory;

-import android.graphics.Point;

-import android.graphics.Rect;

-import android.net.Uri;

-import android.os.Build;

-import android.util.Log;

-

-import com.android.ex.photo.PhotoViewActivity;

-

-import java.io.FileNotFoundException;

-import java.io.IOException;

-import java.io.InputStream;

-

-/**

- * Image utilities

- */

-public class ImageUtils {

-    // Logging

-    private static final String TAG = "ImageUtils";

-

-    /** Minimum class memory class to use full-res photos */

-    private final static long MIN_NORMAL_CLASS = 32;

-    /** Minimum class memory class to use small photos */

-    private final static long MIN_SMALL_CLASS = 24;

-

-    public static enum ImageSize {

-        EXTRA_SMALL,

-        SMALL,

-        NORMAL,

-    }

-

-    public static final ImageSize sUseImageSize;

-    static {

-        // On HC and beyond, assume devices are more capable

-        if (Build.VERSION.SDK_INT >= 11) {

-            sUseImageSize = ImageSize.NORMAL;

-        } else {

-            if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {

-                // We have plenty of memory; use full sized photos

-                sUseImageSize = ImageSize.NORMAL;

-            } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {

-                // We have slight less memory; use smaller sized photos

-                sUseImageSize = ImageSize.SMALL;

-            } else {

-                // We have little memory; use very small sized photos

-                sUseImageSize = ImageSize.EXTRA_SMALL;

-            }

-        }

-    }

-

-    /**

-     * @return true if the MimeType type is image

-     */

-    public static boolean isImageMimeType(String mimeType) {

-        return mimeType != null && mimeType.startsWith("image/");

-    }

-

-    /**

-     * Create a bitmap from a local URI

-     *

-     * @param resolver The ContentResolver

-     * @param uri The local URI

-     * @param maxSize The maximum size (either width or height)

-     *

-     * @return The new bitmap or null

-     */

-    public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {

-        InputStream inputStream = null;

-        try {

-            final BitmapFactory.Options opts = new BitmapFactory.Options();

-            final Point bounds = getImageBounds(resolver, uri);

-

-            inputStream = resolver.openInputStream(uri);

-            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);

-

-            final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);

-

-            // Correct thumbnail orientation as necessary

+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.ex.photo.util;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.ex.photo.PhotoViewActivity;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Image utilities
+ */
+public class ImageUtils {
+    // Logging
+    private static final String TAG = "ImageUtils";
+
+    /** Minimum class memory class to use full-res photos */
+    private final static long MIN_NORMAL_CLASS = 32;
+    /** Minimum class memory class to use small photos */
+    private final static long MIN_SMALL_CLASS = 24;
+
+    public static enum ImageSize {
+        EXTRA_SMALL,
+        SMALL,
+        NORMAL,
+    }
+
+    public static final ImageSize sUseImageSize;
+    static {
+        // On HC and beyond, assume devices are more capable
+        if (Build.VERSION.SDK_INT >= 11) {
+            sUseImageSize = ImageSize.NORMAL;
+        } else {
+            if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {
+                // We have plenty of memory; use full sized photos
+                sUseImageSize = ImageSize.NORMAL;
+            } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
+                // We have slight less memory; use smaller sized photos
+                sUseImageSize = ImageSize.SMALL;
+            } else {
+                // We have little memory; use very small sized photos
+                sUseImageSize = ImageSize.EXTRA_SMALL;
+            }
+        }
+    }
+
+    /**
+     * @return true if the MimeType type is image
+     */
+    public static boolean isImageMimeType(String mimeType) {
+        return mimeType != null && mimeType.startsWith("image/");
+    }
+
+    /**
+     * Create a bitmap from a local URI
+     *
+     * @param resolver The ContentResolver
+     * @param uri The local URI
+     * @param maxSize The maximum size (either width or height)
+     *
+     * @return The new bitmap or null
+     */
+    public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
+        // TODO: make this method not download the image for both getImageBounds and decodeStream
+        InputStream inputStream = null;
+        try {
+            final BitmapFactory.Options opts = new BitmapFactory.Options();
+            final Point bounds = getImageBounds(resolver, uri);
+
+            inputStream = openInputStream(resolver, uri);
+            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
+
+            final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
+
+            // Correct thumbnail orientation as necessary
             // TODO: Fix rotation if it's actually a problem
             //return rotateBitmap(resolver, uri, decodedBitmap);
             return decodedBitmap;
-

-        } catch (FileNotFoundException exception) {

-            // Do nothing - the photo will appear to be missing

-        } catch (IOException exception) {

-            // Do nothing - the photo will appear to be missing

+
+        } catch (FileNotFoundException exception) {
+            // Do nothing - the photo will appear to be missing
+        } catch (IOException exception) {
+            // Do nothing - the photo will appear to be missing
         } catch (IllegalArgumentException exception) {
             // Do nothing - the photo will appear to be missing
-        } finally {

-            try {

-                if (inputStream != null) {

-                    inputStream.close();

-                }

-            } catch (IOException ignore) {

-            }

-        }

-        return null;

-    }

-

-    /**

-     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,

-     * BitmapFactory.Options)} that returns {@code null} on {@link

-     * OutOfMemoryError}.

-     *

-     * @param is The input stream that holds the raw data to be decoded into a

-     *           bitmap.

-     * @param outPadding If not null, return the padding rect for the bitmap if

-     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If

-     *                   no bitmap is returned (null) then padding is

-     *                   unchanged.

-     * @param opts null-ok; Options that control downsampling and whether the

-     *             image should be completely decoded, or just is size returned.

-     * @return The decoded bitmap, or null if the image data could not be

-     *         decoded, or, if opts is non-null, if opts requested only the

-     *         size be returned (in opts.outWidth and opts.outHeight)

-     */

-    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {

-        try {

-            return BitmapFactory.decodeStream(is, outPadding, opts);

-        } catch (OutOfMemoryError oome) {

-            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);

-            return null;

-        }

-    }

-

-    /**

-     * Gets the image bounds

-     *

-     * @param resolver The ContentResolver

-     * @param uri The uri

-     *

-     * @return The image bounds

-     */

-    private static Point getImageBounds(ContentResolver resolver, Uri uri)

-            throws IOException {

-        final BitmapFactory.Options opts = new BitmapFactory.Options();

-        InputStream inputStream = null;

-

-        try {

-            opts.inJustDecodeBounds = true;

-            inputStream = resolver.openInputStream(uri);

-            decodeStream(inputStream, null, opts);

-

-            return new Point(opts.outWidth, opts.outHeight);

-        } finally {

-            try {

-                if (inputStream != null) {

-                    inputStream.close();

-                }

-            } catch (IOException ignore) {

-            }

-        }

-    }

-}

+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+            } catch (IOException ignore) {
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
+     * BitmapFactory.Options)} that returns {@code null} on {@link
+     * OutOfMemoryError}.
+     *
+     * @param is The input stream that holds the raw data to be decoded into a
+     *           bitmap.
+     * @param outPadding If not null, return the padding rect for the bitmap if
+     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
+     *                   no bitmap is returned (null) then padding is
+     *                   unchanged.
+     * @param opts null-ok; Options that control downsampling and whether the
+     *             image should be completely decoded, or just is size returned.
+     * @return The decoded bitmap, or null if the image data could not be
+     *         decoded, or, if opts is non-null, if opts requested only the
+     *         size be returned (in opts.outWidth and opts.outHeight)
+     */
+    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
+        try {
+            return BitmapFactory.decodeStream(is, outPadding, opts);
+        } catch (OutOfMemoryError oome) {
+            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the image bounds
+     *
+     * @param resolver The ContentResolver
+     * @param uri The uri
+     *
+     * @return The image bounds
+     */
+    private static Point getImageBounds(ContentResolver resolver, Uri uri)
+            throws IOException {
+        final BitmapFactory.Options opts = new BitmapFactory.Options();
+        InputStream inputStream = null;
+        String scheme = uri.getScheme();
+        try {
+            opts.inJustDecodeBounds = true;
+            inputStream = openInputStream(resolver, uri);
+            decodeStream(inputStream, null, opts);
+
+            return new Point(opts.outWidth, opts.outHeight);
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+            } catch (IOException ignore) {
+            }
+        }
+    }
+
+    private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws
+            FileNotFoundException {
+        String scheme = uri.getScheme();
+        if("http".equals(scheme) || "https".equals(scheme)) {
+            try {
+                return new URL(uri.toString()).openStream();
+            } catch (MalformedURLException e) {
+                // Fall-back to the previous behaviour, just in case
+                Log.w(TAG, "Could not convert the uri to url: " + uri.toString());
+                return resolver.openInputStream(uri);
+            } catch (IOException e) {
+                Log.w(TAG, "Could not open input stream for uri: " + uri.toString());
+                return null;
+            }
+        }
+        return resolver.openInputStream(uri);
+    }
+}
+
diff --git a/photoviewer/src/com/android/ex/photo/views/PhotoView.java b/photoviewer/src/com/android/ex/photo/views/PhotoView.java
index 8575bb1..838a60d 100644
--- a/photoviewer/src/com/android/ex/photo/views/PhotoView.java
+++ b/photoviewer/src/com/android/ex/photo/views/PhotoView.java
@@ -108,6 +108,8 @@
     private Rect mCropRect = new Rect();
     /** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */
     private int mCropSize;
+    /** The maximum amount of scaling to apply to images */
+    private float mMaxInitialScaleFactor;
 
     /** Gesture detector */
     private GestureDetectorCompat mGestureDetector;
@@ -699,9 +701,13 @@
             } else {
                 mTempDst.set(0, 0, vwidth, vheight);
             }
-
-            if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
-                mMatrix.setTranslate(vwidth / 2 - dwidth / 2, vheight / 2 - dheight / 2);
+            RectF scaledDestination = new RectF(
+                    (vwidth / 2) - (dwidth * mMaxInitialScaleFactor / 2),
+                    (vheight / 2) - (dheight * mMaxInitialScaleFactor / 2),
+                    (vwidth / 2) + (dwidth * mMaxInitialScaleFactor / 2),
+                    (vheight / 2) + (dheight * mMaxInitialScaleFactor / 2));
+            if(mTempDst.contains(scaledDestination)) {
+                mMatrix.setRectToRect(mTempSrc, scaledDestination, Matrix.ScaleToFit.CENTER);
             } else {
                 mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
             }
@@ -1285,4 +1291,8 @@
             mHeader.post(this);
         }
     }
+
+    public void setMaxInitialScale(float f) {
+        mMaxInitialScaleFactor = f;
+    }
 }