Merge changes from topic "am-babf7974-d7e9-418f-b563-40e4867a0e88" into ub-launcher3-master

* changes:
  [automerger] Add spring to QSB when opening all apps. am: c90a89d997
  Add spring to QSB when opening all apps.
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 6340619..39df2b1 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -68,11 +68,10 @@
 
     </com.android.launcher3.allapps.AllAppsRecyclerViewContainerView>
     <View
-        style="@style/AllAppsNavBarProtection"
         android:id="@+id/nav_bar_bg"
+        android:background="?attr/allAppsNavBarScrimColor"
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:layout_gravity="bottom"
-        android:focusable="false"
-        android:visibility="invisible" />
+        android:focusable="false"  />
 </com.android.launcher3.allapps.AllAppsContainerView>
\ No newline at end of file
diff --git a/res/values-v26/styles.xml b/res/values-v26/styles.xml
index fd6fc4d..b25f46a 100644
--- a/res/values-v26/styles.xml
+++ b/res/values-v26/styles.xml
@@ -26,10 +26,4 @@
         <item name="android:colorPrimaryDark">#616161</item> <!-- Gray 700 -->
     </style>
 
-    <!-- From O and above, we show a dark nav bar in all-apps -->
-    <style name="AllAppsNavBarProtection">
-        <item name="android:alpha">0.6</item>
-        <item name="android:background">?android:attr/colorPrimary</item>
-    </style>
-
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 7b52dae..68b628f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -20,6 +20,7 @@
 
     <!-- Attributes used for launcher theme -->
     <attr name="allAppsScrimColor" format="color" />
+    <attr name="allAppsNavBarScrimColor" format="color" />
     <attr name="popupColorPrimary" format="color" />
     <attr name="popupColorSecondary" format="color" />
     <attr name="popupColorTertiary" format="color" />
diff --git a/res/values/config.xml b/res/values/config.xml
index fb0a82e..b41172b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -9,7 +9,7 @@
     <bool name="is_large_tablet">false</bool>
     <bool name="allow_rotation">false</bool>
 
-    <integer name="extracted_color_gradient_alpha">191</integer>
+    <integer name="extracted_color_gradient_alpha">153</integer>
 
     <!-- A string pointer to the original app name string. This allows derived projects to
      easily override the app name without providing all translations -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 5bdf512..5d06705 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -29,7 +29,8 @@
     </style>
 
     <style name="BaseLauncherThemeWithCustomAttrs" parent="@style/BaseLauncherTheme">
-        <item name="allAppsScrimColor">#DDFFFFFF</item>
+        <item name="allAppsScrimColor">#CCFFFFFF</item>
+        <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">#FFF</item>
         <item name="popupColorSecondary">#F5F5F5</item> <!-- Gray 100 -->
         <item name="popupColorTertiary">#E0E0E0</item> <!-- Gray 300 -->
@@ -61,7 +62,8 @@
         <item name="android:textColorHint">#A0FFFFFF</item>
         <item name="android:colorControlHighlight">#A0FFFFFF</item>
         <item name="android:colorPrimary">#FF333333</item>
-        <item name="allAppsScrimColor">#33000000</item>
+        <item name="allAppsScrimColor">#7A000000</item>
+        <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">?android:attr/colorPrimary</item>
         <item name="popupColorSecondary">#424242</item> <!-- Gray 800 -->
         <item name="popupColorTertiary">#757575</item> <!-- Gray 600 -->
@@ -103,12 +105,6 @@
         <item name="android:includeFontPadding">false</item>
     </style>
 
-    <!-- Style for nav bar background in all-apps screen -->
-    <style name="AllAppsNavBarProtection">
-        <item name="android:alpha">?android:attr/spotShadowAlpha</item>
-        <item name="android:background">@color/default_shadow_color_no_alpha</item>
-    </style>
-
     <!-- Base theme for BubbleTextView and sub classes -->
     <style name="BaseIcon">
         <item name="android:layout_width">match_parent</item>
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 1a405f9..a486a3a 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -55,7 +55,6 @@
 
     private final int[] mDirectionVector = new int[2];
     private final int[] mLastDirectionVector = new int[2];
-    private final int[] mTmpPt = new int[2];
 
     private final IntRange mTempRange1 = new IntRange();
     private final IntRange mTempRange2 = new IntRange();
@@ -344,13 +343,12 @@
         return rect;
     }
 
-    /**
-     * This is the final step of the resize. Here we save the new widget size and position
-     * to LauncherModel and animate the resize frame.
-     */
-    public void commitResize() {
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        // We are done with resizing the widget. Save the widget size & position to LauncherModel
         resizeWidgetIfNeeded(true);
-        requestLayout();
     }
 
     private void onTouchUp() {
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index a63767c..ac842f9 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -184,7 +184,7 @@
     public void applyFromShortcutInfo(ShortcutInfo info, boolean promiseStateChanged) {
         applyIconAndLabel(info.iconBitmap, info);
         setTag(info);
-        if (promiseStateChanged || info.isPromise()) {
+        if (promiseStateChanged || (info.hasPromiseIconUi())) {
             applyPromiseState(promiseStateChanged);
         }
 
@@ -481,7 +481,7 @@
     public void applyPromiseState(boolean promiseStateChanged) {
         if (getTag() instanceof ShortcutInfo) {
             ShortcutInfo info = (ShortcutInfo) getTag();
-            final boolean isPromise = info.isPromise();
+            final boolean isPromise = info.hasPromiseIconUi();
             final int progressLevel = isPromise ?
                     ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ?
                             info.getInstallProgress() : 0)) : 100;
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index e4a3226..632e490 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -29,6 +29,7 @@
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -69,6 +70,7 @@
     /** The paint applied to the drag view on hover */
     protected int mHoverColor = 0;
 
+    protected CharSequence mText;
     protected ColorStateList mOriginalTextColor;
     protected Drawable mDrawable;
 
@@ -96,6 +98,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        mText = getText();
         mOriginalTextColor = getTextColors();
     }
 
@@ -297,4 +300,30 @@
     public int getTextColor() {
         return getTextColors().getDefaultColor();
     }
+
+    /**
+     * Returns True if any update was made.
+     */
+    public boolean updateText(boolean hide) {
+        if ((hide && getText().toString().isEmpty()) || (!hide && mText.equals(getText()))) {
+            return false;
+        }
+
+        setText(hide ? "" : mText);
+        return true;
+    }
+
+    public boolean isTextTruncated() {
+        int availableWidth = getMeasuredWidth();
+        if (mHideParentOnDisable) {
+            ViewGroup parent = (ViewGroup) getParent();
+            availableWidth = parent.getMeasuredWidth() - parent.getPaddingLeft()
+                    - parent.getPaddingRight();
+        }
+        availableWidth -= (getPaddingLeft() + getPaddingRight() + mDrawable.getIntrinsicWidth()
+                + getCompoundDrawablePadding());
+        CharSequence displayedText = TextUtils.ellipsize(mText, getPaint(), availableWidth,
+                TextUtils.TruncateAt.END);
+        return !mText.equals(displayedText);
+    }
 }
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 975675a..4dcb64f 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -65,9 +65,11 @@
      * Set the drop target's text to either "Remove" or "Cancel" depending on the drag source.
      */
     public void setTextBasedOnDragSource(DragSource dragSource) {
-        if (!TextUtils.isEmpty(getText())) {
-            setText(dragSource.supportsDeleteDropTarget() ? R.string.remove_drop_target_label
+        if (!TextUtils.isEmpty(mText)) {
+            mText = getResources().getString(dragSource.supportsDeleteDropTarget()
+                    ? R.string.remove_drop_target_label
                     : android.R.string.cancel);
+            requestLayout();
         }
     }
 
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index 0840b70..29a1349 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -78,6 +78,58 @@
         setupButtonDropTarget(this, dragController);
     }
 
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        boolean hideText = hideTextHelper(false /* shouldUpdateText */, false /* no-op */);
+        if (hideTextHelper(true /* shouldUpdateText */, hideText)) {
+            // Text has changed, so we need to re-measure.
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * Helper method that iterates through the children and returns whether any of the visible
+     * {@link ButtonDropTarget} has truncated text.
+     *
+     * @param shouldUpdateText If True, updates the text of all children.
+     * @param hideText If True and {@param shouldUpdateText} is True, clears the text of all
+     *                 children; otherwise it sets the original text value.
+     *
+     *
+     * @return If shouldUpdateText is True, returns whether any of the children updated their text.
+     *         Else, returns whether any of the children have truncated their text.
+     */
+    private boolean hideTextHelper(boolean shouldUpdateText, boolean hideText) {
+        boolean result = false;
+        View visibleView;
+        ButtonDropTarget dropTarget;
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            if (getChildAt(i) instanceof ButtonDropTarget) {
+                visibleView = dropTarget = (ButtonDropTarget) getChildAt(i);
+            } else if (getChildAt(i) instanceof ViewGroup) {
+                // The Drop Target is wrapped in a FrameLayout.
+                visibleView = getChildAt(i);
+                dropTarget = (ButtonDropTarget) ((ViewGroup) visibleView).getChildAt(0);
+            } else {
+                // Ignore other views.
+                continue;
+            }
+
+            if (visibleView.getVisibility() == View.VISIBLE) {
+                if (shouldUpdateText) {
+                    result |= dropTarget.updateText(hideText);
+                } else if (dropTarget.isTextTruncated()) {
+                    result = true;
+                    break;
+                }
+            }
+        }
+
+        return result;
+    }
+
     private void setupButtonDropTarget(View view, DragController dragController) {
         if (view instanceof ButtonDropTarget) {
             ButtonDropTarget bdt = (ButtonDropTarget) view;
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 3bcd7af..ecadb18 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -478,6 +478,12 @@
     }
 
     private void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
+        if (info instanceof ShortcutInfo
+                && ((ShortcutInfo) info).hasStatusFlag(ShortcutInfo.FLAG_SUPPORTS_WEB_UI)
+                && (entry.icon == null || isDefaultIcon(entry.icon, info.user))) {
+            // skip updating shortcut info if no icon and supports web ui
+            return;
+        }
         info.title = Utilities.trim(entry.title);
         info.contentDescription = entry.contentDescription;
         info.iconBitmap = entry.icon == null ? getDefaultIcon(info.user) : entry.icon;
diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java
index f088d11..f919dd0 100644
--- a/src/com/android/launcher3/InfoDropTarget.java
+++ b/src/com/android/launcher3/InfoDropTarget.java
@@ -75,7 +75,7 @@
         if (info instanceof AppInfo) {
             componentName = ((AppInfo) info).componentName;
         } else if (info instanceof ShortcutInfo) {
-            componentName = ((ShortcutInfo) info).intent.getComponent();
+            componentName = info.getTargetComponent();
         } else if (info instanceof PendingAddItemInfo) {
             componentName = ((PendingAddItemInfo) info).componentName;
         } else if (info instanceof LauncherAppWidgetInfo) {
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index c5be096..fa3253c 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -138,17 +138,11 @@
 
     public ComponentName getTargetComponent() {
         Intent intent = getIntent();
-        if (intent == null) {
+        if (intent != null) {
+            return intent.getComponent();
+        } else {
             return null;
         }
-        ComponentName cn = intent.getComponent();
-        if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT && cn == null) {
-            // Legacy shortcuts may not have a componentName but just a packageName. In that case
-            // create a dummy componentName instead of adding additional check everywhere.
-            String pkg = intent.getPackage();
-            return pkg == null ? null : new ComponentName(pkg, IconCache.EMPTY_CLASS_NAME);
-        }
-        return cn;
     }
 
     public void writeToValues(ContentWriter writer) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8492a79..614d086 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -113,7 +113,6 @@
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -121,6 +120,7 @@
 import com.android.launcher3.util.ActivityResultInfo;
 import com.android.launcher3.util.RunnableWithId;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -2424,7 +2424,7 @@
         }
 
         // Check for abandoned promise
-        if ((v instanceof BubbleTextView) && shortcut.isPromise()) {
+        if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
             String packageName = shortcut.intent.getComponent() != null ?
                     shortcut.intent.getComponent().getPackageName() : shortcut.intent.getPackage();
             if (!TextUtils.isEmpty(packageName)) {
@@ -3068,7 +3068,7 @@
      */
     public void tryAndUpdatePredictedApps() {
         if (mLauncherCallbacks != null) {
-            List<ComponentKey> apps = mLauncherCallbacks.getPredictedApps();
+            List<ComponentKeyMapper<AppInfo>> apps = mLauncherCallbacks.getPredictedApps();
             if (apps != null) {
                 mAppsView.setPredictedApps(apps);
             }
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index d66b14c..c900e53 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -21,7 +21,7 @@
 import android.view.Menu;
 import android.view.View;
 
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -91,7 +91,7 @@
      */
     boolean shouldMoveToDefaultScreenOnHomeIntent();
     boolean hasSettings();
-    List<ComponentKey> getPredictedApps();
+    List<ComponentKeyMapper<AppInfo>> getPredictedApps();
     int SEARCH_BAR_HEIGHT_NORMAL = 0, SEARCH_BAR_HEIGHT_TALL = 1;
     /** Must return one of {@link #SEARCH_BAR_HEIGHT_NORMAL} or {@link #SEARCH_BAR_HEIGHT_TALL} */
     int getSearchBarHeight();
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index f0d9367..adf008b 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import android.annotation.TargetApi;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
@@ -59,6 +60,11 @@
     public static final int FLAG_RESTORE_STARTED = 8; //0B1000;
 
     /**
+     * Web UI supported.
+     */
+    public static final int FLAG_SUPPORTS_WEB_UI = 16; //0B10000;
+
+    /**
      * Indicates if it represents a common type mentioned in {@link CommonAppTypeParser}.
      * Upto 15 different types supported.
      */
@@ -188,6 +194,10 @@
         return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON);
     }
 
+    public boolean hasPromiseIconUi() {
+        return isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI);
+    }
+
     public int getInstallProgress() {
         return mInstallProgress;
     }
@@ -226,4 +236,18 @@
     public boolean isDisabled() {
         return isDisabled != 0;
     }
+
+    @Override
+    public ComponentName getTargetComponent() {
+        ComponentName cn = super.getTargetComponent();
+        if (cn == null && (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
+                || hasStatusFlag(FLAG_SUPPORTS_WEB_UI))) {
+            // Legacy shortcuts and promise icons with web UI may not have a componentName but just
+            // a packageName. In that case create a dummy componentName instead of adding additional
+            // check everywhere.
+            String pkg = intent.getPackage();
+            return pkg == null ? null : new ComponentName(pkg, IconCache.EMPTY_CLASS_NAME);
+        }
+        return cn;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 97a87c1..4eba5c6 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -50,6 +50,7 @@
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.List;
@@ -116,7 +117,7 @@
     /**
      * Sets the current set of predicted apps.
      */
-    public void setPredictedApps(List<ComponentKey> apps) {
+    public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
         mApps.setPredictedApps(apps);
     }
 
@@ -349,8 +350,6 @@
             ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams();
             navBarBgLp.height = insets.bottom;
             navBarBg.setLayoutParams(navBarBgLp);
-            navBarBg.setVisibility(FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS
-                    ? View.INVISIBLE : View.VISIBLE);
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 5e7a5ca..6bbe3ea 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.discovery.AppDiscoveryItem;
 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
 import com.android.launcher3.util.LabelComparator;
 
 import java.util.ArrayList;
@@ -173,7 +174,7 @@
     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
     private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
     // The set of predicted app component names
-    private final List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
+    private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
     // The set of predicted apps resolved from the component names and the current set of apps
     private final List<AppInfo> mPredictedApps = new ArrayList<>();
     private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>();
@@ -298,20 +299,20 @@
         updateAdapterItems();
     }
 
-    private List<AppInfo> processPredictedAppComponents(List<ComponentKey> components) {
+    private List<AppInfo> processPredictedAppComponents(List<ComponentKeyMapper<AppInfo>> components) {
         if (mComponentToAppMap.isEmpty()) {
             // Apps have not been bound yet.
             return Collections.emptyList();
         }
 
         List<AppInfo> predictedApps = new ArrayList<>();
-        for (ComponentKey ck : components) {
-            AppInfo info = mComponentToAppMap.get(ck);
+        for (ComponentKeyMapper<AppInfo> mapper : components) {
+            AppInfo info = mapper.getItem(mComponentToAppMap);
             if (info != null) {
                 predictedApps.add(info);
             } else {
                 if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                    Log.e(TAG, "Predicted app not found: " + ck);
+                    Log.e(TAG, "Predicted app not found: " + mapper);
                 }
             }
             // Stop at the number of predicted apps
@@ -331,7 +332,7 @@
      * If the number of predicted apps is the same as the previous list of predicted apps,
      * we can optimize by swapping them in place.
      */
-    public void setPredictedApps(List<ComponentKey> apps) {
+    public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
         mPredictedAppComponents.clear();
         mPredictedAppComponents.addAll(apps);
 
@@ -472,14 +473,14 @@
 
         if (DEBUG_PREDICTIONS) {
             if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
-                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle()));
-                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle()));
-                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle()));
-                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle()));
+                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                        Process.myUserHandle())));
+                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                        Process.myUserHandle())));
+                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                        Process.myUserHandle())));
+                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                        Process.myUserHandle())));
             }
         }
 
@@ -644,8 +645,8 @@
         return result;
     }
 
-    public AppInfo findApp(ComponentKey key) {
-        return mComponentToAppMap.get(key);
+    public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
+        return mapper.getItem(mComponentToAppMap);
     }
 
     /**
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index ee6a0e0..fde7995 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -544,7 +544,6 @@
 
     public void clearResizeFrame() {
         if (mCurrentResizeFrame != null) {
-            mCurrentResizeFrame.commitResize();
             removeView(mCurrentResizeFrame);
             mCurrentResizeFrame = null;
         }
diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java
index d955674..7c80c30 100644
--- a/src/com/android/launcher3/graphics/LauncherIcons.java
+++ b/src/com/android/launcher3/graphics/LauncherIcons.java
@@ -38,6 +38,7 @@
 import android.support.annotation.Nullable;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.IconCache;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
@@ -192,13 +193,16 @@
      * Adds the {@param badge} on top of {@param srcTgt} using the badge dimensions.
      */
     public static Bitmap badgeWithBitmap(Bitmap srcTgt, Bitmap badge, Context context) {
+        return badgeWithDrawable(srcTgt, new FastBitmapDrawable(badge), context);
+    }
+
+    public static Bitmap badgeWithDrawable(Bitmap srcTgt, Drawable badge, Context context) {
         int badgeSize = context.getResources().getDimensionPixelSize(R.dimen.profile_badge_size);
         synchronized (sCanvas) {
             sCanvas.setBitmap(srcTgt);
-            sCanvas.drawBitmap(badge, new Rect(0, 0, badge.getWidth(), badge.getHeight()),
-                    new Rect(srcTgt.getWidth() - badgeSize,
-                            srcTgt.getHeight() - badgeSize, srcTgt.getWidth(), srcTgt.getHeight()),
-                    new Paint(Paint.FILTER_BITMAP_FLAG));
+            int iconSize = srcTgt.getWidth();
+            badge.setBounds(iconSize - badgeSize, iconSize - badgeSize, iconSize, iconSize);
+            badge.draw(sCanvas);
             sCanvas.setBitmap(null);
         }
         return srcTgt;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index c56325a..4756edc 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -431,6 +431,10 @@
                                 }
                             }
 
+                            if ((c.restoreFlag & ShortcutInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
+                                validTarget = false;
+                            }
+
                             if (validTarget) {
                                 // The shortcut points to a valid target (either no target
                                 // or something which is ready to be used)
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 1e0af68..ebc6f23 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -94,7 +94,7 @@
                 if (info instanceof ShortcutInfo) {
                     ShortcutInfo si = (ShortcutInfo) info;
                     ComponentName cn = si.getTargetComponent();
-                    if (si.isPromise() && (cn != null)
+                    if (si.hasPromiseIconUi() && (cn != null)
                             && mInstallInfo.packageName.equals(cn.getPackageName())) {
                         si.setInstallProgress(mInstallInfo.progress);
                         if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 6c78d5b..98bffe1 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -104,6 +104,7 @@
                         SessionCommitReceiver.queueAppIconAddition(context, packages[i], mUser);
                     }
                 }
+                flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
             }
             case OP_UPDATE:
@@ -170,12 +171,12 @@
             }
         }
 
+        final LongArrayMap<Boolean> removedShortcuts = new LongArrayMap<>();
+
         // Update shortcut infos
         if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
             final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
-            final LongArrayMap<Boolean> removedShortcuts = new LongArrayMap<>();
             final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
-
             synchronized (dataModel) {
                 for (ItemInfo info : dataModel.itemsIdMap) {
                     if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
@@ -197,6 +198,12 @@
                         if (cn != null && matcher.matches(si, cn)) {
                             AppInfo appInfo = addedOrUpdatedApps.get(cn);
 
+                            if (mOp == OP_REMOVE
+                                    && si.hasStatusFlag(ShortcutInfo.FLAG_SUPPORTS_WEB_UI)) {
+                                removedShortcuts.put(si.id, false);
+                                continue;
+                            }
+
                             // For system apps, package manager send OP_UPDATE when an
                             // app is enabled.
                             if (si.isPromise() && (mOp == OP_ADD || mOp == OP_UPDATE)) {
@@ -220,7 +227,6 @@
                                         si.intent = intent;
                                     }
                                 }
-
                                 si.status = ShortcutInfo.DEFAULT;
                                 infoUpdated = true;
                                 if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
@@ -308,7 +314,8 @@
 
         if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
             ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
-                    .or(ItemInfoMatcher.ofComponents(removedComponents, mUser));
+                    .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
+                    .and(ItemInfoMatcher.ofItemIds(removedShortcuts, true));
             deleteAndBindComponentsRemoved(removeMatch);
 
             // Remove any queued items from the install queue
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
index 5ce78dc..f44f5c8 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -66,7 +66,7 @@
 
     public static boolean supportsShortcuts(ItemInfo info) {
         boolean isItemPromise = info instanceof com.android.launcher3.ShortcutInfo
-                && ((com.android.launcher3.ShortcutInfo) info).isPromise();
+                && ((com.android.launcher3.ShortcutInfo) info).hasPromiseIconUi();
         return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
                 && !info.isDisabled() && !isItemPromise;
     }
diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java
index 8d43518..e5842fa 100644
--- a/src/com/android/launcher3/testing/LauncherExtension.java
+++ b/src/com/android/launcher3/testing/LauncherExtension.java
@@ -11,6 +11,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherCallbacks;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -197,7 +198,7 @@
         }
 
         @Override
-        public List<ComponentKey> getPredictedApps() {
+        public List<ComponentKeyMapper<AppInfo>> getPredictedApps() {
             // To debug app predictions, enable AlphabeticalAppsList#DEBUG_PREDICTIONS
             return new ArrayList<>();
         }
diff --git a/src/com/android/launcher3/util/ComponentKeyMapper.java b/src/com/android/launcher3/util/ComponentKeyMapper.java
new file mode 100644
index 0000000..916176a
--- /dev/null
+++ b/src/com/android/launcher3/util/ComponentKeyMapper.java
@@ -0,0 +1,50 @@
+package com.android.launcher3.util;
+
+/**
+ * Copyright (C) 2017 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.
+ */
+
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class ComponentKeyMapper<T> {
+
+    protected final ComponentKey mComponentKey;
+
+    public ComponentKeyMapper(ComponentKey key) {
+        this.mComponentKey = key;
+    }
+
+    public @Nullable T getItem(Map<ComponentKey, T> map) {
+        return map.get(mComponentKey);
+    }
+
+    public String getPackage() {
+        return mComponentKey.componentName.getPackageName();
+    }
+
+    public String getComponentClass() {
+        return mComponentKey.componentName.getClassName();
+    }
+
+    @Override
+    public String toString() {
+        return mComponentKey.toString();
+    }
+
+}
diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/WidgetListRowEntry.java
index 3e89eeb..335b8c7 100644
--- a/src/com/android/launcher3/widget/WidgetListRowEntry.java
+++ b/src/com/android/launcher3/widget/WidgetListRowEntry.java
@@ -41,4 +41,8 @@
         this.widgets = items;
     }
 
+    @Override
+    public String toString() {
+        return pkgItem.packageName + ":" + widgets.size();
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 14a9d17..acec3dd 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -17,10 +17,12 @@
 package com.android.launcher3.widget;
 
 import android.content.Context;
+import android.content.pm.LauncherApps;
 import android.graphics.Point;
 import android.support.v7.widget.LinearLayoutManager;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Toast;
@@ -31,8 +33,10 @@
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.model.PackageItemInfo;
@@ -74,7 +78,11 @@
     public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mLauncher = Launcher.getLauncher(context);
-        mAdapter = new WidgetsListAdapter(this, this, context);
+        LauncherAppState apps = LauncherAppState.getInstance(context);
+        mAdapter = new WidgetsListAdapter(context, LayoutInflater.from(context),
+                apps.getWidgetCache(), new AlphabeticIndexCompat(context), this, this,
+                new WidgetsDiffReporter(apps.getIconCache()));
+        mAdapter.setNotifyListener();
         if (LOGD) {
             Log.d(TAG, "WidgetsContainerView constructor");
         }
@@ -232,7 +240,6 @@
      */
     public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> model) {
         mAdapter.setWidgets(model);
-        mAdapter.notifyDataSetChanged();
 
         View loader = getContentView().findViewById(R.id.loader);
         if (loader != null) {
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
new file mode 100644
index 0000000..d9c9ef9
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017 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.launcher3.widget;
+
+import android.util.Log;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Do diff on widget's tray list items and call the {@link NotifyListener} methods accordingly.
+ */
+public class WidgetsDiffReporter {
+    private final boolean DEBUG = true;
+    private final String TAG = "WidgetsDiffReporter";
+    private final IconCache mIconCache;
+    private NotifyListener mListener;
+
+    public interface NotifyListener {
+        void notifyDataSetChanged();
+        void notifyItemChanged(int index);
+        void notifyItemInserted(int index);
+        void notifyItemRemoved(int index);
+    }
+
+    public WidgetsDiffReporter(IconCache iconCache) {
+        mIconCache = iconCache;
+    }
+
+    public void setListener(NotifyListener listener) {
+        mListener = listener;
+    }
+
+    public void process(ArrayList<WidgetListRowEntry> currentEntries,
+            ArrayList<WidgetListRowEntry> newEntries, WidgetListRowEntryComparator comparator) {
+        if (DEBUG) {
+            Log.d(TAG, "process oldEntries#=" + currentEntries.size()
+                    + " newEntries#=" + newEntries.size());
+        }
+        if (currentEntries.size() == 0 && newEntries.size() > 0) {
+            currentEntries.addAll(newEntries);
+            mListener.notifyDataSetChanged();
+            return;
+        }
+        ArrayList<WidgetListRowEntry> orgEntries =
+                (ArrayList<WidgetListRowEntry>) currentEntries.clone();
+        Iterator<WidgetListRowEntry> orgIter = orgEntries.iterator();
+        Iterator<WidgetListRowEntry> newIter = newEntries.iterator();
+
+        WidgetListRowEntry orgRowEntry = orgIter.next();
+        WidgetListRowEntry newRowEntry = newIter.next();
+
+        do {
+            int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
+            if (DEBUG) {
+                Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
+                        diff, orgRowEntry != null? orgRowEntry.toString() : null,
+                        newRowEntry != null? newRowEntry.toString() : null));
+            }
+            int index = -1;
+            if (diff < 0) {
+                index = currentEntries.indexOf(orgRowEntry);
+                mListener.notifyItemRemoved(index);
+                if (DEBUG) {
+                    Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
+                            orgRowEntry.titleSectionName));
+                }
+                currentEntries.remove(index);
+                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+            } else if (diff > 0) {
+                index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry):
+                        currentEntries.size();
+                currentEntries.add(index, newRowEntry);
+                newRowEntry = newIter.hasNext() ? newIter.next() : null;
+                mListener.notifyItemInserted(index);
+                if (DEBUG) {
+                    Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
+                            newRowEntry.titleSectionName));
+                }
+            } else {
+                // same package name but,
+                // did the icon, title, etc, change?
+                // or did the widget size and desc, span, etc change?
+                if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) ||
+                        !orgRowEntry.widgets.equals(newRowEntry.widgets)) {
+                    index = currentEntries.indexOf(orgRowEntry);
+                    currentEntries.set(index, newRowEntry);
+                    mListener.notifyItemChanged(index);
+                    if (DEBUG) {
+                        Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
+                                newRowEntry.titleSectionName));
+                    }
+                }
+                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+                newRowEntry = newIter.hasNext() ? newIter.next() : null;
+            }
+        } while(orgRowEntry != null || newRowEntry != null);
+    }
+
+    /**
+     * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
+     * Also handle null row pointers.
+     */
+    private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow,
+            WidgetListRowEntryComparator comparator) {
+        if (curRow == null && newRow == null) {
+            throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null.");
+        }
+
+        if (curRow == null && newRow != null) {
+            return 1; // new row needs to be inserted
+        } else if (curRow != null && newRow == null) {
+            return -1; // old row needs to be deleted
+        }
+        return comparator.compare(curRow, newRow);
+    }
+
+    private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
+        return curInfo.iconBitmap.equals(newInfo.iconBitmap) &&
+                !mIconCache.isDefaultIcon(curInfo.iconBitmap, curInfo.user);
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
index a1eb0ab..6b1800c 100644
--- a/src/com/android/launcher3/widget/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java
@@ -21,9 +21,10 @@
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
 
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
@@ -55,40 +56,67 @@
 
     private final WidgetPreviewLoader mWidgetPreviewLoader;
     private final LayoutInflater mLayoutInflater;
-
-    private final View.OnClickListener mIconClickListener;
-    private final View.OnLongClickListener mIconLongClickListener;
-
-    private final ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
     private final AlphabeticIndexCompat mIndexer;
 
+    private final OnClickListener mIconClickListener;
+    private final OnLongClickListener mIconLongClickListener;
     private final int mIndent;
+    private ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
+    private final WidgetsDiffReporter mDiffReporter;
 
-    public WidgetsListAdapter(View.OnClickListener iconClickListener,
-            View.OnLongClickListener iconLongClickListener,
-            Context context) {
-        mLayoutInflater = LayoutInflater.from(context);
-        mWidgetPreviewLoader = LauncherAppState.getInstance(context).getWidgetCache();
-
-        mIndexer = new AlphabeticIndexCompat(context);
-
+    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
+            WidgetPreviewLoader widgetPreviewLoader, AlphabeticIndexCompat indexCompat,
+            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,
+            WidgetsDiffReporter diffReporter) {
+        mLayoutInflater = layoutInflater;
+        mWidgetPreviewLoader = widgetPreviewLoader;
+        mIndexer = indexCompat;
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
         mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
+        mDiffReporter = diffReporter;
     }
 
-    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> widgets) {
-        mEntries.clear();
-        WidgetItemComparator widgetComparator = new WidgetItemComparator();
+    public void setNotifyListener() {
+        mDiffReporter.setListener(new WidgetsDiffReporter.NotifyListener() {
+            @Override
+            public void notifyDataSetChanged() {
+                WidgetsListAdapter.this.notifyDataSetChanged();
+            }
 
+            @Override
+            public void notifyItemChanged(int index) {
+                WidgetsListAdapter.this.notifyItemChanged(index);
+            }
+
+            @Override
+            public void notifyItemInserted(int index) {
+                WidgetsListAdapter.this.notifyItemInserted(index);
+            }
+
+            @Override
+            public void notifyItemRemoved(int index) {
+                WidgetsListAdapter.this.notifyItemRemoved(index);
+            }
+        });
+    }
+
+    /**
+     * Update the widget list.
+     */
+    public void setWidgets(MultiHashMap<PackageItemInfo, WidgetItem> widgets) {
+        ArrayList<WidgetListRowEntry> tempEntries = new ArrayList<>();
+
+        WidgetItemComparator widgetComparator = new WidgetItemComparator();
         for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : widgets.entrySet()) {
             WidgetListRowEntry row = new WidgetListRowEntry(entry.getKey(), entry.getValue());
             row.titleSectionName = mIndexer.computeSectionName(row.pkgItem.title);
             Collections.sort(row.widgets, widgetComparator);
-            mEntries.add(row);
+            tempEntries.add(row);
         }
-
-        Collections.sort(mEntries, new WidgetListRowEntryComparator());
+        WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator();
+        Collections.sort(tempEntries, rowComparator);
+        mDiffReporter.process(mEntries, tempEntries, rowComparator);
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
new file mode 100644
index 0000000..40b65e4
--- /dev/null
+++ b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017 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.launcher3.widget;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.LayoutInflater;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.MultiHashMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WidgetsListAdapterTest {
+
+    private final String TAG = "WidgetsListAdapterTest";
+
+    @Mock private LayoutInflater mMockLayoutInflater;
+    @Mock private WidgetPreviewLoader mMockWidgetCache;
+    @Mock private WidgetsDiffReporter.NotifyListener mListener;
+    @Mock private IconCache mIconCache;
+
+    private WidgetsListAdapter mAdapter;
+    private AlphabeticIndexCompat mIndexCompat;
+    private InvariantDeviceProfile mTestProfile;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getTargetContext();
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+        mIndexCompat = new AlphabeticIndexCompat(mContext);
+        WidgetsDiffReporter reporter = new WidgetsDiffReporter(mIconCache);
+        reporter.setListener(mListener);
+        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
+                mIndexCompat, null, null, reporter);
+    }
+
+    @Test
+    public void test_notifyDataSetChanged() throws Exception {
+        mAdapter.setWidgets(generateSampleMap(1));
+        verify(mListener, times(1)).notifyDataSetChanged();
+    }
+
+    @Test
+    public void test_notifyItemInserted() throws Exception {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(2));
+        verify(mListener, times(1)).notifyDataSetChanged();
+        verify(mListener, times(1)).notifyItemInserted(1);
+    }
+
+    @Test
+    public void test_notifyItemRemoved() throws Exception {
+        mAdapter.setWidgets(generateSampleMap(2));
+        mAdapter.setWidgets(generateSampleMap(1));
+        verify(mListener, times(1)).notifyDataSetChanged();
+        verify(mListener, times(1)).notifyItemRemoved(1);
+    }
+
+    @Test
+    public void testNotifyItemChanged_PackageIconDiff() throws Exception {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(1));
+        verify(mListener, times(1)).notifyDataSetChanged();
+        verify(mListener, times(1)).notifyItemChanged(0);
+    }
+
+    @Test
+    public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
+        // TODO: same package name but item number changed
+    }
+
+    @Test
+    public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
+        // TODO: insert and remove combined.          curMap
+        // newMap [A, C, D]                           [A, B, E]
+        // B - C < 0, removed B from index 1          [A, E]
+        // E - C > 0, C inserted to index 1           [A, C, E]
+        // E - D > 0, D inserted to index 2           [A, C, D, E]
+        // E - null = -1, E deleted from index 3      [A, C, D]
+    }
+
+    /**
+     * Helper method to generate the sample widget model map that can be used for the tests
+     * @param num the number of WidgetItem the map should contain
+     * @return
+     */
+    private MultiHashMap<PackageItemInfo, WidgetItem> generateSampleMap(int num) {
+        MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
+        if (num <= 0) return newMap;
+
+        PackageManager pm = mContext.getPackageManager();
+        AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext);
+        for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
+            WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
+                    .fromProviderInfo(mContext, widgetInfo), pm, mTestProfile);
+
+            PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
+            pInfo.title = pInfo.packageName;
+            pInfo.user = wi.user;
+            pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
+            newMap.addToList(pInfo, wi);
+            if (newMap.size() == num) {
+                break;
+            }
+        }
+        return newMap;
+    }
+}