Merge "Make sure we set the QSB bar on the SearchDropTargetBar" into ub-launcher3-burnaby
diff --git a/res/layout/user_folder_scroll.xml b/res/layout/user_folder_scroll.xml
index 421e426..12e5097 100644
--- a/res/layout/user_folder_scroll.xml
+++ b/res/layout/user_folder_scroll.xml
@@ -45,7 +45,9 @@
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:orientation="horizontal" >
+        android:orientation="horizontal"
+        android:paddingStart="12dp"
+        android:paddingEnd="8dp" >
 
         <com.android.launcher3.FolderEditText
             android:id="@+id/folder_name"
@@ -55,7 +57,7 @@
             android:layout_weight="1"
             android:background="#00000000"
             android:fontFamily="sans-serif-condensed"
-            android:gravity="center_horizontal"
+            android:gravity="start"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
             android:paddingBottom="@dimen/folder_name_padding"
@@ -71,8 +73,34 @@
             android:id="@+id/folder_page_indicator"
             android:layout_width="wrap_content"
             android:layout_height="12dp"
-            android:layout_gravity="center_vertical"
+            android:layout_gravity="top"
+            android:layout_marginTop="5dp"
             layout="@layout/page_indicator" />
+
+        <LinearLayout
+            android:id="@+id/folder_sort"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:layout_gravity="center_vertical"
+            android:gravity="end|center_vertical" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="8dp"
+                android:text="@string/sort_alphabetical"
+                android:textColor="#ff777777"
+                android:textSize="14sp" />
+
+            <Switch
+                android:id="@+id/folder_sort_switch"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:clickable="false"
+                android:duplicateParentState="true"
+                android:focusable="false" />
+        </LinearLayout>
     </LinearLayout>
 
 </com.android.launcher3.Folder>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a1e4601..0b34d00 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -89,6 +89,8 @@
     <string name="rename_action">OK</string>
     <!-- Buttons in Rename folder dialog box -->
     <string name="cancel_action">Cancel</string>
+    <!-- Label for button to sort folder contents. [CHAR_LIMIT=10] -->
+    <string name="sort_alphabetical">A-Z</string>
 
     <!-- Shortcuts -->
     <skip />
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index 5ed7a62..dd646bb 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -148,7 +148,7 @@
                 if (applicationInfo == null) {
                     add(new AppInfo(context, info, user, mIconCache));
                 } else {
-                    mIconCache.getTitleAndIcon(applicationInfo, info);
+                    mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
                     modified.add(applicationInfo);
                 }
             }
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index 455c6d1..a1391b2 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -46,6 +46,11 @@
     Bitmap iconBitmap;
 
     /**
+     * Indicates whether we're using a low res icon
+     */
+    boolean usingLowResIcon;
+
+    /**
      * The time at which the app was first installed.
      */
     long firstInstallTime;
@@ -79,7 +84,7 @@
 
         flags = initFlags(info);
         firstInstallTime = info.getFirstInstallTime();
-        iconCache.getTitleAndIcon(this, info);
+        iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */);
         intent = makeLaunchIntent(context, info, user);
         this.user = user;
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 8ef234b..50549ca 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -34,6 +34,8 @@
 import android.view.ViewConfiguration;
 import android.widget.TextView;
 
+import com.android.launcher3.IconCache.IconLoadRequest;
+
 /**
  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
  * because we want to make the bubble taller than the text and TextView's clip is
@@ -74,6 +76,8 @@
     private boolean mStayPressed;
     private boolean mIgnorePressedStateChange;
 
+    private IconLoadRequest mIconLoadRequest;
+
     public BubbleTextView(Context context) {
         this(context, null, 0);
     }
@@ -163,6 +167,9 @@
         }
         // We don't need to check the info since it's not a ShortcutInfo
         super.setTag(info);
+
+        // Verify high res immediately
+        verifyHighRes();
     }
 
     @Override
@@ -450,4 +457,42 @@
         }
         return icon;
     }
+
+    /**
+     * Applies the item info if it is same as what the view is pointing to currently.
+     */
+    public void reapplyItemInfo(final ItemInfo info) {
+        if (getTag() == info) {
+            mIconLoadRequest = null;
+            if (info instanceof AppInfo) {
+                applyFromApplicationInfo((AppInfo) info);
+            } else if (info instanceof ShortcutInfo) {
+                applyFromShortcutInfo((ShortcutInfo) info,
+                        LauncherAppState.getInstance().getIconCache(), false);
+            }
+        }
+    }
+
+    /**
+     * Verifies that the current icon is high-res otherwise posts a request to load the icon.
+     */
+    public void verifyHighRes() {
+        if (mIconLoadRequest != null) {
+            mIconLoadRequest.cancel();
+            mIconLoadRequest = null;
+        }
+        if (getTag() instanceof AppInfo) {
+            AppInfo info = (AppInfo) getTag();
+            if (info.usingLowResIcon) {
+                mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
+                        .updateIconInBackground(BubbleTextView.this, info);
+            }
+        } else if (getTag() instanceof ShortcutInfo) {
+            ShortcutInfo info = (ShortcutInfo) getTag();
+            if (info.usingLowResIcon) {
+                mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
+                        .updateIconInBackground(BubbleTextView.this, info);
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index ddd3002..bc9ef76 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -81,6 +81,7 @@
     boolean isTablet;
     boolean isLargeTablet;
     boolean isLayoutRtl;
+
     boolean transposeLayoutWithOrientation;
 
     int desiredWorkspaceLeftRightMarginPx;
@@ -699,6 +700,10 @@
         return isLargeTablet;
     }
 
+    /**
+     * When {@code true}, hotseat is on the bottom row when in landscape mode.
+     * If {@code false}, hotseat is on the right column when in landscape mode.
+     */
     boolean isVerticalBarLayout() {
         return isLandscape && transposeLayoutWithOrientation;
     }
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index b090a7c..fc68952 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -207,12 +207,13 @@
         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
             return consume;
         }
-        int orientation = v.getResources().getConfiguration().orientation;
 
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile profile = app.getDynamicGrid().getDeviceProfile();
         if (DEBUG) {
             Log.v(TAG, String.format(
-                    "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, orientation=%d",
-                    KeyEvent.keyCodeToString(keyCode), orientation));
+                    "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
+                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
         }
 
         // Initialize the variables.
@@ -226,6 +227,8 @@
         int countX = -1;
         int countY = -1;
         int iconIndex = findIndexOfView(hotseatParent, v);
+        int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
+                .getChildAt(iconIndex).getLayoutParams()).cellX;
 
         final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
         final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
@@ -234,23 +237,25 @@
         int[][] matrix = null;
 
         if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
-                orientation == Configuration.ORIENTATION_PORTRAIT) {
-            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation,
-                    hotseat.getAllAppsButtonRank(), true /* include all apps icon */);
+                !profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
+                    true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
+                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
             iconIndex += iconParent.getChildCount();
             countX = iconLayout.getCountX();
             countY = iconLayout.getCountY() + hotseatLayout.getCountY();
             parent = iconParent;
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
-                orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation,
-                    hotseat.getAllAppsButtonRank(), true /* include all apps icon */);
+                profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
+                    false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
+                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
             iconIndex += iconParent.getChildCount();
             countX = iconLayout.getCountX() + hotseatLayout.getCountX();
             countY = iconLayout.getCountY();
             parent = iconParent;
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
-                orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                profile.isVerticalBarLayout()) {
             keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
         }else {
             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
@@ -296,10 +301,13 @@
         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
             return consume;
         }
-        int orientation = v.getResources().getConfiguration().orientation;
+
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile profile = app.getDynamicGrid().getDeviceProfile();
+
         if (DEBUG) {
-            Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] orientation=%d",
-                    KeyEvent.keyCodeToString(keyCode), orientation));
+            Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
+                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
         }
 
         // Initialize the variables.
@@ -322,14 +330,13 @@
         // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
         // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
         // with the hotseat.
-        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN &&
-                orientation == Configuration.ORIENTATION_PORTRAIT) {
-            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation,
+        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
                     hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
             countY = countY + 1;
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
-                orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation,
+                profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
                     hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
             countX = countX + 1;
         } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index 7ff60de..5d8a865 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -86,6 +86,12 @@
     public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
 
     /**
+     * Time in milliseconds for which an icon sticks to the target position
+     * in case of a sorted folder.
+     */
+    private static final int SORTED_STICKY_REORDER_DELAY = 1500;
+
+    /**
      * Fraction of icon width which behave as scroll region.
      */
     private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
@@ -423,8 +429,11 @@
         if (!(getParent() instanceof DragLayer)) return;
 
         if (ALLOW_FOLDER_SCROLL) {
-            // Always open on the first page.
-            mPagedView.snapToPageImmediately(0);
+            mPagedView.completePendingPageChanges();
+            if (!(mDragInProgress && mPagedView.mIsSorted)) {
+                // Open on the first page.
+                mPagedView.snapToPageImmediately(0);
+            }
         }
 
         Animator openFolderAnim = null;
@@ -527,13 +536,24 @@
         if (mDragController.isDragging()) {
             mDragController.forceTouchMove();
         }
+
+        if (ALLOW_FOLDER_SCROLL) {
+            FolderPagedView pages = (FolderPagedView) mContent;
+            pages.verifyVisibleHighResIcons(pages.getNextPage());
+        }
     }
 
     public void beginExternalDrag(ShortcutInfo item) {
         mCurrentDragInfo = item;
-        mEmptyCellRank = mContent.allocateNewLastItemRank();
+        mEmptyCellRank = mContent.allocateRankForNewItem(item);
         mIsExternalDrag = true;
         mDragInProgress = true;
+        if (ALLOW_FOLDER_SCROLL && mPagedView.mIsSorted) {
+            mScrollPauseAlarm.setOnAlarmListener(null);
+            mScrollPauseAlarm.cancelAlarm();
+            mScrollPauseAlarm.setAlarm(SORTED_STICKY_REORDER_DELAY);
+        }
+
     }
 
     private void sendCustomAccessibilityEvent(int type, String text) {
@@ -747,6 +767,7 @@
                 if (!successfulDrop) {
                     mSuppressFolderDeletion = true;
                 }
+                mScrollPauseAlarm.cancelAlarm();
                 completeDragExit();
             }
         }
@@ -1151,11 +1172,11 @@
     }
 
     public void onAdd(ShortcutInfo item) {
-        mItemsInvalidated = true;
         // If the item was dropped onto this open folder, we have done the work associated
         // with adding the item to the folder, as indicated by mSuppressOnAdd being set
         if (mSuppressOnAdd) return;
-        mContent.createAndAddViewForRank(item, mContent.allocateNewLastItemRank());
+        mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item));
+        mItemsInvalidated = true;
         LauncherModel.addOrMoveItemInDatabase(
                 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
     }
@@ -1304,10 +1325,10 @@
         ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> children);
 
         /**
-         * Create space for a new item at the end, and returns the rank for that item.
+         * Create space for a new item, and returns the rank for that item.
          * Resizes the content if necessary.
          */
-        int allocateNewLastItemRank();
+        int allocateRankForNewItem(ShortcutInfo info);
 
         View createAndAddViewForRank(ShortcutInfo item, int rank);
 
diff --git a/src/com/android/launcher3/FolderCellLayout.java b/src/com/android/launcher3/FolderCellLayout.java
index 1566912..8585add 100644
--- a/src/com/android/launcher3/FolderCellLayout.java
+++ b/src/com/android/launcher3/FolderCellLayout.java
@@ -133,7 +133,7 @@
     }
 
     @Override
-    public int allocateNewLastItemRank() {
+    public int allocateRankForNewItem(ShortcutInfo info) {
         int rank = getItemCount();
         mFolder.rearrangeChildren(rank + 1);
         return rank;
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index a3e8295..dbfedaa 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -58,7 +58,7 @@
     private CheckLongPressHelper mLongPressHelper;
 
     // The number of icons to display in the
-    private static final int NUM_ITEMS_IN_PREVIEW = 3;
+    public static final int NUM_ITEMS_IN_PREVIEW = 3;
     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
     private static final int DROP_IN_ANIMATION_DURATION = 400;
     private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index 85a792f..3240cbf 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -29,11 +29,20 @@
  */
 public class FolderInfo extends ItemInfo {
 
+    public static final int NO_FLAGS = 0x00000000;
+
+    /**
+     * The folder is locked in sorted mode
+     */
+    public static final int FLAG_ITEMS_SORTED = 0x00000001;
+
     /**
      * Whether this folder has been opened
      */
     boolean opened;
 
+    public int options;
+
     /**
      * The apps and shortcuts
      */
@@ -83,6 +92,8 @@
     void onAddToDatabase(Context context, ContentValues values) {
         super.onAddToDatabase(context, values);
         values.put(LauncherSettings.Favorites.TITLE, title.toString());
+        values.put(LauncherSettings.Favorites.OPTIONS, options);
+
     }
 
     void addListener(FolderListener listener) {
@@ -121,4 +132,25 @@
                 + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX
                 + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")";
     }
+
+    public boolean hasOption(int optionFlag) {
+        return (options & optionFlag) != 0;
+    }
+
+    /**
+     * @param option flag to set or clear
+     * @param isEnabled whether to set or clear the flag
+     * @param context if not null, save changes to the db.
+     */
+    public void setOption(int option, boolean isEnabled, Context context) {
+        int oldOptions = options;
+        if (isEnabled) {
+            options |= option;
+        } else {
+            options &= ~option;
+        }
+        if (context != null && oldOptions != options) {
+            LauncherModel.updateItemInDatabase(context, this);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java
index b4a7a75..21158b4 100644
--- a/src/com/android/launcher3/FolderPagedView.java
+++ b/src/com/android/launcher3/FolderPagedView.java
@@ -20,15 +20,22 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.Switch;
 
 import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
 import com.android.launcher3.PageIndicator.PageMarkerResources;
 import com.android.launcher3.Workspace.ItemOperator;
 
+import java.text.Collator;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -40,14 +47,19 @@
     private static final int REORDER_ANIMATION_DURATION = 230;
     private static final int START_VIEW_REORDER_DELAY = 30;
     private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
+
+    private static final int SPAN_TO_PAGE_DURATION = 350;
+    private static final int SORT_ANIM_HIDE_DURATION = 130;
+    private static final int SORT_ANIM_SHOW_DURATION = 160;
+
     private static final int[] sTempPosArray = new int[2];
 
     // TODO: Remove this restriction
-    private static final int MAX_ITEMS_PER_PAGE = 3;
+    private static final int MAX_ITEMS_PER_PAGE = 4;
 
     private final LayoutInflater mInflater;
     private final IconCache mIconCache;
-    private final HashMap<View, Runnable> mPageChangingViews = new HashMap<>();
+    private final HashMap<View, Runnable> mPendingAnimations = new HashMap<>();
 
     private final int mMaxCountX;
     private final int mMaxCountY;
@@ -61,6 +73,13 @@
     private FocusIndicatorView mFocusIndicatorView;
     private PagedFolderKeyEventListener mKeyListener;
 
+    private View mSortButton;
+    private Switch mSortSwitch;
+    private View mPageIndicator;
+
+    private boolean mSortOperationPending;
+    boolean mIsSorted;
+
     public FolderPagedView(Context context, AttributeSet attrs) {
         super(context, attrs);
         LauncherAppState app = LauncherAppState.getInstance();
@@ -80,6 +99,134 @@
         mFolder = folder;
         mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
         mKeyListener = new PagedFolderKeyEventListener(folder);
+
+        mSortButton = folder.findViewById(R.id.folder_sort);
+        mSortButton.setOnClickListener(new OnClickListener() {
+
+            @Override
+            public void onClick(View v) {
+                onSortClicked();
+            }
+        });
+        mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
+        mSortSwitch = (Switch) folder.findViewById(R.id.folder_sort_switch);
+    }
+
+    private void onSortClicked() {
+        if (mSortOperationPending) {
+            return;
+        }
+        if (mIsSorted) {
+            setIsSorted(false, true);
+        } else {
+            mSortOperationPending = true;
+            doSort();
+        }
+    }
+
+    private void setIsSorted(boolean isSorted, boolean saveChanges) {
+        mIsSorted = isSorted;
+        mSortSwitch.setChecked(isSorted);
+        mFolder.mInfo.setOption(FolderInfo.FLAG_ITEMS_SORTED, isSorted,
+                saveChanges ? mFolder.mLauncher : null);
+    }
+
+    /**
+     * Sorts the contents of the folder and animates the icons on the first page to reflect
+     * the changes.
+     * Steps:
+     *      1. Scroll to first page
+     *      2. Sort all icons in one go
+     *      3. Re-apply the old IconInfos on the first page (so that there is no instant change)
+     *      4. Animate each view individually to reflect the new icon.
+     */
+    private void doSort() {
+        if (!mSortOperationPending) {
+            return;
+        }
+        if (getNextPage() != 0) {
+            snapToPage(0, SPAN_TO_PAGE_DURATION, new DecelerateInterpolator());
+            return;
+        }
+
+        mSortOperationPending = false;
+        ShortcutInfo[][] oldItems = new ShortcutInfo[mGridCountX][mGridCountY];
+        CellLayout currentPage = getCurrentCellLayout();
+        for (int x = 0; x < mGridCountX; x++) {
+            for (int y = 0; y < mGridCountY; y++) {
+                View v = currentPage.getChildAt(x, y);
+                if (v != null) {
+                    oldItems[x][y] = (ShortcutInfo) v.getTag();
+                }
+            }
+        }
+
+        ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder());
+        Collections.sort(views, new ViewComparator());
+        arrangeChildren(views, views.size());
+
+        int delay = 0;
+        float delayAmount = START_VIEW_REORDER_DELAY;
+        final Interpolator hideInterpolator = new DecelerateInterpolator(2);
+        final Interpolator showInterpolator = new OvershootInterpolator(0.8f);
+
+        currentPage = getCurrentCellLayout();
+        for (int x = 0; x < mGridCountX; x++) {
+            for (int y = 0; y < mGridCountY; y++) {
+                final BubbleTextView v = (BubbleTextView) currentPage.getChildAt(x, y);
+                if (v != null) {
+                    final ShortcutInfo info = (ShortcutInfo) v.getTag();
+                    final Runnable clearPending = new Runnable() {
+
+                        @Override
+                        public void run() {
+                            mPendingAnimations.remove(v);
+                            v.setScaleX(1);
+                            v.setScaleY(1);
+                        }
+                    };
+                    if (oldItems[x][y] == null) {
+                        v.setScaleX(0);
+                        v.setScaleY(0);
+                        v.animate().setDuration(SORT_ANIM_SHOW_DURATION)
+                            .setStartDelay(SORT_ANIM_HIDE_DURATION + delay)
+                            .scaleX(1).scaleY(1).setInterpolator(showInterpolator)
+                            .withEndAction(clearPending);
+                        mPendingAnimations.put(v, clearPending);
+                    } else {
+                        // Apply the old iconInfo so that there is no sudden change.
+                        v.applyFromShortcutInfo(oldItems[x][y], mIconCache, false);
+                        v.animate().setStartDelay(delay).setDuration(SORT_ANIM_HIDE_DURATION)
+                            .scaleX(0).scaleY(0)
+                            .setInterpolator(hideInterpolator)
+                            .withEndAction(new Runnable() {
+
+                                @Override
+                                public void run() {
+                                    // Apply the new iconInfo as part of the animation.
+                                    v.applyFromShortcutInfo(info, mIconCache, false);
+                                    v.animate().scaleX(1).scaleY(1)
+                                        .setDuration(SORT_ANIM_SHOW_DURATION).setStartDelay(0)
+                                        .setInterpolator(showInterpolator)
+                                        .withEndAction(clearPending);
+                                }
+                       });
+                       mPendingAnimations.put(v, new Runnable() {
+
+                           @Override
+                           public void run() {
+                               clearPending.run();
+                               v.applyFromShortcutInfo(info, mIconCache, false);
+                           }
+                        });
+                    }
+                    delay += delayAmount;
+                    delayAmount *= VIEW_REORDER_DELAY_FACTOR;
+                }
+            }
+        }
+
+        setIsSorted(true, true);
     }
 
     /**
@@ -125,6 +272,7 @@
 
     @Override
     public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
+        mIsSorted = mFolder.mInfo.hasOption(FolderInfo.FLAG_ITEMS_SORTED);
         ArrayList<View> icons = new ArrayList<View>();
         for (ShortcutInfo item : items) {
             icons.add(createNewView(item));
@@ -138,20 +286,33 @@
      * Also sets the current page to the last page.
      */
     @Override
-    public int allocateNewLastItemRank() {
+    public int allocateRankForNewItem(ShortcutInfo info) {
         int rank = getItemCount();
-        int total = rank + 1;
-        // Rearrange the items as the grid size might change.
-        mFolder.rearrangeChildren(total);
+        ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder());
+        if (mIsSorted) {
+            View tmp = new View(getContext());
+            tmp.setTag(info);
+            int index = Collections.binarySearch(views, tmp, new ViewComparator());
+            if (index < 0) {
+                rank = -index - 1;
+            } else {
+                // Item with same name already exists.
+                // We will just insert it before that item.
+                rank = index;
+            }
 
-        setCurrentPage(getChildCount() - 1);
+        }
+
+        views.add(rank, null);
+        arrangeChildren(views, views.size(), false);
+        setCurrentPage(rank / mMaxItemsPerPage);
         return rank;
     }
 
     @Override
     public View createAndAddViewForRank(ShortcutInfo item, int rank) {
         View icon = createNewView(item);
-        addViewForRank(createNewView(item), item, rank);
+        addViewForRank(icon, item, rank);
         return icon;
     }
 
@@ -259,6 +420,10 @@
         int position = 0;
         int newX, newY, rank;
 
+        boolean isSorted = mIsSorted;
+
+        ViewComparator comparator = new ViewComparator();
+        View lastView = null;
         rank = 0;
         for (int i = 0; i < itemCount; i++) {
             View v = list.size() > i ? list.get(i) : null;
@@ -273,6 +438,10 @@
             }
 
             if (v != null) {
+                if (lastView != null) {
+                    isSorted &= comparator.compare(lastView, v) <= 0;
+                }
+
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
                 newX = position % mGridCountX;
                 newY = position / mGridCountX;
@@ -292,6 +461,7 @@
                         v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
             }
 
+            lastView = v;
             rank ++;
             position++;
         }
@@ -305,6 +475,19 @@
         if (removed) {
             setCurrentPage(0);
         }
+
+        setIsSorted(isSorted, saveChanges);
+
+        // Update footer
+        if (getPageCount() > 1) {
+            mPageIndicator.setVisibility(View.VISIBLE);
+            mSortButton.setVisibility(View.VISIBLE);
+            mFolder.mFolderName.setGravity(Gravity.START);
+        } else {
+            mPageIndicator.setVisibility(View.GONE);
+            mSortButton.setVisibility(View.GONE);
+            mFolder.mFolderName.setGravity(Gravity.CENTER_HORIZONTAL);
+        }
     }
 
     @Override
@@ -407,6 +590,17 @@
         if (mFolder != null) {
             mFolder.updateTextViewFocus();
         }
+        if (mSortOperationPending && getNextPage() == 0) {
+            post(new Runnable() {
+
+                @Override
+                public void run() {
+                    if (mSortOperationPending) {
+                        doSort();
+                    }
+                }
+            });
+        }
     }
 
     /**
@@ -433,8 +627,8 @@
      * Finish animation all the views which are animating across pages
      */
     public void completePendingPageChanges() {
-        if (!mPageChangingViews.isEmpty()) {
-            HashMap<View, Runnable> pendingViews = new HashMap<>(mPageChangingViews);
+        if (!mPendingAnimations.isEmpty()) {
+            HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations);
             for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
                 e.getKey().animate().cancel();
                 e.getValue().run();
@@ -448,6 +642,28 @@
     }
 
     @Override
+    protected void onPageBeginMoving() {
+        super.onPageBeginMoving();
+        getVisiblePages(sTempPosArray);
+        for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) {
+            verifyVisibleHighResIcons(i);
+        }
+    }
+
+    /**
+     * Ensures that all the icons on the given page are of high-res
+     */
+    public void verifyVisibleHighResIcons(int pageNo) {
+        CellLayout page = getPageAt(pageNo);
+        if (page != null) {
+            ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
+            for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+                ((BubbleTextView) parent.getChildAt(i)).verifyHighRes();
+            }
+        }
+    }
+
+    @Override
     public void realTimeReorder(int empty, int target) {
         completePendingPageChanges();
         int delay = 0;
@@ -533,7 +749,7 @@
 
                         @Override
                         public void run() {
-                            mPageChangingViews.remove(v);
+                            mPendingAnimations.remove(v);
                             v.setTranslationX(oldTranslateX);
                             ((CellLayout) v.getParent().getParent()).removeView(v);
                             addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
@@ -544,7 +760,7 @@
                         .setDuration(REORDER_ANIMATION_DURATION)
                         .setStartDelay(0)
                         .withEndAction(endAction);
-                    mPageChangingViews.put(v, endAction);
+                    mPendingAnimations.put(v, endAction);
                 }
             }
             moveStart = rankToMove;
@@ -569,4 +785,14 @@
             }
         }
     }
+
+    private static class ViewComparator implements Comparator<View> {
+        private final Collator mCollator = Collator.getInstance();
+
+        @Override
+        public int compare(View lhs, View rhs) {
+            return mCollator.compare( ((ShortcutInfo) lhs.getTag()).title.toString(),
+                    ((ShortcutInfo) rhs.getTag()).title.toString());
+        }
+    }
 }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 43f838e..39a80be 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -31,8 +31,10 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -62,14 +64,18 @@
 
     private static final boolean DEBUG = false;
 
+    private static final int LOW_RES_SCALE_FACTOR = 8;
+
     private static class CacheEntry {
         public Bitmap icon;
         public CharSequence title;
         public CharSequence contentDescription;
+        public boolean isLowResIcon;
     }
 
-    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
-            new HashMap<UserHandleCompat, Bitmap>();
+    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>();
+    private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
+
     private final Context mContext;
     private final PackageManager mPackageManager;
     private final UserManagerCompat mUserManager;
@@ -79,6 +85,8 @@
     private final int mIconDpi;
     private final IconDB mIconDb;
 
+    private final Handler mWorkerHandler;
+
     public IconCache(Context context) {
         ActivityManager activityManager =
                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
@@ -89,6 +97,8 @@
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
         mIconDpi = activityManager.getLauncherLargeIconDensity();
         mIconDb = new IconDB(context);
+
+        mWorkerHandler = new Handler(LauncherModel.sWorkerThread.getLooper());
     }
 
     private Drawable getFullResDefaultActivityIcon() {
@@ -306,10 +316,7 @@
         entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
         mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry);
 
-        ContentValues values = new ContentValues();
-        values.put(IconDB.COLUMN_ICON, ItemInfo.flattenBitmap(entry.icon));
-        values.put(IconDB.COLUMN_LABEL, entry.title.toString());
-        return values;
+        return mIconDb.newContentValues(entry.icon, entry.title.toString());
     }
 
 
@@ -335,16 +342,52 @@
     }
 
     /**
+     * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
+     * @return a request ID that can be used to cancel the request.
+     */
+    public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) {
+        Runnable request = new Runnable() {
+
+            @Override
+            public void run() {
+                if (info instanceof AppInfo) {
+                    getTitleAndIcon((AppInfo) info, null, false);
+                } else if (info instanceof ShortcutInfo) {
+                    ShortcutInfo st = (ShortcutInfo) info;
+                    getTitleAndIcon(st,
+                            st.promisedIntent != null ? st.promisedIntent : st.intent,
+                            st.user, false);
+                }
+                mMainThreadExecutor.execute(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        caller.reapplyItemInfo(info);
+                    }
+                });
+            }
+        };
+        mWorkerHandler.post(request);
+        return new IconLoadRequest(request, mWorkerHandler);
+    }
+
+    /**
      * Fill in "application" with the icon and label for "info."
      */
-    public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info) {
-        CacheEntry entry = cacheLocked(application.componentName, info, info.getUser(), false);
-
+    public synchronized void getTitleAndIcon(AppInfo application,
+            LauncherActivityInfoCompat info, boolean useLowResIcon) {
+        CacheEntry entry = cacheLocked(application.componentName, info,
+                info == null ? application.user : info.getUser(),
+                false, useLowResIcon);
         application.title = entry.title;
         application.iconBitmap = entry.icon;
         application.contentDescription = entry.contentDescription;
+        application.usingLowResIcon = entry.isLowResIcon;
     }
 
+    /**
+     * Returns a high res icon for the given intent and user
+     */
     public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
@@ -354,7 +397,7 @@
         }
 
         LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user);
-        CacheEntry entry = cacheLocked(component, launcherActInfo, user, true);
+        CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, true);
         return entry.icon;
     }
 
@@ -363,7 +406,7 @@
      * corresponding activity is not found, it reverts to the package icon.
      */
     public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent,
-            UserHandleCompat user) {
+            UserHandleCompat user, boolean useLowResIcon) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
@@ -371,9 +414,10 @@
             shortcutInfo.setIcon(getDefaultIcon(user));
             shortcutInfo.title = "";
             shortcutInfo.usingFallbackIcon = true;
+            shortcutInfo.usingLowResIcon = false;
         } else {
             LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user);
-            getTitleAndIcon(shortcutInfo, component, info, user, true);
+            getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon);
         }
     }
 
@@ -382,11 +426,12 @@
      */
     public synchronized void getTitleAndIcon(
             ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info,
-            UserHandleCompat user, boolean usePkgIcon) {
-        CacheEntry entry = cacheLocked(component, info, user, usePkgIcon);
+            UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) {
+        CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon);
         shortcutInfo.setIcon(entry.icon);
         shortcutInfo.title = entry.title;
         shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
+        shortcutInfo.usingLowResIcon = entry.isLowResIcon;
     }
 
     public synchronized Bitmap getDefaultIcon(UserHandleCompat user) {
@@ -405,15 +450,15 @@
      * This method is not thread safe, it must be called from a synchronized method.
      */
     private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
-            UserHandleCompat user, boolean usePackageIcon) {
+            UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null) {
+        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
             entry = new CacheEntry();
             mCache.put(cacheKey, entry);
 
             // Check the DB first.
-            if (!getEntryFromDB(componentName, user, entry)) {
+            if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) {
                 if (info != null) {
                     entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext);
                 } else {
@@ -509,25 +554,26 @@
             // pass
         }
 
-        ContentValues values = new ContentValues();
+        ContentValues values = mIconDb.newContentValues(icon, label);
         values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString());
         values.put(IconDB.COLUMN_USER, userSerial);
-        values.put(IconDB.COLUMN_ICON, ItemInfo.flattenBitmap(icon));
-        values.put(IconDB.COLUMN_LABEL, label);
         mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
                 SQLiteDatabase.CONFLICT_REPLACE);
     }
 
-    private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, CacheEntry entry) {
+    private boolean getEntryFromDB(ComponentName component, UserHandleCompat user,
+            CacheEntry entry, boolean lowRes) {
         Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
-                new String[] {IconDB.COLUMN_ICON, IconDB.COLUMN_LABEL},
+                new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
+                        IconDB.COLUMN_LABEL},
                 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
                 new String[] {component.flattenToString(),
                     Long.toString(mUserManager.getSerialNumberForUser(user))},
                 null, null, null);
         try {
             if (c.moveToNext()) {
-                entry.icon = Utilities.createIconBitmap(c, 0, mContext);
+                entry.icon = loadIconNoResize(c, 0);
+                entry.isLowResIcon = lowRes;
                 entry.title = c.getString(1);
                 if (entry.title == null) {
                     entry.title = "";
@@ -543,8 +589,22 @@
         return false;
     }
 
+    public static class IconLoadRequest {
+        private final Runnable mRunnable;
+        private final Handler mHandler;
+
+        IconLoadRequest(Runnable runnable, Handler handler) {
+            mRunnable = runnable;
+            mHandler = handler;
+        }
+
+        public void cancel() {
+            mHandler.removeCallbacks(mRunnable);
+        }
+    }
+
     private static final class IconDB extends SQLiteOpenHelper {
-        private final static int DB_VERSION = 1;
+        private final static int DB_VERSION = 2;
 
         private final static String TABLE_NAME = "icons";
         private final static String COLUMN_ROWID = "rowid";
@@ -553,6 +613,7 @@
         private final static String COLUMN_LAST_UPDATED = "lastUpdated";
         private final static String COLUMN_VERSION = "version";
         private final static String COLUMN_ICON = "icon";
+        private final static String COLUMN_ICON_LOW_RES = "icon_low_res";
         private final static String COLUMN_LABEL = "label";
 
         public IconDB(Context context) {
@@ -567,6 +628,7 @@
                     COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
                     COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
                     COLUMN_ICON + " BLOB, " +
+                    COLUMN_ICON_LOW_RES + " BLOB, " +
                     COLUMN_LABEL + " TEXT, " +
                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
                     ");");
@@ -590,5 +652,25 @@
             db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
             onCreate(db);
         }
+
+        public ContentValues newContentValues(Bitmap icon, String label) {
+            ContentValues values = new ContentValues();
+            values.put(IconDB.COLUMN_ICON, ItemInfo.flattenBitmap(icon));
+            values.put(IconDB.COLUMN_ICON_LOW_RES, ItemInfo.flattenBitmap(
+                    Bitmap.createScaledBitmap(icon,
+                            icon.getWidth() / LOW_RES_SCALE_FACTOR,
+                            icon.getHeight() / LOW_RES_SCALE_FACTOR, true)));
+            values.put(IconDB.COLUMN_LABEL, label);
+            return values;
+        }
+    }
+
+    private static Bitmap loadIconNoResize(Cursor c, int iconIndex) {
+        byte[] data = c.getBlob(iconIndex);
+        try {
+            return BitmapFactory.decodeByteArray(data, 0, data.length);
+        } catch (Exception e) {
+            return null;
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 2fd9db2..1e16baf 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -117,7 +117,7 @@
 
     private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings";
 
-    private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
+    static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
     static {
         sWorkerThread.start();
     }
@@ -961,6 +961,7 @@
                 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
                 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
                 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+                final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS);
 
                 FolderInfo folderInfo = null;
                 switch (c.getInt(itemTypeIndex)) {
@@ -975,6 +976,7 @@
                 folderInfo.screenId = c.getInt(screenIndex);
                 folderInfo.cellX = c.getInt(cellXIndex);
                 folderInfo.cellY = c.getInt(cellYIndex);
+                folderInfo.options = c.getInt(optionsIndex);
 
                 return folderInfo;
             }
@@ -1862,9 +1864,8 @@
                             LauncherSettings.Favorites.RESTORED);
                     final int profileIdIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.PROFILE_ID);
-                    //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
-                    //final int displayModeIndex = c.getColumnIndexOrThrow(
-                    //        LauncherSettings.Favorites.DISPLAY_MODE);
+                    final int optionsIndex = c.getColumnIndexOrThrow(
+                            LauncherSettings.Favorites.OPTIONS);
 
                     ShortcutInfo info;
                     String intentDescription;
@@ -2017,10 +2018,14 @@
                                     continue;
                                 }
 
+                                container = c.getInt(containerIndex);
+                                boolean useLowResIcon = container >= 0 &&
+                                        c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
+
                                 if (itemReplaced) {
                                     if (user.equals(UserHandleCompat.myUserHandle())) {
                                         info = getAppShortcutInfo(manager, intent, user, context, null,
-                                                iconIndex, titleIndex, false);
+                                                iconIndex, titleIndex, false, useLowResIcon);
                                     } else {
                                         // Don't replace items for other profiles.
                                         itemsToRemove.add(id);
@@ -2031,7 +2036,8 @@
                                         Launcher.addDumpLog(TAG,
                                                 "constructing info for partially restored package",
                                                 true);
-                                        info = getRestoredItemInfo(c, titleIndex, intent, promiseType);
+                                        info = getRestoredItemInfo(c, titleIndex, intent,
+                                                promiseType, useLowResIcon);
                                         intent = getRestoredItemIntent(c, context, intent);
                                     } else {
                                         // Don't restore items for other profiles.
@@ -2041,7 +2047,7 @@
                                 } else if (itemType ==
                                         LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
                                     info = getAppShortcutInfo(manager, intent, user, context, c,
-                                            iconIndex, titleIndex, allowMissingTarget);
+                                            iconIndex, titleIndex, allowMissingTarget, useLowResIcon);
                                 } else {
                                     info = getShortcutInfo(c, context, iconTypeIndex,
                                             iconPackageIndex, iconResourceIndex, iconIndex,
@@ -2063,7 +2069,6 @@
                                 if (info != null) {
                                     info.id = id;
                                     info.intent = intent;
-                                    container = c.getInt(containerIndex);
                                     info.container = container;
                                     info.screenId = c.getInt(screenIndex);
                                     info.cellX = c.getInt(cellXIndex);
@@ -2114,6 +2119,7 @@
                                 folderInfo.cellY = c.getInt(cellYIndex);
                                 folderInfo.spanX = 1;
                                 folderInfo.spanY = 1;
+                                folderInfo.options = c.getInt(optionsIndex);
 
                                 // check & update map of what's occupied
                                 if (!checkItemPlacement(occupied, folderInfo)) {
@@ -3350,10 +3356,10 @@
      * to a package that is not yet installed on the system.
      */
     public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent,
-            int promiseType) {
+            int promiseType, boolean useLowResIcon) {
         final ShortcutInfo info = new ShortcutInfo();
         info.user = UserHandleCompat.myUserHandle();
-        mIconCache.getTitleAndIcon(info, intent, info.user);
+        mIconCache.getTitleAndIcon(info, intent, info.user, useLowResIcon);
 
         if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) {
             String title = (cursor != null) ? cursor.getString(titleIndex) : null;
@@ -3402,7 +3408,7 @@
      */
     public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent,
             UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex,
-            boolean allowMissingTarget) {
+            boolean allowMissingTarget, boolean useLowResIcon) {
         if (user == null) {
             Log.d(TAG, "Null user found in getShortcutInfo");
             return null;
@@ -3424,7 +3430,7 @@
         }
 
         final ShortcutInfo info = new ShortcutInfo();
-        mIconCache.getTitleAndIcon(info, componentName, lai, user, false);
+        mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon);
         if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) {
             Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context);
             info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon);
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index b7a271e..59c8d92 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -57,7 +57,7 @@
     private static final String TAG = "Launcher.LauncherProvider";
     private static final boolean LOGD = false;
 
-    private static final int DATABASE_VERSION = 22;
+    private static final int DATABASE_VERSION = 23;
 
     static final String OLD_AUTHORITY = "com.android.launcher2.settings";
     static final String AUTHORITY = ProviderConfig.AUTHORITY;
@@ -413,7 +413,8 @@
                     "modified INTEGER NOT NULL DEFAULT 0," +
                     "restored INTEGER NOT NULL DEFAULT 0," +
                     "profileId INTEGER DEFAULT " + userSerialNumber + "," +
-                    "rank INTEGER NOT NULL DEFAULT 0" +
+                    "rank INTEGER NOT NULL DEFAULT 0," +
+                    "options INTEGER NOT NULL DEFAULT 0" +
                     ");");
             addWorkspacesTable(db);
 
@@ -524,18 +525,9 @@
                     }
                 }
                 case 15: {
-                    db.beginTransaction();
-                    try {
-                        // Insert new column for holding restore status
-                        db.execSQL("ALTER TABLE favorites " +
-                                "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
-                        db.setTransactionSuccessful();
-                    } catch (SQLException ex) {
-                        Log.e(TAG, ex.getMessage(), ex);
+                    if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
                         // Old version remains, which means we wipe old data
                         break;
-                    } finally {
-                        db.endTransaction();
                     }
                 }
                 case 16: {
@@ -573,6 +565,12 @@
                         break;
                     }
                 case 22: {
+                    if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
+                        // Old version remains, which means we wipe old data
+                        break;
+                    }
+                }
+                case 23: {
                     // DB Upgraded successfully
                     return;
                 }
@@ -682,20 +680,21 @@
         }
 
         private boolean addProfileColumn(SQLiteDatabase db) {
+            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
+            // Default to the serial number of this user, for older
+            // shortcuts.
+            long userSerialNumber = userManager.getSerialNumberForUser(
+                    UserHandleCompat.myUserHandle());
+            return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber);
+        }
+
+        private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
             db.beginTransaction();
             try {
-                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
-                // Default to the serial number of this user, for older
-                // shortcuts.
-                long userSerialNumber = userManager.getSerialNumberForUser(
-                        UserHandleCompat.myUserHandle());
-                // Insert new column for holding user serial number
-                db.execSQL("ALTER TABLE favorites " +
-                        "ADD COLUMN profileId INTEGER DEFAULT "
-                                        + userSerialNumber + ";");
+                db.execSQL("ALTER TABLE favorites ADD COLUMN "
+                        + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
                 db.setTransactionSuccessful();
             } catch (SQLException ex) {
-                // Old version remains, which means we wipe old data
                 Log.e(TAG, ex.getMessage(), ex);
                 return false;
             } finally {
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 13fd7ee..d161fbb 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -309,5 +309,11 @@
          * <p>Type: INTEGER</p>
          */
         static final String RANK = "rank";
+
+        /**
+         * Stores general flag based options for {@link ItemInfo}s.
+         * <p>Type: INTEGER</p>
+         */
+        static final String OPTIONS = "options";
     }
 }
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index 08ffaa2..9f7da6c 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -84,6 +84,11 @@
     boolean usingFallbackIcon;
 
     /**
+     * Indicates whether we're using a low res icon
+     */
+    boolean usingLowResIcon;
+
+    /**
      * If isShortcut=true and customIcon=false, this contains a reference to the
      * shortcut icon as an application's resource.
      */
@@ -192,7 +197,8 @@
 
     public void updateIcon(IconCache iconCache) {
         if (itemType == Favorites.ITEM_TYPE_APPLICATION) {
-            iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user);
+            iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user,
+                    shouldUseLowResIcon());
         }
     }
 
@@ -264,5 +270,9 @@
         mInstallProgress = progress;
         status |= FLAG_INSTALL_SESSION_ACTIVE;
     }
+
+    public boolean shouldUseLowResIcon() {
+        return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
+    }
 }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index a59e25e..7ebdf3a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -4817,7 +4817,8 @@
                         if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
                             // For auto install apps update the icon as well as label.
                             mIconCache.getTitleAndIcon(shortcutInfo,
-                                    shortcutInfo.promisedIntent, user);
+                                    shortcutInfo.promisedIntent, user,
+                                    shortcutInfo.shouldUseLowResIcon());
                         } else {
                             // Only update the icon for restored apps.
                             shortcutInfo.updateIcon(mIconCache);
diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java
index 0c6bfbf..6e80c2f 100644
--- a/src/com/android/launcher3/util/FocusLogic.java
+++ b/src/com/android/launcher3/util/FocusLogic.java
@@ -184,21 +184,18 @@
      */
     // TODO: get rid of the dynamic matrix creation
     public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout,
-            int orientation, int allappsiconRank, boolean includeAllappsicon) {
+            boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) {
 
         ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
         ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets();
 
         int m, n;
-        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+        if (isHorizontal) {
             m = iconLayout.getCountX();
             n = iconLayout.getCountY() + hotseatLayout.getCountY();
-        } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+        } else {
             m = iconLayout.getCountX() + hotseatLayout.getCountX();
             n = iconLayout.getCountY();
-        } else {
-            throw new IllegalStateException(String.format(
-                    "orientation type=%d is not supported for key board events.", orientation));
         }
         int[][] matrix = createFullMatrix(m, n, false /* set all cell to empty */);
 
@@ -215,7 +212,7 @@
         // is truncated. If it is negative, then all apps icon index is not inserted.
         for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) {
             int delta = 0;
-            if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+            if (isHorizontal) {
                 int cx = ((CellLayout.LayoutParams)
                         hotseatParent.getChildAt(i).getLayoutParams()).cellX;
                 if ((includeAllappsicon && cx >= allappsiconRank) ||
@@ -223,7 +220,7 @@
                         delta = -1;
                 }
                 matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i;
-            } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            } else {
                 int cy = ((CellLayout.LayoutParams)
                         hotseatParent.getChildAt(i).getLayoutParams()).cellY;
                 if ((includeAllappsicon && cy >= allappsiconRank) ||