Merge "App discovery integration in All Apps search" into ub-launcher3-dorval
diff --git a/res/drawable/ic_instant_app.xml b/res/drawable/ic_instant_app.xml
new file mode 100644
index 0000000..be5a3e0
--- /dev/null
+++ b/res/drawable/ic_instant_app.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/badge_size"
+    android:height="@dimen/badge_size"
+    android:viewportWidth="18"
+    android:viewportHeight="18">
+
+    <path
+        android:fillColor="@android:color/black"
+        android:fillType="evenOdd"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:fillType="evenOdd"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:fillType="evenOdd"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/black"
+        android:fillAlpha="0.87"
+        android:fillType="evenOdd"
+        android:strokeWidth="1"
+        android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
+</vector>
diff --git a/res/drawable/ic_star_rating.xml b/res/drawable/ic_star_rating.xml
new file mode 100644
index 0000000..4e34fa3
--- /dev/null
+++ b/res/drawable/ic_star_rating.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="12dp"
+    android:viewportWidth="12"
+    android:viewportHeight="12">
+
+    <path
+        android:fillColor="#FB8C00"
+        android:fillType="evenOdd"
+        android:strokeWidth="1"
+        android:pathData="M 9.76511755 11.9348136 L 8.33665684 7.16088817 L 12.080006 4.41656311 L 7.49967039 4.41856896 L 6.03138903 0 L 4.57932894 4.41856896 L -1.34115008e-16 4.41656311 L 3.72612122 7.16088817 L 2.29967385 11.9348136 L 6.03138903 8.82574452 Z" />
+</vector>
\ No newline at end of file
diff --git a/res/layout/all_apps_discovery_item.xml b/res/layout/all_apps_discovery_item.xml
new file mode 100644
index 0000000..2b21ef5
--- /dev/null
+++ b/res/layout/all_apps_discovery_item.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.discovery.AppDiscoveryItemView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clickable="true"
+    android:background="?android:selectableItemBackground">
+
+    <ImageView
+        android:id="@+id/image"
+        android:layout_width="56dp"
+        android:layout_height="56dp"
+        android:padding="8dp"
+        android:scaleType="fitCenter"/>
+
+    <ImageView
+        android:id="@+id/badge"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_instant_app"
+        android:layout_marginRight="6dp"
+        android:layout_marginBottom="6dp"
+        android:layout_alignRight="@+id/image"
+        android:layout_alignBottom="@+id/image"
+        android:clickable="false"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_centerVertical="true"
+        android:layout_toRightOf="@id/image">
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="?android:textColorSecondary"
+            android:textSize="15sp"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+            <TextView
+                android:id="@+id/rating"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="?android:textColorSecondary"
+                android:textSize="14sp"
+                android:layout_gravity="center_vertical"/>
+
+            <com.android.launcher3.discovery.RatingView
+                android:id="@+id/rating_view"
+                android:layout_width="80dp"
+                android:layout_height="16dp"
+                android:layout_marginLeft="5dp"
+                android:layout_marginRight="5dp"
+                android:layout_gravity="center_vertical"/>
+
+            <TextView
+                android:id="@+id/review_count"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="5dp"
+                android:textColor="?android:textColorHint"
+                android:textSize="14sp"
+                android:layout_gravity="center_vertical"/>
+
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_weight="1"/>
+
+            <TextView
+                android:id="@+id/price"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="?android:textColorHint"
+                android:textSize="14sp"
+                android:layout_marginRight="12dp"
+                android:textAllCaps="true"/>
+        </LinearLayout>
+    </LinearLayout>
+
+    <ImageView
+        android:importantForAccessibility="no"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:paddingLeft="@dimen/container_fastscroll_thumb_max_width"
+        android:paddingRight="@dimen/container_fastscroll_thumb_max_width"
+        android:src="@drawable/all_apps_divider"
+        android:scaleType="fitXY"
+        android:focusable="false" />
+</com.android.launcher3.discovery.AppDiscoveryItemView>
\ No newline at end of file
diff --git a/res/layout/all_apps_discovery_loading_divider.xml b/res/layout/all_apps_discovery_loading_divider.xml
new file mode 100644
index 0000000..c7b5ad2
--- /dev/null
+++ b/res/layout/all_apps_discovery_loading_divider.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="20dp"
+    android:paddingLeft="@dimen/container_fastscroll_thumb_max_width"
+    android:paddingRight="@dimen/container_fastscroll_thumb_max_width">
+
+    <ProgressBar
+        android:id="@+id/loadingProgressBar"
+        style="@android:style/Widget.Material.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="6dp"
+        android:maxHeight="6dp"
+        android:indeterminate="true"
+        android:layout_gravity="center"/>
+
+    <View
+        android:id="@+id/loadedDivider"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="@drawable/all_apps_divider"
+        android:layout_gravity="center"
+        android:visibility="invisible"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index 8bf49c2..0ddde73 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -21,14 +21,11 @@
 import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.os.UserHandle;
-import android.util.Log;
 
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
 
-import java.util.ArrayList;
-
 /**
  * Represents an app in AllAppsView.
  */
@@ -44,7 +41,7 @@
     /**
      * {@see ShortcutInfo#isDisabled}
      */
-    int isDisabled = ShortcutInfo.DEFAULT;
+    public int isDisabled = ShortcutInfo.DEFAULT;
 
     public AppInfo() {
         itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
@@ -83,7 +80,6 @@
         title = Utilities.trim(info.title);
         intent = new Intent(info.intent);
         isDisabled = info.isDisabled;
-        iconBitmap = info.iconBitmap;
     }
 
     @Override
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java
index a3d8c6a..1e020e2 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/ItemInfoWithIcon.java
@@ -35,7 +35,9 @@
 
     protected ItemInfoWithIcon() { }
 
-    protected ItemInfoWithIcon(ItemInfo info) {
+    protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
+        iconBitmap = info.iconBitmap;
+        usingLowResIcon = info.usingLowResIcon;
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 595e11a..f9e6f4b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1059,6 +1059,7 @@
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onResume();
         }
+
     }
 
     @Override
@@ -2459,7 +2460,7 @@
             throw new IllegalArgumentException("Input must have a valid intent");
         }
         boolean success = startActivitySafely(v, intent, item);
-        getUserEventDispatcher().logAppLaunch(v, intent);
+        getUserEventDispatcher().logAppLaunch(v, intent); // TODO for discovered apps b/35802115
 
         if (success && v instanceof BubbleTextView) {
             mWaitingForResume = (BubbleTextView) v;
@@ -2708,9 +2709,10 @@
             intent.setSourceBounds(getViewBounds(v));
         }
         try {
-            if (Utilities.ATLEAST_MARSHMALLOW && item != null
+            if (Utilities.ATLEAST_MARSHMALLOW
+                    && (item instanceof ShortcutInfo)
                     && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT
-                    || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                     || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
                     && !((ShortcutInfo) item).isPromise()) {
                 // Shortcuts need some special checks due to legacy reasons.
                 startShortcutIntentSafely(intent, optsBundle, item);
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index b35dcb7..f0bb1c0 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -134,7 +134,6 @@
         title = info.title;
         intent = new Intent(info.intent);
         iconResource = info.iconResource;
-        iconBitmap = info.iconBitmap;
         status = info.status;
         mInstallProgress = info.mInstallProgress;
         isDisabled = info.isDisabled;
@@ -146,8 +145,6 @@
         title = Utilities.trim(info.title);
         intent = new Intent(info.intent);
         isDisabled = info.isDisabled;
-        iconBitmap = info.iconBitmap;
-        usingLowResIcon = info.usingLowResIcon;
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 0732004..cc5fa8c 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -20,6 +20,8 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.InsetDrawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
 import android.text.Spannable;
@@ -46,6 +48,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.discovery.AppDiscoveryItem;
+import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
@@ -211,7 +215,7 @@
 
         // IF scroller is at the very top OR there is no scroll bar because there is probably not
         // enough items to scroll, THEN it's okay for the container to be pulled down.
-        if (mAppsRecyclerView.getScrollBar().getThumbOffsetY() <= 0) {
+        if (mAppsRecyclerView.getCurrentScrollY() == 0) {
             return true;
         }
         return false;
@@ -425,14 +429,22 @@
     @Override
     public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
         if (apps != null) {
-            if (mApps.setOrderedFilter(apps)) {
-                mAppsRecyclerView.onSearchResultsChanged();
-            }
+            mApps.setOrderedFilter(apps);
+            mAppsRecyclerView.onSearchResultsChanged();
             mAdapter.setLastSearchQuery(query);
         }
     }
 
     @Override
+    public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
+            @NonNull AppDiscoveryUpdateState state) {
+        if (!mLauncher.isDestroyed()) {
+            mApps.onAppDiscoverySearchUpdate(app, state);
+            mAppsRecyclerView.onSearchResultsChanged();
+        }
+    }
+
+    @Override
     public void clearSearchResult() {
         if (mApps.setOrderedFilter(null)) {
             mAppsRecyclerView.onSearchResultsChanged();
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index f352304..59cac8d 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -33,12 +33,13 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 
+import com.android.launcher3.discovery.AppDiscoveryAppInfo;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
+import com.android.launcher3.discovery.AppDiscoveryItemView;
 
 import java.util.List;
 
@@ -67,6 +68,8 @@
     public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6;
     // The divider that separates prediction icons from the app list
     public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7;
+    public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 8;
+    public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 9;
 
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER
@@ -74,6 +77,8 @@
             | VIEW_TYPE_PREDICTION_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
             | VIEW_TYPE_PREDICTION_ICON;
+    public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
+            | VIEW_TYPE_DISCOVERY_ITEM;
 
 
     public interface BindViewCallback {
@@ -84,7 +89,6 @@
      * ViewHolder for each icon.
      */
     public static class ViewHolder extends RecyclerView.ViewHolder {
-
         public ViewHolder(View v) {
             super(v);
         }
@@ -150,7 +154,7 @@
             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
             int extraRows = 0;
             for (int i = 0; i <= adapterPosition; i++) {
-                if ((items.get(i).viewType & VIEW_TYPE_MASK_ICON) == 0) {
+                if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
                     extraRows++;
                 }
             }
@@ -273,8 +277,7 @@
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
             case VIEW_TYPE_ICON:
-                /* falls through */
-            case VIEW_TYPE_PREDICTION_ICON: {
+            case VIEW_TYPE_PREDICTION_ICON:
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         R.layout.all_apps_icon, parent, false);
                 icon.setOnClickListener(mIconClickListener);
@@ -284,14 +287,14 @@
                 icon.setOnFocusChangeListener(mIconFocusListener);
 
                 // Ensure the all apps icon height matches the workspace icons
-                DeviceProfile profile = mLauncher.getDeviceProfile();
-                Point cellSize = profile.getCellSize();
-                GridLayoutManager.LayoutParams lp =
-                        (GridLayoutManager.LayoutParams) icon.getLayoutParams();
-                lp.height = cellSize.y;
-                icon.setLayoutParams(lp);
+                icon.getLayoutParams().height = getCellSize().y;
                 return new ViewHolder(icon);
-            }
+            case VIEW_TYPE_DISCOVERY_ITEM:
+                AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
+                        .inflate(R.layout.all_apps_discovery_item, parent, false);
+                appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
+                        mIconLongClickListener);
+                return new ViewHolder(appDiscoveryItemView);
             case VIEW_TYPE_EMPTY_SEARCH:
                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
                         parent, false));
@@ -308,8 +311,11 @@
             case VIEW_TYPE_SEARCH_DIVIDER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.all_apps_search_divider, parent, false));
+            case VIEW_TYPE_APPS_LOADING_DIVIDER:
+                View loadingDividerView = mLayoutInflater.inflate(
+                        R.layout.all_apps_discovery_loading_divider, parent, false);
+                return new ViewHolder(loadingDividerView);
             case VIEW_TYPE_PREDICTION_DIVIDER:
-                /* falls through */
             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.all_apps_divider, parent, false));
@@ -318,23 +324,26 @@
         }
     }
 
+    private Point getCellSize() {
+        return mLauncher.getDeviceProfile().getCellSize();
+    }
+
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
         switch (holder.getItemViewType()) {
-            case VIEW_TYPE_ICON: {
+            case VIEW_TYPE_ICON:
+            case VIEW_TYPE_PREDICTION_ICON:
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.itemView;
                 icon.applyFromApplicationInfo(info);
                 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                 break;
-            }
-            case VIEW_TYPE_PREDICTION_ICON: {
-                AppInfo info = mApps.getAdapterItems().get(position).appInfo;
-                BubbleTextView icon = (BubbleTextView) holder.itemView;
-                icon.applyFromApplicationInfo(info);
-                icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
+            case VIEW_TYPE_DISCOVERY_ITEM:
+                AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
+                        mApps.getAdapterItems().get(position).appInfo;
+                AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
+                view.apply(appDiscoveryAppInfo);
                 break;
-            }
             case VIEW_TYPE_EMPTY_SEARCH:
                 TextView emptyViewText = (TextView) holder.itemView;
                 emptyViewText.setText(mEmptySearchMessage);
@@ -349,6 +358,15 @@
                     searchView.setVisibility(View.GONE);
                 }
                 break;
+            case VIEW_TYPE_APPS_LOADING_DIVIDER:
+                int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
+                int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
+                holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
+                holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
+                break;
+            case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
+                // nothing to do
+                break;
         }
         if (mBindViewCallback != null) {
             mBindViewCallback.onBindView(holder);
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index a41d832..64e2fcb 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
@@ -110,44 +111,40 @@
      * all the different view types.
      */
     public void preMeasureViews(AllAppsGridAdapter adapter) {
+        View icon = adapter.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON).itemView;
+        final int iconHeight = icon.getLayoutParams().height;
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight);
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight);
+
         final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                 getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST);
         final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                 getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST);
 
-        // Icons
-        BubbleTextView icon = (BubbleTextView) adapter.onCreateViewHolder(this,
-                AllAppsGridAdapter.VIEW_TYPE_ICON).itemView;
-        int iconHeight = icon.getLayoutParams().height;
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight);
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight);
+        putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER,
+                AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER);
+        putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER);
+        putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET);
+        putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH);
 
-        // Search divider
-        View searchDivider = adapter.onCreateViewHolder(this,
-                AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER).itemView;
-        searchDivider.measure(widthMeasureSpec, heightMeasureSpec);
-        int searchDividerHeight = searchDivider.getMeasuredHeight();
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER, searchDividerHeight);
+        if (FeatureFlags.DISCOVERY_ENABLED) {
+            putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                    AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER);
+            putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
+                    AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM);
+        }
+    }
 
-        // Generic dividers
-        View divider = adapter.onCreateViewHolder(this,
-                AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER).itemView;
-        divider.measure(widthMeasureSpec, heightMeasureSpec);
-        int dividerHeight = divider.getMeasuredHeight();
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, dividerHeight);
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, dividerHeight);
-
-        // Search views
-        View emptySearch = adapter.onCreateViewHolder(this,
-                AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH).itemView;
-        emptySearch.measure(widthMeasureSpec, heightMeasureSpec);
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH,
-                emptySearch.getMeasuredHeight());
-        View searchMarket = adapter.onCreateViewHolder(this,
-                AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET).itemView;
-        searchMarket.measure(widthMeasureSpec, heightMeasureSpec);
-        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET,
-                searchMarket.getMeasuredHeight());
+    private void putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes) {
+        View view = adapter.onCreateViewHolder(this, viewTypes[0]).itemView;
+        view.measure(w, h);
+        for (int viewType : viewTypes) {
+            mViewHeights.put(viewType, view.getMeasuredHeight());
+        }
     }
 
     /**
@@ -207,7 +204,7 @@
         // Always scroll the view to the top so the user can see the changed results
         scrollToTop();
 
-        if (mApps.hasNoFilteredResults()) {
+        if (mApps.shouldShowEmptySearch()) {
             if (mEmptySearchBackground == null) {
                 mEmptySearchBackground = DrawableFactory.get(getContext())
                         .getAllAppsBackground(getContext());
@@ -438,4 +435,5 @@
                 x + mEmptySearchBackground.getIntrinsicWidth(),
                 y + mEmptySearchBackground.getIntrinsicHeight());
     }
+
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
index 365ab31..c7ba3ab 100644
--- a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
@@ -19,6 +19,8 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
@@ -32,6 +34,8 @@
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.discovery.AppDiscoveryItem;
+import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.util.ComponentKey;
 
 import java.util.ArrayList;
@@ -46,7 +50,7 @@
     protected AlphabeticalAppsList mApps;
     protected Callbacks mCb;
     protected ExtendedEditText mInput;
-    private String mQuery;
+    protected String mQuery;
 
     protected DefaultAppSearchAlgorithm mSearchAlgorithm;
     protected InputMethodManager mInputMethodManager;
@@ -73,6 +77,14 @@
                 mInput.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
 
         mSearchAlgorithm = onInitializeSearch();
+
+        onInitialized();
+    }
+
+    /**
+     * You can override this method to perform custom initialization.
+     */
+    protected void onInitialized() {
     }
 
     /**
@@ -117,6 +129,7 @@
         if (actionId != EditorInfo.IME_ACTION_SEARCH) {
             return false;
         }
+
         // Skip if the query is empty
         String query = v.getText().toString();
         if (query.isEmpty()) {
@@ -206,5 +219,19 @@
          * Called when the search results should be cleared.
          */
         void clearSearchResult();
+
+
+        /**
+         * Called when the app discovery is providing an update of search, which can either be
+         * START for starting a new discovery,
+         * UPDATE for providing a new search result, can be called multiple times,
+         * END for indicating the end of results.
+         *
+         * @param app result item if UPDATE, else null
+         * @param app the update state, START, UPDATE or END
+         */
+        void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
+                @NonNull AppDiscoveryUpdateState state);
     }
+
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0bfbd3e..f228470 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -17,12 +17,17 @@
 
 import android.content.Context;
 import android.os.Process;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.discovery.AppDiscoveryAppInfo;
+import com.android.launcher3.discovery.AppDiscoveryItem;
+import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.LabelComparator;
 
@@ -48,6 +53,8 @@
 
     private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
 
+    private AppDiscoveryUpdateState mAppDiscoveryUpdateState;
+
     /**
      * Info about a fast scroller section, depending if sections are merged, the fast scroller
      * sections will not be the same set as the section headers.
@@ -106,6 +113,17 @@
             return item;
         }
 
+        public static AdapterItem asDiscoveryItem(int pos, String sectionName, AppInfo appInfo,
+                int appIndex) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM;
+            item.position = pos;
+            item.sectionName = sectionName;
+            item.appInfo = appInfo;
+            item.appIndex = appIndex;
+            return item;
+        }
+
         public static AdapterItem asEmptySearch(int pos) {
             AdapterItem item = new AdapterItem();
             item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH;
@@ -134,6 +152,13 @@
             return item;
         }
 
+        public static AdapterItem asLoadingDivider(int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER;
+            item.position = pos;
+            return item;
+        }
+
         public static AdapterItem asMarketSearch(int pos) {
             AdapterItem item = new AdapterItem();
             item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET;
@@ -142,22 +167,24 @@
         }
     }
 
-    private Launcher mLauncher;
+    private final Launcher mLauncher;
 
     // The set of apps from the system not including predictions
     private final List<AppInfo> mApps = new ArrayList<>();
     private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
 
     // The set of filtered apps with the current filter
-    private List<AppInfo> mFilteredApps = new ArrayList<>();
+    private final List<AppInfo> mFilteredApps = new ArrayList<>();
     // The current set of adapter items
-    private List<AdapterItem> mAdapterItems = new ArrayList<>();
+    private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
-    private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
+    private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
     // The set of predicted app component names
-    private List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
+    private final List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
     // The set of predicted apps resolved from the component names and the current set of apps
-    private List<AppInfo> mPredictedApps = new ArrayList<>();
+    private final List<AppInfo> mPredictedApps = new ArrayList<>();
+    private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>();
+
     // The of ordered component names as a result of a search query
     private ArrayList<ComponentKey> mSearchResults;
     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
@@ -240,6 +267,10 @@
         return (mSearchResults != null) && mFilteredApps.isEmpty();
     }
 
+    boolean shouldShowEmptySearch() {
+        return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty();
+    }
+
     /**
      * Sets the sorted list of filtered components.
      */
@@ -253,6 +284,20 @@
         return false;
     }
 
+    public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
+                @NonNull AppDiscoveryUpdateState state) {
+        mAppDiscoveryUpdateState = state;
+        switch (state) {
+            case START:
+                mDiscoveredApps.clear();
+                break;
+            case UPDATE:
+                mDiscoveredApps.add(new AppDiscoveryAppInfo(app, mLauncher));
+                break;
+        }
+        updateAdapterItems();
+    }
+
     /**
      * Sets the current set of predicted apps.  Since this can be called before we get the full set
      * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
@@ -350,6 +395,17 @@
      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
      */
     private void updateAdapterItems() {
+        refillAdapterItems();
+        refreshRecyclerView();
+    }
+
+    private void refreshRecyclerView() {
+        if (mAdapter != null) {
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    private void refillAdapterItems() {
         String lastSectionName = null;
         FastScrollSectionInfo lastFastScrollerSectionInfo = null;
         int position = 0;
@@ -435,14 +491,30 @@
             mFilteredApps.add(info);
         }
 
-        // Append the search market item if we are currently searching
         if (hasFilter()) {
-            if (hasNoFilteredResults()) {
-                mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+            if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) {
+                mAdapterItems.add(AdapterItem.asLoadingDivider(position++));
+
+                // Append all app discovery results
+                for (int i = 0; i < mDiscoveredApps.size(); i++) {
+                    AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i);
+                    AdapterItem item = AdapterItem.asDiscoveryItem(position++,
+                            "", appDiscoveryAppInfo, appIndex++);
+                    mAdapterItems.add(item);
+                }
+
+                if (!isAppDiscoveryRunning()) {
+                    mAdapterItems.add(AdapterItem.asMarketSearch(position++));
+                }
             } else {
-                mAdapterItems.add(AdapterItem.asMarketDivider(position++));
+                // Append the search market item
+                if (hasNoFilteredResults()) {
+                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+                } else {
+                    mAdapterItems.add(AdapterItem.asMarketDivider(position++));
+                }
+                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
             }
-            mAdapterItems.add(AdapterItem.asMarketSearch(position++));
         }
 
         if (mNumAppsPerRow != 0) {
@@ -498,11 +570,11 @@
                     break;
             }
         }
+    }
 
-        // Refresh the recycler view
-        if (mAdapter != null) {
-            mAdapter.notifyDataSetChanged();
-        }
+    public boolean isAppDiscoveryRunning() {
+        return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START
+                || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE;
     }
 
     private List<AppInfo> getFiltersAppInfos() {
@@ -532,4 +604,5 @@
         }
         return sectionName;
     }
+
 }
diff --git a/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java b/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java
new file mode 100644
index 0000000..50e979a
--- /dev/null
+++ b/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java
@@ -0,0 +1,88 @@
+/*
+ * 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.discovery;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+
+public class AppDiscoveryAppInfo extends AppInfo {
+
+    private final @NonNull Launcher mLauncher;
+
+    public final boolean showAsDiscoveryItem;
+    public final boolean isInstantApp;
+    public final float rating;
+    public final long reviewCount;
+    public final @NonNull String publisher;
+    public final @NonNull Intent installIntent;
+    public final @NonNull Intent launchIntent;
+    public final @Nullable String priceFormatted;
+
+    public AppDiscoveryAppInfo(AppDiscoveryItem item, Launcher launcher) {
+        this.mLauncher = launcher;
+        this.intent = item.isInstantApp ? item.launchIntent : item.installIntent;
+        this.title = item.title;
+        this.iconBitmap = item.bitmap;
+        this.isDisabled = ShortcutInfo.DEFAULT;
+        this.usingLowResIcon = false;
+        this.isInstantApp = item.isInstantApp;
+        this.rating = item.starRating;
+        this.showAsDiscoveryItem = true;
+        this.publisher = item.publisher != null ? item.publisher : "";
+        this.priceFormatted = item.price;
+        this.componentName = new ComponentName(item.packageName, "");
+        this.installIntent = item.installIntent;
+        this.launchIntent = item.launchIntent;
+        this.reviewCount = item.reviewCount;
+        this.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+    }
+
+    @Override
+    public ShortcutInfo makeShortcut() {
+        if (!isDragAndDropSupported()) {
+            throw new RuntimeException("DnD is currently not supported for discovered store apps");
+        }
+        ShortcutInfo shortcutInfo = super.makeShortcut();
+        if (isInstantApp) {
+            int iconSize = iconBitmap.getWidth();
+            int badgeSize = mLauncher.getResources().getDimensionPixelOffset(R.dimen.badge_size);
+            Bitmap icon = Bitmap.createBitmap(iconBitmap);
+            Drawable badgeDrawable = mLauncher.getDrawable(R.drawable.ic_instant_app);
+            badgeDrawable.setBounds(iconSize - badgeSize, iconSize - badgeSize, iconSize, iconSize);
+            Canvas canvas = new Canvas(icon);
+            badgeDrawable.draw(canvas);
+            shortcutInfo.iconBitmap = icon;
+        }
+        return shortcutInfo;
+    }
+
+    public boolean isDragAndDropSupported() {
+        return isInstantApp;
+    }
+
+}
diff --git a/src/com/android/launcher3/discovery/AppDiscoveryItem.java b/src/com/android/launcher3/discovery/AppDiscoveryItem.java
new file mode 100644
index 0000000..7c10371
--- /dev/null
+++ b/src/com/android/launcher3/discovery/AppDiscoveryItem.java
@@ -0,0 +1,62 @@
+/*
+ * 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.discovery;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+
+/**
+ * This class represents the model for a discovered app via app discovery.
+ * It holds all information for one result retrieved from an app discovery service.
+ */
+public class AppDiscoveryItem {
+
+    public final String packageName;
+    public final boolean isInstantApp;
+    public final float starRating;
+    public final long reviewCount;
+    public final Intent launchIntent;
+    public final Intent installIntent;
+    public final CharSequence title;
+    public final String publisher;
+    public final String price;
+    public final Bitmap bitmap;
+
+    public AppDiscoveryItem(String packageName,
+                            boolean isInstantApp,
+                            float starRating,
+                            long reviewCount,
+                            CharSequence title,
+                            String publisher,
+                            Bitmap bitmap,
+                            String price,
+                            Intent launchIntent,
+                            Intent installIntent) {
+        this.packageName = packageName;
+        this.isInstantApp = isInstantApp;
+        this.starRating = starRating;
+        this.reviewCount = reviewCount;
+        this.launchIntent = launchIntent;
+        this.installIntent = installIntent;
+        this.title = title;
+        this.publisher = publisher;
+        this.price = price;
+        this.bitmap = bitmap;
+    }
+
+}
diff --git a/src/com/android/launcher3/discovery/AppDiscoveryItemView.java b/src/com/android/launcher3/discovery/AppDiscoveryItemView.java
new file mode 100644
index 0000000..6faad87
--- /dev/null
+++ b/src/com/android/launcher3/discovery/AppDiscoveryItemView.java
@@ -0,0 +1,100 @@
+/*
+ * 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.discovery;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+public class AppDiscoveryItemView extends RelativeLayout {
+
+    private static boolean SHOW_REVIEW_COUNT = false;
+
+    private ImageView mImage;
+    private ImageView mBadge;
+    private TextView mTitle;
+    private TextView mRatingText;
+    private RatingView mRatingView;
+    private TextView mReviewCount;
+    private TextView mPrice;
+    private OnLongClickListener mOnLongClickListener;
+
+    public AppDiscoveryItemView(Context context) {
+        this(context, null);
+    }
+
+    public AppDiscoveryItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppDiscoveryItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        this.mImage = (ImageView) findViewById(R.id.image);
+        this.mBadge = (ImageView) findViewById(R.id.badge);
+        this.mTitle = (TextView) findViewById(R.id.title);
+        this.mRatingText = (TextView) findViewById(R.id.rating);
+        this.mRatingView = (RatingView) findViewById(R.id.rating_view);
+        this.mPrice = (TextView) findViewById(R.id.price);
+        this.mReviewCount = (TextView) findViewById(R.id.review_count);
+    }
+
+    public void init(OnClickListener clickListener,
+                     AccessibilityDelegate accessibilityDelegate,
+                     OnLongClickListener onLongClickListener) {
+        setOnClickListener(clickListener);
+        mImage.setOnClickListener(clickListener);
+        setAccessibilityDelegate(accessibilityDelegate);
+        mOnLongClickListener = onLongClickListener;
+    }
+
+    public void apply(@NonNull AppDiscoveryAppInfo info) {
+        setTag(info);
+        mImage.setTag(info);
+        mImage.setImageBitmap(info.iconBitmap);
+        mImage.setOnLongClickListener(info.isDragAndDropSupported() ? mOnLongClickListener : null);
+        mBadge.setVisibility(info.isInstantApp ? View.VISIBLE : View.GONE);
+        mTitle.setText(info.title);
+        mPrice.setText(info.priceFormatted != null ? info.priceFormatted : "");
+        mReviewCount.setVisibility(SHOW_REVIEW_COUNT ? View.VISIBLE : View.GONE);
+        if (info.rating >= 0) {
+            mRatingText.setText(new DecimalFormat("#.#").format(info.rating));
+            mRatingView.setRating(info.rating);
+            mRatingView.setVisibility(View.VISIBLE);
+            String reviewCountFormatted = NumberFormat.getInstance().format(info.reviewCount);
+            mReviewCount.setText("(" + reviewCountFormatted + ")");
+        } else {
+            // if we don't have a rating
+            mRatingView.setVisibility(View.GONE);
+            mRatingText.setText("");
+            mReviewCount.setText("");
+        }
+    }
+}
diff --git a/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java b/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java
new file mode 100644
index 0000000..0700a10
--- /dev/null
+++ b/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java
@@ -0,0 +1,21 @@
+/*
+ * 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.discovery;
+
+public enum AppDiscoveryUpdateState {
+    START, UPDATE, END
+}
diff --git a/src/com/android/launcher3/discovery/RatingView.java b/src/com/android/launcher3/discovery/RatingView.java
new file mode 100644
index 0000000..8fe63d6
--- /dev/null
+++ b/src/com/android/launcher3/discovery/RatingView.java
@@ -0,0 +1,94 @@
+/*
+ * 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.discovery;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+
+import com.android.launcher3.R;
+
+/**
+ * A simple rating view that shows stars with a rating from 0-5.
+ */
+public class RatingView extends View {
+
+    private static final float WIDTH_FACTOR = 0.9f;
+    private static final int MAX_LEVEL = 10000;
+    private static final int MAX_STARS = 5;
+
+    private final Drawable mStarDrawable;
+    private final int mColorGray;
+    private final int mColorHighlight;
+
+    private float rating;
+
+    public RatingView(Context context) {
+        this(context, null);
+    }
+
+    public RatingView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RatingView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mStarDrawable = getResources().getDrawable(R.drawable.ic_star_rating, null);
+        mColorGray = 0x1E000000;
+        mColorHighlight = 0x8A000000;
+    }
+
+    public void setRating(float rating) {
+        this.rating = Math.min(Math.max(rating, 0), MAX_STARS);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        drawStars(canvas, MAX_STARS, mColorGray);
+        drawStars(canvas, rating, mColorHighlight);
+    }
+
+    private void drawStars(Canvas canvas, float stars, int color) {
+        int fullWidth = getLayoutParams().width;
+        int cellWidth = fullWidth / MAX_STARS;
+        int starWidth = (int) (cellWidth * WIDTH_FACTOR);
+        int padding = cellWidth - starWidth;
+        int fullStars = (int) stars;
+        float partialStarFactor = stars - fullStars;
+
+        for (int i = 0; i < fullStars; i++) {
+            int x = i * cellWidth + padding;
+            Drawable star = mStarDrawable.getConstantState().newDrawable().mutate();
+            star.setTint(color);
+            star.setBounds(x, padding, x + starWidth, padding + starWidth);
+            star.draw(canvas);
+        }
+        if (partialStarFactor > 0f) {
+            int x = fullStars * cellWidth + padding;
+            ClipDrawable star = new ClipDrawable(mStarDrawable,
+                    Gravity.LEFT, ClipDrawable.HORIZONTAL);
+            star.setTint(color);
+            star.setLevel((int) (MAX_LEVEL * partialStarFactor));
+            star.setBounds(x, padding, x + starWidth, padding + starWidth);
+            star.draw(canvas);
+        }
+    }
+}
diff --git a/src_config/com/android/launcher3/config/FeatureFlags.java b/src_config/com/android/launcher3/config/FeatureFlags.java
index 358a678..80eebec 100644
--- a/src_config/com/android/launcher3/config/FeatureFlags.java
+++ b/src_config/com/android/launcher3/config/FeatureFlags.java
@@ -49,4 +49,6 @@
     public static final boolean LEGACY_ICON_TREATMENT = false;
     // When enabled, adaptive icons would have shadows baked when being stored to icon cache.
     public static final boolean ADAPTIVE_ICON_SHADOW = true;
+    // When enabled, app discovery will be enabled if service is implemented
+    public static final boolean DISCOVERY_ENABLED = false;
 }