Merge "Use custom drawable for Recents Go thumbnails (2/2)" into ub-launcher3-qt-dev
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
index 003ee86..be76d53 100644
--- a/go/quickstep/res/layout/clear_all_button.xml
+++ b/go/quickstep/res/layout/clear_all_button.xml
@@ -23,7 +23,9 @@
         android:id="@+id/clear_all_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:layout_marginVertical="16dp"
         android:layout_gravity="center_horizontal"
+        android:paddingHorizontal="32dp"
         android:background="@drawable/clear_all_button"
         android:gravity="center"
         android:text="@string/recents_clear_all"
diff --git a/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
new file mode 100644
index 0000000..52cc6fc
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
@@ -0,0 +1,19 @@
+<!--
+    Copyright (C) 2018 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <solid android:color="?android:attr/colorAccent" />
+    <corners android:radius="8dp" />
+</shape>
diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
new file mode 100644
index 0000000..b0f2b4b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingStart="24dp"
+        android:paddingEnd="4dp"
+        android:background="@drawable/arrow_toast_rounded_background"
+        android:layout_gravity="center_horizontal"
+        android:elevation="2dp"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/text"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:layout_gravity="center_vertical"
+            android:textColor="@android:color/white"
+            android:textSize="16sp"/>
+        <ImageView
+            android:id="@+id/dismiss"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:layout_gravity="center_vertical"
+            android:padding="10dp"
+            android:layout_marginStart="2dp"
+            android:layout_marginEnd="2dp"
+            android:alpha="0.7"
+            android:src="@drawable/ic_remove_no_shadow"
+            android:tint="@android:color/white"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/accessibility_close_task"/>
+    </LinearLayout>
+
+    <View
+        android:id="@+id/arrow"
+        android:elevation="2dp"
+        android:layout_width="10dp"
+        android:layout_height="8dp"
+        android:layout_marginTop="-2dp"
+        android:layout_gravity="center_horizontal"/>
+</merge>
diff --git a/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
new file mode 100644
index 0000000..b21c34b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <com.android.launcher3.appprediction.PredictionRowView
+        android:id="@+id/prediction_row"
+        android:accessibilityPaneTitle="@string/title_app_suggestions"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <com.android.launcher3.appprediction.AppsDividerView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/apps_divider_view" />
+</merge>
diff --git a/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml
new file mode 100644
index 0000000..20c4004
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
+    style="?android:attr/progressBarStyleHorizontal"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:layout_marginLeft="20dp"
+    android:layout_marginRight="20dp"
+    android:indeterminate="true"
+    android:indeterminateOnly="true"
+    android:indeterminateTint="?workspaceTextColor" />
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 1e8d0cc..7426e30 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <color name="chip_hint_foreground_color">#fff</color>
+
+    <color name="all_apps_label_text">#61000000</color>
+    <color name="all_apps_label_text_dark">#61FFFFFF</color>
+    <color name="all_apps_prediction_row_separator">#3c000000</color>
+    <color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index b654d5c..f991435 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -12,4 +12,13 @@
     <dimen name="chip_text_top_padding">4dp</dimen>
     <dimen name="chip_text_start_padding">10dp</dimen>
     <dimen name="chip_text_size">14sp</dimen>
+
+    <dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
+    <dimen name="all_apps_label_top_padding">16dp</dimen>
+    <dimen name="all_apps_label_bottom_padding">8dp</dimen>
+    <dimen name="all_apps_label_text_size">14sp</dimen>
+    <dimen name="all_apps_tip_bottom_margin">8dp</dimen>
+    <!-- The size of corner radius of the arrow in the arrow toast. -->
+    <dimen name="arrow_toast_corner_radius">2dp</dimen>
+
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index c60cf5a..1ddd3f5 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -21,6 +21,8 @@
 
   <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
 
+  <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
+
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
 </resources>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
new file mode 100644
index 0000000..c5c4add
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 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;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import java.util.function.BiPredicate;
+
+public class LauncherInitListenerEx extends LauncherInitListener {
+
+    public LauncherInitListenerEx(BiPredicate<Launcher, Boolean> onInitListener) {
+        super(onInitListener);
+    }
+
+    @Override
+    protected boolean init(Launcher launcher, boolean alreadyOnHome) {
+        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
+        return super.init(launcher, alreadyOnHome);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
new file mode 100644
index 0000000..948f39e
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+import com.android.systemui.shared.system.LauncherEventUtil;
+
+import androidx.core.content.ContextCompat;
+
+/**
+ * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the
+ * first time.
+ */
+public class AllAppsTipView extends AbstractFloatingView {
+
+    private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
+    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
+    private static final long SHOW_DELAY_MS = 200;
+    private static final long SHOW_DURATION_MS = 300;
+    private static final long HIDE_DURATION_MS = 100;
+
+    private final Launcher mLauncher;
+    private final Handler mHandler = new Handler();
+
+    private AllAppsTipView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setOrientation(LinearLayout.VERTICAL);
+
+        mLauncher = Launcher.getLauncher(context);
+
+        init(context);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            close(true);
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (mIsOpen) {
+            if (animate) {
+                animate().alpha(0f)
+                        .withLayer()
+                        .setStartDelay(0)
+                        .setDuration(HIDE_DURATION_MS)
+                        .setInterpolator(Interpolators.ACCEL)
+                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
+                        .start();
+            } else {
+                animate().cancel();
+                mLauncher.getDragLayer().removeView(this);
+            }
+            mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
+            mIsOpen = false;
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    private void init(Context context) {
+        inflate(context, R.layout.arrow_toast, this);
+
+        TextView textView = findViewById(R.id.text);
+        textView.setText(R.string.all_apps_prediction_tip);
+
+        View dismissButton = findViewById(R.id.dismiss);
+        dismissButton.setOnClickListener(view -> {
+            mLauncher.getUserEventDispatcher().logActionTip(
+                    LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS);
+            handleClose(true);
+        });
+
+        View arrowView = findViewById(R.id.arrow);
+        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
+        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                arrowLp.width, arrowLp.height, false));
+        Paint arrowPaint = arrowDrawable.getPaint();
+        TypedValue typedValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
+        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+        arrowPaint.setPathEffect(new CornerPathEffect(
+                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
+        arrowView.setBackground(arrowDrawable);
+
+        mIsOpen = true;
+
+        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
+        FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
+        if (!floatingHeaderView.hasVisibleContent()
+                || AbstractFloatingView.getOpenView(launcher,
+                TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE) != null
+                || !launcher.isInState(ALL_APPS)
+                || hasSeenAllAppsTip(launcher)
+                || UserManagerCompat.getInstance(launcher).isDemoUser()
+                || ActivityManager.isRunningInTestHarness()) {
+            return false;
+        }
+
+        AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
+            null);
+        launcher.getDragLayer().addView(allAppsTipView);
+
+        DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
+        params.gravity = Gravity.CENTER_HORIZONTAL;
+
+        int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
+        allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
+                R.dimen.all_apps_tip_bottom_margin));
+
+        allAppsTipView.setAlpha(0);
+        allAppsTipView.animate()
+                .alpha(1f)
+                .withLayer()
+                .setStartDelay(SHOW_DELAY_MS)
+                .setDuration(SHOW_DURATION_MS)
+                .setInterpolator(Interpolators.DEACCEL)
+                .start();
+
+        launcher.getUserEventDispatcher().logActionTip(
+                LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
+        return true;
+    }
+
+    private static boolean hasSeenAllAppsTip(Launcher launcher) {
+        return launcher.getSharedPrefs().getBoolean(ALL_APPS_TIP_SEEN, false);
+    }
+
+    public static void scheduleShowIfNeeded(Launcher launcher) {
+        if (!hasSeenAllAppsTip(launcher)) {
+            launcher.getStateManager().addStateListener(
+                    new LauncherStateManager.StateListener() {
+                        @Override
+                        public void onStateTransitionStart(LauncherState toState) {
+                        }
+
+                        @Override
+                        public void onStateTransitionComplete(LauncherState finalState) {
+                            if (finalState == ALL_APPS) {
+                                if (showAllAppsTipIfNecessary(launcher)) {
+                                    launcher.getStateManager().removeStateListener(this);
+                                }
+                            }
+                        }
+                    });
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
new file mode 100644
index 0000000..311db21
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -0,0 +1,308 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.util.Themes;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+/**
+ * A view which shows a horizontal divider
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class AppsDividerView extends View implements LauncherStateManager.StateListener,
+        FloatingHeaderRow {
+
+    private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
+    private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
+
+    public enum DividerType {
+        NONE,
+        LINE,
+        ALL_APPS_LABEL
+    }
+
+    private final Launcher mLauncher;
+    private final TextPaint mPaint = new TextPaint();
+    private DividerType mDividerType = DividerType.NONE;
+
+    private final @ColorInt int mStrokeColor;
+    private final @ColorInt int mAllAppsLabelTextColor;
+
+    private Layout mAllAppsLabelLayout;
+    private boolean mShowAllAppsLabel;
+
+    private FloatingHeaderView mParent;
+    private boolean mTabsHidden;
+    private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
+
+    private boolean mIsScrolledOut = false;
+
+    public AppsDividerView(Context context) {
+        this(context, null);
+    }
+
+    public AppsDividerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+
+        boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
+        mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
+
+        mStrokeColor = ContextCompat.getColor(context, isMainColorDark
+                ? R.color.all_apps_prediction_row_separator_dark
+                : R.color.all_apps_prediction_row_separator);
+
+        mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
+                ? R.color.all_apps_label_text_dark
+                : R.color.all_apps_label_text);
+    }
+
+    public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+        mParent = parent;
+        mTabsHidden = tabsHidden;
+        mRows = rows;
+        updateDividerType();
+    }
+
+    @Override
+    public int getExpectedHeight() {
+        return getPaddingTop() + getPaddingBottom();
+    }
+
+    @Override
+    public boolean shouldDraw() {
+        return mDividerType != DividerType.NONE;
+    }
+
+    @Override
+    public boolean hasVisibleContent() {
+        return false;
+    }
+
+    private void updateDividerType() {
+        final DividerType dividerType;
+        if (!mTabsHidden) {
+            dividerType = DividerType.NONE;
+        } else {
+            // Check how many sections above me.
+            int sectionCount = 0;
+            for (FloatingHeaderRow row : mRows) {
+                if (row == this) {
+                    break;
+                } else if (row.shouldDraw()) {
+                    sectionCount ++;
+                }
+            }
+
+            if (mShowAllAppsLabel && sectionCount > 0) {
+                dividerType = DividerType.ALL_APPS_LABEL;
+            } else if (sectionCount == 1) {
+                dividerType = DividerType.LINE;
+            } else {
+                dividerType = DividerType.NONE;
+            }
+        }
+
+        if (mDividerType != dividerType) {
+            mDividerType = dividerType;
+            int topPadding;
+            int bottomPadding;
+            switch (dividerType) {
+                case LINE:
+                    topPadding = 0;
+                    bottomPadding = getResources()
+                            .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+                    mPaint.setColor(mStrokeColor);
+                    break;
+                case ALL_APPS_LABEL:
+                    topPadding = getAllAppsLabelLayout().getHeight() + getResources()
+                            .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
+                    bottomPadding = getResources()
+                            .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
+                    mPaint.setColor(mAllAppsLabelTextColor);
+                    break;
+                case NONE:
+                default:
+                    topPadding = bottomPadding = 0;
+                    break;
+            }
+            setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
+            updateViewVisibility();
+            invalidate();
+            requestLayout();
+            if (mParent != null) {
+                mParent.onHeightUpdated();
+            }
+        }
+    }
+
+    private void updateViewVisibility() {
+        setVisibility(mDividerType == DividerType.NONE
+                ? GONE
+                : (mIsScrolledOut ? INVISIBLE : VISIBLE));
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mDividerType == DividerType.LINE) {
+            int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+            int y = getHeight() - (getPaddingBottom() / 2);
+            int x1 = getPaddingLeft() + side;
+            int x2 = getWidth() - getPaddingRight() - side;
+            canvas.drawLine(x1, y, x2, y, mPaint);
+        } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
+            Layout textLayout = getAllAppsLabelLayout();
+            int x = getWidth() / 2 - textLayout.getWidth() / 2;
+            int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
+            canvas.translate(x, y);
+            textLayout.draw(canvas);
+            canvas.translate(-x, -y);
+        }
+    }
+
+    private Layout getAllAppsLabelLayout() {
+        if (mAllAppsLabelLayout == null) {
+            mPaint.setAntiAlias(true);
+            mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
+            mPaint.setTextSize(
+                    getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
+
+            CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
+            mAllAppsLabelLayout = StaticLayout.Builder.obtain(
+                    allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
+                    Math.round(mPaint.measureText(allAppsLabelText.toString())))
+                    .setAlignment(Layout.Alignment.ALIGN_CENTER)
+                    .setMaxLines(1)
+                    .setIncludePad(true)
+                    .build();
+        }
+        return mAllAppsLabelLayout;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+                getPaddingBottom() + getPaddingTop());
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (shouldShowAllAppsLabel()) {
+            mShowAllAppsLabel = true;
+            mLauncher.getStateManager().addStateListener(this);
+            updateDividerType();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mLauncher.getStateManager().removeStateListener(this);
+    }
+
+    @Override
+    public void onStateTransitionStart(LauncherState toState) { }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        if (finalState == ALL_APPS) {
+            setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
+        } else {
+            if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
+                mShowAllAppsLabel = !mShowAllAppsLabel;
+                updateDividerType();
+            }
+
+            if (!mShowAllAppsLabel) {
+                mLauncher.getStateManager().removeStateListener(this);
+            }
+        }
+    }
+
+    private void setAllAppsVisitedCount(int count) {
+        mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
+    }
+
+    private int getAllAppsVisitedCount() {
+        return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
+    }
+
+    private boolean shouldShowAllAppsLabel() {
+        return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
+    }
+
+    @Override
+    public void setInsets(Rect insets, DeviceProfile grid) {
+        int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+                + grid.cellLayoutPaddingLeftRightPx;
+        setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+    }
+
+    @Override
+    public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+            PropertySetter setter, Interpolator fadeInterpolator) {
+        // Don't use setViewAlpha as we want to control the visibility ourselves.
+        setter.setFloat(this, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+    }
+
+    @Override
+    public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+        setTranslationY(scroll);
+        mIsScrolledOut = isScrolledOut;
+        updateViewVisibility();
+    }
+
+    @Override
+    public Class<AppsDividerView> getTypeClass() {
+        return AppsDividerView.class;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
new file mode 100644
index 0000000..b9f4147
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.Context;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+
+public class ComponentKeyMapper {
+
+    protected final ComponentKey componentKey;
+    private final Context mContext;
+    private final DynamicItemCache mCache;
+
+    public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
+        mContext = context;
+        componentKey = key;
+        mCache = cache;
+    }
+
+    public String getPackage() {
+        return componentKey.componentName.getPackageName();
+    }
+
+    public String getComponentClass() {
+        return componentKey.componentName.getClassName();
+    }
+
+    public ComponentKey getComponentKey() {
+        return componentKey;
+    }
+
+    @Override
+    public String toString() {
+        return componentKey.toString();
+    }
+
+    public ItemInfoWithIcon getApp(AllAppsStore store) {
+        AppInfo item = store.getApp(componentKey);
+        if (item != null) {
+            return item;
+        } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
+            return mCache.getInstantApp(componentKey.componentName.getPackageName());
+        } else if (componentKey instanceof ShortcutKey) {
+            return mCache.getShortcutInfo((ShortcutKey) componentKey);
+        }
+        return null;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
new file mode 100644
index 0000000..4ecc39c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static android.content.pm.PackageManager.MATCH_INSTANT;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.InstantAppResolver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Utility class which loads and caches predicted items like instant apps and shortcuts, before
+ * they can be displayed on the UI
+ */
+public class DynamicItemCache {
+
+    private static final String TAG = "DynamicItemCache";
+    private static final boolean DEBUG = false;
+    private static final String DEFAULT_URL = "default-url";
+
+    private static final int BG_MSG_LOAD_SHORTCUTS = 1;
+    private static final int BG_MSG_LOAD_INSTANT_APPS = 2;
+
+    private static final int UI_MSG_UPDATE_SHORTCUTS = 1;
+    private static final int UI_MSG_UPDATE_INSTANT_APPS = 2;
+
+    private final Context mContext;
+    private final Handler mWorker;
+    private final Handler mUiHandler;
+    private final InstantAppResolver mInstantAppResolver;
+    private final Runnable mOnUpdateCallback;
+
+    private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
+    private final Map<String, InstantAppItemInfo> mInstantApps;
+
+    public DynamicItemCache(Context context, Runnable onUpdateCallback) {
+        mContext = context;
+        mWorker = new Handler(LauncherModel.getWorkerLooper(), this::handleWorkerMessage);
+        mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
+        mInstantAppResolver = InstantAppResolver.newInstance(context);
+        mOnUpdateCallback = onUpdateCallback;
+
+        mShortcuts = new HashMap<>();
+        mInstantApps = new HashMap<>();
+    }
+
+    public void cacheItems(List<ShortcutKey> shortcutKeys, List<String> pkgNames) {
+        if (!shortcutKeys.isEmpty()) {
+            mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS);
+            Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget();
+        }
+        if (!pkgNames.isEmpty()) {
+            mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS);
+            Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget();
+        }
+    }
+
+    private boolean handleWorkerMessage(Message msg) {
+        switch (msg.what) {
+            case BG_MSG_LOAD_SHORTCUTS: {
+                List<ShortcutKey> shortcutKeys = msg.obj != null ?
+                        (List<ShortcutKey>) msg.obj : Collections.EMPTY_LIST;
+                Map<ShortcutKey, WorkspaceItemInfo> shortcutKeyAndInfos = new ArrayMap<>();
+                for (ShortcutKey shortcutKey : shortcutKeys) {
+                    WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey);
+                    if (workspaceItemInfo != null) {
+                        shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo);
+                    }
+                }
+                Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos)
+                        .sendToTarget();
+                return true;
+            }
+            case BG_MSG_LOAD_INSTANT_APPS: {
+                List<String> pkgNames = msg.obj != null ?
+                        (List<String>) msg.obj : Collections.EMPTY_LIST;
+                List<InstantAppItemInfo> instantAppItemInfos = new ArrayList<>();
+                for (String pkgName : pkgNames) {
+                    InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName);
+                    if (instantAppItemInfo != null) {
+                        instantAppItemInfos.add(instantAppItemInfo);
+                    }
+                }
+                Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos)
+                        .sendToTarget();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean handleUiMessage(Message msg) {
+        switch (msg.what) {
+            case UI_MSG_UPDATE_SHORTCUTS: {
+                mShortcuts.clear();
+                mShortcuts.putAll((Map<ShortcutKey, WorkspaceItemInfo>) msg.obj);
+                mOnUpdateCallback.run();
+                return true;
+            }
+            case UI_MSG_UPDATE_INSTANT_APPS: {
+                List<InstantAppItemInfo> instantAppItemInfos = (List<InstantAppItemInfo>) msg.obj;
+                mInstantApps.clear();
+                for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) {
+                    mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(),
+                            instantAppItemInfo);
+                }
+                mOnUpdateCallback.run();
+                if (DEBUG) {
+                    Log.d(TAG, String.format("Cache size: %d, Cache: %s",
+                            mInstantApps.size(), mInstantApps.toString()));
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @WorkerThread
+    private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
+        DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
+        List<ShortcutInfo> details = mgr.queryForFullDetails(
+                shortcutKey.componentName.getPackageName(),
+                Collections.<String>singletonList(shortcutKey.getId()),
+                shortcutKey.user);
+        if (!details.isEmpty()) {
+            WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
+            try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
+                si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
+            } catch (Exception e) {
+                if (DEBUG) {
+                    Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
+                }
+                return null;
+            }
+            return si;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "No shortcut found: " + shortcutKey.toString());
+        }
+        return null;
+    }
+
+    private InstantAppItemInfo loadInstantApp(String pkgName) {
+        PackageManager pm = mContext.getPackageManager();
+
+        try {
+            ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
+            if (!mInstantAppResolver.isInstantApp(ai)) {
+                return null;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+
+        String url = retrieveDefaultUrl(pkgName, pm);
+        if (url == null) {
+            Log.w(TAG, "no default-url available for pkg " + pkgName);
+            return null;
+        }
+
+        Intent intent = new Intent(Intent.ACTION_VIEW)
+                .addCategory(Intent.CATEGORY_BROWSABLE)
+                .setData(Uri.parse(url));
+        InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
+        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+        iconCache.getTitleAndIcon(info, false);
+        if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
+            return null;
+        }
+        return info;
+    }
+
+    @Nullable
+    public static String retrieveDefaultUrl(String pkgName, PackageManager pm) {
+        Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName);
+        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
+                mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA);
+        String url = null;
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            if (resolveInfo.activityInfo.metaData != null
+                    && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) {
+                url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL);
+            }
+        }
+        return url;
+    }
+
+    @UiThread
+    public InstantAppItemInfo getInstantApp(String pkgName) {
+        return mInstantApps.get(pkgName);
+    }
+
+    @MainThread
+    public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+        return mShortcuts.get(key);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
new file mode 100644
index 0000000..6e5f461
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.WorkspaceItemInfo;
+
+public class InstantAppItemInfo extends AppInfo {
+
+    public InstantAppItemInfo(Intent intent, String packageName) {
+        this.intent = intent;
+        this.componentName = new ComponentName(packageName, COMPONENT_CLASS_MARKER);
+    }
+
+    @Override
+    public ComponentName getTargetComponent() {
+        return componentName;
+    }
+
+    @Override
+    public WorkspaceItemInfo makeWorkspaceItem() {
+        WorkspaceItemInfo workspaceItemInfo = super.makeWorkspaceItem();
+        workspaceItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        workspaceItemInfo.status = WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON
+                | WorkspaceItemInfo.FLAG_RESTORE_STARTED
+                | WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI;
+        workspaceItemInfo.intent.setPackage(componentName.getPackageName());
+        return workspaceItemInfo;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
new file mode 100644
index 0000000..bd78573
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.launcher3.appprediction.PredictionUiStateManager.KEY_APP_SUGGESTION;
+
+import android.annotation.TargetApi;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.util.UiThreadHelper;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class PredictionAppTracker extends AppLaunchTracker
+        implements OnSharedPreferenceChangeListener {
+
+    private static final String TAG = "PredictionAppTracker";
+    private static final boolean DBG = false;
+
+    private static final int MSG_INIT = 0;
+    private static final int MSG_DESTROY = 1;
+    private static final int MSG_LAUNCH = 2;
+    private static final int MSG_PREDICT = 3;
+
+    private final Context mContext;
+    private final Handler mMessageHandler;
+
+    private boolean mEnabled;
+
+    // Accessed only on worker thread
+    private AppPredictor mHomeAppPredictor;
+    private AppPredictor mRecentsOverviewPredictor;
+
+    public PredictionAppTracker(Context context) {
+        mContext = context;
+        mMessageHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleMessage);
+
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true));
+        prefs.registerOnSharedPreferenceChangeListener(this);
+        InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
+    }
+
+    @UiThread
+    private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        // Reinitialize everything
+        setEnabled(mEnabled);
+    }
+
+    @Override
+    @UiThread
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        if (KEY_APP_SUGGESTION.equals(key)) {
+            setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true));
+        }
+    }
+
+    @WorkerThread
+    private void destroy() {
+        if (mHomeAppPredictor != null) {
+            mHomeAppPredictor.destroy();
+            mHomeAppPredictor = null;
+        }
+        if (mRecentsOverviewPredictor != null) {
+            mRecentsOverviewPredictor.destroy();
+            mRecentsOverviewPredictor = null;
+        }
+    }
+
+    @WorkerThread
+    private AppPredictor createPredictor(Client client, int count) {
+        AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
+
+        AppPredictor predictor = apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(mContext)
+                        .setUiSurface(client.id)
+                        .setPredictedTargetCount(count)
+                        .build());
+        predictor.registerPredictionUpdates(mContext.getMainExecutor(),
+                PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
+        predictor.requestPredictionUpdate();
+        return predictor;
+    }
+
+    @WorkerThread
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_INIT: {
+                // Destroy any existing clients
+                destroy();
+
+                // Initialize the clients
+                int count = InvariantDeviceProfile.INSTANCE.get(mContext).numColumns;
+                mHomeAppPredictor = createPredictor(Client.HOME, count);
+                mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
+                return true;
+            }
+            case MSG_DESTROY: {
+                destroy();
+                return true;
+            }
+            case MSG_LAUNCH: {
+                if (mEnabled && mHomeAppPredictor != null) {
+                    mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
+                }
+                return true;
+            }
+            case MSG_PREDICT: {
+                if (mEnabled && mHomeAppPredictor != null) {
+                    String client = (String) msg.obj;
+                    if (Client.HOME.id.equals(client)) {
+                        mHomeAppPredictor.requestPredictionUpdate();
+                    } else {
+                        mRecentsOverviewPredictor.requestPredictionUpdate();
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    @UiThread
+    public void onReturnedToHome() {
+        String client = Client.HOME.id;
+        mMessageHandler.removeMessages(MSG_PREDICT, client);
+        Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
+        if (DBG) {
+            Log.d(TAG, String.format("Sent immediate message to update %s", client));
+        }
+    }
+
+    @UiThread
+    public void setEnabled(boolean isEnabled) {
+        mEnabled = isEnabled;
+        if (isEnabled) {
+            mMessageHandler.removeMessages(MSG_DESTROY);
+            mMessageHandler.sendEmptyMessage(MSG_INIT);
+        } else {
+            mMessageHandler.removeMessages(MSG_INIT);
+            mMessageHandler.sendEmptyMessage(MSG_DESTROY);
+        }
+    }
+
+    @Override
+    @UiThread
+    public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
+            String container) {
+        // TODO: Use the full shortcut info
+        AppTarget target = new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutId))
+                .setTarget(packageName, user)
+                .setClassName(shortcutId)
+                .build();
+        sendLaunch(target, container);
+    }
+
+    @Override
+    @UiThread
+    public void onStartApp(ComponentName cn, UserHandle user, String container) {
+        if (cn != null) {
+            AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn))
+                    .setTarget(cn.getPackageName(), user)
+                    .setClassName(cn.getClassName())
+                    .build();
+            sendLaunch(target, container);
+        }
+    }
+
+    @UiThread
+    private void sendLaunch(AppTarget target, String container) {
+        AppTargetEvent event = new AppTargetEvent.Builder(target, AppTargetEvent.ACTION_LAUNCH)
+                .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
+                .build();
+        Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
new file mode 100644
index 0000000..55f4c98
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -0,0 +1,411 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.IntProperty;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.keyboard.FocusIndicatorHelper;
+import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
+import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.AnimatedFloat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class PredictionRowView extends LinearLayout implements
+        LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
+
+    private static final String TAG = "PredictionRowView";
+
+    private static final IntProperty<PredictionRowView> TEXT_ALPHA =
+            new IntProperty<PredictionRowView>("textAlpha") {
+                @Override
+                public void setValue(PredictionRowView view, int alpha) {
+                    view.setTextAlpha(alpha);
+                }
+
+                @Override
+                public Integer get(PredictionRowView view) {
+                    return view.mIconCurrentTextAlpha;
+                }
+            };
+
+    private static final Interpolator ALPHA_FACTOR_INTERPOLATOR =
+            (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
+
+    private static final OnClickListener PREDICTION_CLICK_LISTENER =
+            ItemClickHandler.getInstance(AppLaunchTracker.CONTAINER_PREDICTIONS);
+
+    private final Launcher mLauncher;
+    private final PredictionUiStateManager mPredictionUiStateManager;
+    private final int mNumPredictedAppsPerRow;
+
+    // The set of predicted app component names
+    private final List<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
+    // The set of predicted apps resolved from the component names and the current set of apps
+    private final ArrayList<ItemInfoWithIcon> mPredictedApps = new ArrayList<>();
+    // Helper to drawing the focus indicator.
+    private final FocusIndicatorHelper mFocusHelper;
+
+    private final int mIconTextColor;
+    private final int mIconFullTextAlpha;
+    private int mIconCurrentTextAlpha;
+
+    private FloatingHeaderView mParent;
+    private boolean mScrolledOut;
+
+    private float mScrollTranslation = 0;
+    private final AnimatedFloat mContentAlphaFactor =
+            new AnimatedFloat(this::updateTranslationAndAlpha);
+    private final AnimatedFloat mOverviewScrollFactor =
+            new AnimatedFloat(this::updateTranslationAndAlpha);
+
+    private View mLoadingProgress;
+
+    private boolean mPredictionsEnabled = false;
+
+    public PredictionRowView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        setOrientation(LinearLayout.HORIZONTAL);
+
+        mFocusHelper = new SimpleFocusIndicatorHelper(this);
+
+        mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+        mLauncher = Launcher.getLauncher(context);
+        mLauncher.addOnDeviceProfileChangeListener(this);
+
+        mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context);
+
+        mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary);
+        mIconFullTextAlpha = Color.alpha(mIconTextColor);
+        mIconCurrentTextAlpha = mIconFullTextAlpha;
+
+        updateVisibility();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView());
+        getAppsStore().registerIconContainer(this);
+        AllAppsTipView.scheduleShowIfNeeded(mLauncher);
+    }
+
+    private AllAppsStore getAppsStore() {
+        return mLauncher.getAppsView().getAppsStore();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mPredictionUiStateManager.setTargetAppsView(null);
+        getAppsStore().unregisterIconContainer(this);
+    }
+
+    public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+        mParent = parent;
+        setPredictionsEnabled(mPredictionUiStateManager.arePredictionsEnabled());
+    }
+
+    private void setPredictionsEnabled(boolean predictionsEnabled) {
+        if (predictionsEnabled != mPredictionsEnabled) {
+            mPredictionsEnabled = predictionsEnabled;
+            updateVisibility();
+        }
+    }
+
+    private void updateVisibility() {
+        setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
+                MeasureSpec.EXACTLY));
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        mFocusHelper.draw(canvas);
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    public int getExpectedHeight() {
+        return getVisibility() == GONE ? 0 :
+                Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
+                + getPaddingTop() + getPaddingBottom();
+    }
+
+    @Override
+    public boolean shouldDraw() {
+        return getVisibility() != GONE;
+    }
+
+    @Override
+    public boolean hasVisibleContent() {
+        return mPredictionUiStateManager.arePredictionsEnabled();
+    }
+
+    /**
+     * Returns the predicted apps.
+     */
+    public List<ItemInfoWithIcon> getPredictedApps() {
+        return mPredictedApps;
+    }
+
+    /**
+     * Sets the current set of predicted apps.
+     *
+     * This can be called before we get the full set of applications, we should merge the results
+     * only in onPredictionsUpdated() which is idempotent.
+     *
+     * 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(boolean predictionsEnabled, List<ComponentKeyMapper> apps) {
+        setPredictionsEnabled(predictionsEnabled);
+        mPredictedAppComponents.clear();
+        mPredictedAppComponents.addAll(apps);
+
+        mPredictedApps.clear();
+        mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+        applyPredictionApps();
+    }
+
+    @Override
+    public void onDeviceProfileChanged(DeviceProfile dp) {
+        removeAllViews();
+        applyPredictionApps();
+    }
+
+    private void applyPredictionApps() {
+        if (mLoadingProgress != null) {
+            removeView(mLoadingProgress);
+        }
+        if (!mPredictionsEnabled) {
+            mParent.onHeightUpdated();
+            return;
+        }
+
+        if (getChildCount() != mNumPredictedAppsPerRow) {
+            while (getChildCount() > mNumPredictedAppsPerRow) {
+                removeViewAt(0);
+            }
+            while (getChildCount() < mNumPredictedAppsPerRow) {
+                BubbleTextView icon = (BubbleTextView) mLauncher.getLayoutInflater().inflate(
+                        R.layout.all_apps_icon, this, false);
+                icon.setOnClickListener(PREDICTION_CLICK_LISTENER);
+                icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
+                icon.setLongPressTimeoutFactor(1f);
+                icon.setOnFocusChangeListener(mFocusHelper);
+
+                LayoutParams lp = (LayoutParams) icon.getLayoutParams();
+                // Ensure the all apps icon height matches the workspace icons in portrait mode.
+                lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+                lp.width = 0;
+                lp.weight = 1;
+                addView(icon);
+            }
+        }
+
+        int predictionCount = mPredictedApps.size();
+        int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
+
+        for (int i = 0; i < getChildCount(); i++) {
+            BubbleTextView icon = (BubbleTextView) getChildAt(i);
+            icon.reset();
+            if (predictionCount > i) {
+                icon.setVisibility(View.VISIBLE);
+                if (mPredictedApps.get(i) instanceof AppInfo) {
+                    icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i));
+                } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) {
+                    icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i));
+                }
+                icon.setTextColor(iconColor);
+            } else {
+                icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
+            }
+        }
+
+        if (predictionCount == 0) {
+            if (mLoadingProgress == null) {
+                mLoadingProgress = LayoutInflater.from(getContext())
+                        .inflate(R.layout.prediction_load_progress, this, false);
+            }
+            addView(mLoadingProgress);
+        } else {
+            mLoadingProgress = null;
+        }
+
+        mParent.onHeightUpdated();
+    }
+
+    private List<ItemInfoWithIcon> processPredictedAppComponents(List<ComponentKeyMapper> components) {
+        if (getAppsStore().getApps().isEmpty()) {
+            // Apps have not been bound yet.
+            return Collections.emptyList();
+        }
+
+        List<ItemInfoWithIcon> predictedApps = new ArrayList<>();
+        for (ComponentKeyMapper mapper : components) {
+            ItemInfoWithIcon info = mapper.getApp(getAppsStore());
+            if (info != null) {
+                predictedApps.add(info);
+            } else {
+                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    Log.e(TAG, "Predicted app not found: " + mapper);
+                }
+            }
+            // Stop at the number of predicted apps
+            if (predictedApps.size() == mNumPredictedAppsPerRow) {
+                break;
+            }
+        }
+        return predictedApps;
+    }
+
+    @Override
+    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+            LauncherLogProto.Target targetParent) {
+        for (int i = 0; i < mPredictedApps.size(); i++) {
+            ItemInfoWithIcon appInfo = mPredictedApps.get(i);
+            if (appInfo == info) {
+                targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
+                target.predictedRank = i;
+                break;
+            }
+        }
+    }
+
+    public void setTextAlpha(int alpha) {
+        mIconCurrentTextAlpha = alpha;
+        int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
+
+        if (mLoadingProgress == null) {
+            for (int i = 0; i < getChildCount(); i++) {
+                ((BubbleTextView) getChildAt(i)).setTextColor(iconColor);
+            }
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+
+    @Override
+    public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+        mScrolledOut = isScrolledOut;
+        updateTranslationAndAlpha();
+        if (!isScrolledOut) {
+            mScrollTranslation = scroll;
+            updateTranslationAndAlpha();
+        }
+    }
+
+    private void updateTranslationAndAlpha() {
+        if (mPredictionsEnabled) {
+            setTranslationY((1 - mOverviewScrollFactor.value) * mScrollTranslation);
+
+            float factor = ALPHA_FACTOR_INTERPOLATOR.getInterpolation(mOverviewScrollFactor.value);
+            float endAlpha = factor + (1 - factor) * (mScrolledOut ? 0 : 1);
+            setAlpha(mContentAlphaFactor.value * endAlpha);
+            AlphaUpdateListener.updateVisibility(this);
+        }
+    }
+
+    @Override
+    public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+            PropertySetter setter, Interpolator fadeInterpolator) {
+        boolean isDrawn = getAlpha() > 0;
+        int textAlpha = hasHeaderExtra
+                ? (hasContent ? mIconFullTextAlpha : 0) // Text follows the content visibility
+                : mIconCurrentTextAlpha; // Leave as before
+        if (!isDrawn) {
+            // If the header is not drawn, no need to animate the text alpha
+            setTextAlpha(textAlpha);
+        } else {
+            setter.setInt(this, TEXT_ALPHA, textAlpha, fadeInterpolator);
+        }
+
+        setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE,
+                (hasHeaderExtra && !hasContent) ? 1 : 0, LINEAR);
+        setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0,
+                fadeInterpolator);
+    }
+
+    @Override
+    public void setInsets(Rect insets, DeviceProfile grid) {
+        int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+                + grid.cellLayoutPaddingLeftRightPx;
+        setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+    }
+
+    @Override
+    public Class<PredictionRowView> getTypeClass() {
+        return PredictionRowView.class;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
new file mode 100644
index 0000000..54fd845
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -0,0 +1,330 @@
+/**
+ * Copyright (C) 2019 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.appprediction;
+
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.MainThreadInitializedObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Handler responsible to updating the UI due to predicted apps changes. Operations:
+ * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes
+ * invisible again before applying the changes. This ensures that the UI does not change abruptly
+ * in front of the user, even if an app launched and user pressed back button to return to the
+ * all-apps UI again.
+ * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory
+ * even if all-apps is not opened as they are shown in search UI as well
+ * 3) Load instant app if it is not already in memory. As predictions are persisted on disk,
+ * instant app will not be in memory when launcher starts.
+ * 4) Maintains the current active client id (for the predictions) and all updates are performed on
+ * that client id.
+ */
+public class PredictionUiStateManager implements OnGlobalLayoutListener, ItemInfoUpdateReceiver,
+        OnSharedPreferenceChangeListener, OnIDPChangeListener, OnUpdateListener {
+
+    public static final String KEY_APP_SUGGESTION = "pref_show_predictions";
+
+    // TODO (b/129421797): Update the client constants
+    public enum Client {
+        HOME("GEL"),
+        OVERVIEW("OVERVIEW_GEL");
+
+        public final String id;
+
+        Client(String id) {
+            this.id = id;
+        }
+    }
+
+    public static final MainThreadInitializedObject<PredictionUiStateManager> INSTANCE =
+            new MainThreadInitializedObject<>(PredictionUiStateManager::new);
+
+    private final Context mContext;
+    private final SharedPreferences mMainPrefs;
+
+    private final DynamicItemCache mDynamicItemCache;
+    private final List[] mPredictionServicePredictions;
+
+    private int mMaxIconsPerRow;
+    private Client mActiveClient;
+
+    private AllAppsContainerView mAppsView;
+
+    private PredictionState mPendingState;
+    private PredictionState mCurrentState;
+
+    private PredictionUiStateManager(Context context) {
+        mContext = context;
+        mMainPrefs = Utilities.getPrefs(context);
+
+        mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
+
+        mActiveClient = Client.HOME;
+
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        mMaxIconsPerRow = idp.numColumns;
+
+        idp.addOnChangeListener(this);
+        mPredictionServicePredictions = new List[Client.values().length];
+        for (int i = 0; i < mPredictionServicePredictions.length; i++) {
+            mPredictionServicePredictions[i] = Collections.emptyList();
+        }
+        // Listens for enable/disable signal, and predictions if using AiAi is disabled.
+        mMainPrefs.registerOnSharedPreferenceChangeListener(this);
+        // Call this last
+        mCurrentState = parseLastState();
+    }
+
+    @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        mMaxIconsPerRow = profile.numColumns;
+    }
+
+    public Client getClient() {
+        return mActiveClient;
+    }
+
+    public void switchClient(Client client) {
+        if (client == mActiveClient) {
+            return;
+        }
+        mActiveClient = client;
+        dispatchOnChange(true);
+    }
+
+    public void setTargetAppsView(AllAppsContainerView appsView) {
+        if (mAppsView != null) {
+            mAppsView.getAppsStore().removeUpdateListener(this);
+        }
+        mAppsView = appsView;
+        if (mAppsView != null) {
+            mAppsView.getAppsStore().addUpdateListener(this);
+        }
+        if (mPendingState != null) {
+            applyState(mPendingState);
+            mPendingState = null;
+        } else {
+            applyState(mCurrentState);
+        }
+        updateDependencies(mCurrentState);
+    }
+
+    @Override
+    public void reapplyItemInfo(ItemInfoWithIcon info) { }
+
+    @Override
+    public void onGlobalLayout() {
+        if (mAppsView == null) {
+            return;
+        }
+        if (mPendingState != null && canApplyPredictions(mPendingState)) {
+            applyState(mPendingState);
+            mPendingState = null;
+        }
+        if (mPendingState == null) {
+            mAppsView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+        }
+    }
+
+    private void scheduleApplyPredictedApps(PredictionState state) {
+        boolean registerListener = mPendingState == null;
+        mPendingState = state;
+        if (registerListener) {
+            // OnGlobalLayoutListener is called whenever a view in the view tree changes
+            // visibility. Add a listener and wait until appsView is invisible again.
+            mAppsView.getViewTreeObserver().addOnGlobalLayoutListener(this);
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        if (KEY_APP_SUGGESTION.equals(key)) {
+            dispatchOnChange(true);
+        }
+    }
+
+    private void applyState(PredictionState state) {
+        boolean wasEnabled = mCurrentState.isEnabled;
+        mCurrentState = state;
+        if (mAppsView != null) {
+            mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+                    .setPredictedApps(mCurrentState.isEnabled, mCurrentState.apps);
+
+            if (wasEnabled != mCurrentState.isEnabled) {
+                // Reapply state as the State UI might have changed.
+                Launcher.getLauncher(mAppsView.getContext()).getStateManager().reapplyState(true);
+            }
+        }
+    }
+
+    public AppPredictor.Callback appPredictorCallback(Client client) {
+        return targets -> {
+            mPredictionServicePredictions[client.ordinal()] = targets;
+            dispatchOnChange(true);
+        };
+    }
+
+    private void dispatchOnChange(boolean changed) {
+        PredictionState newState = changed ? parseLastState() :
+                (mPendingState == null ? mCurrentState : mPendingState);
+        if (changed && mAppsView != null && !canApplyPredictions(newState)) {
+            scheduleApplyPredictedApps(newState);
+        } else {
+            applyState(newState);
+        }
+    }
+
+    private PredictionState parseLastState() {
+        PredictionState state = new PredictionState();
+        state.isEnabled = mMainPrefs.getBoolean(KEY_APP_SUGGESTION, true);
+        if (!state.isEnabled) {
+            state.apps = Collections.EMPTY_LIST;
+            return state;
+        }
+
+        state.apps = new ArrayList<>();
+
+        List<AppTarget> appTargets = mPredictionServicePredictions[mActiveClient.ordinal()];
+        if (!appTargets.isEmpty()) {
+            for (AppTarget appTarget : appTargets) {
+                ComponentKey key;
+                if (appTarget.getShortcutInfo() != null) {
+                    key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
+                } else {
+                    key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
+                            appTarget.getClassName()), appTarget.getUser());
+                }
+                state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
+            }
+        }
+        updateDependencies(state);
+        return state;
+    }
+
+    private void updateDependencies(PredictionState state) {
+        if (!state.isEnabled || mAppsView == null) {
+            return;
+        }
+
+        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+        List<String> instantAppsToLoad = new ArrayList<>();
+        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
+        int total = state.apps.size();
+        for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
+            ComponentKeyMapper mapper = state.apps.get(i);
+            // Update instant apps
+            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+                instantAppsToLoad.add(mapper.getPackage());
+                count++;
+            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+                count++;
+            } else {
+                // Reload high res icon
+                AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
+                if (info != null) {
+                    if (info.usingLowResIcon()) {
+                        // TODO: Update icon cache to support null callbacks.
+                        iconCache.updateIconInBackground(this, info);
+                    }
+                    count++;
+                }
+            }
+        }
+        mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
+    }
+
+    @Override
+    public void onAppsUpdated() {
+        dispatchOnChange(false);
+    }
+
+    public boolean arePredictionsEnabled() {
+        return mCurrentState.isEnabled;
+    }
+
+    private boolean canApplyPredictions(PredictionState newState) {
+        if (mAppsView == null) {
+            // If there is no apps view, no need to schedule.
+            return true;
+        }
+        Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
+        PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
+                findFixedRowByType(PredictionRowView.class);
+        if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
+                launcher.isForceInvisible()) {
+            return true;
+        }
+
+        if (mCurrentState.isEnabled != newState.isEnabled
+                || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
+            // If the visibility of the prediction row is changing, apply immediately.
+            return true;
+        }
+
+        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+            // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
+            return false;
+        }
+        if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
+            // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
+            return false;
+        }
+
+        // Instead of checking against 1, we should check against (1 + delta), where delta accounts
+        // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
+        // against 1, keeps the logic simple :)
+        return launcher.getAllAppsController().getProgress() > 1;
+    }
+
+    public PredictionState getCurrentState() {
+        return mCurrentState;
+    }
+
+    public static class PredictionState {
+
+        public boolean isEnabled;
+        public List<ComponentKeyMapper> apps;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 3b2d66d..35783b5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -45,7 +45,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherInitListenerEx;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -304,7 +304,7 @@
     @Override
     public ActivityInitListener createActivityInitListener(
             BiPredicate<Launcher, Boolean> onInitListener) {
-        return new LauncherInitListener(onInitListener);
+        return new LauncherInitListenerEx(onInitListener);
     }
 
     @Nullable
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index d979c99..84af109 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -36,6 +36,8 @@
  */
 public class TaskOverlayFactory implements ResourceBasedOverride {
 
+    public static final String AIAI_PACKAGE = "com.google.android.as";
+
     /** Note that these will be shown in order from top to bottom, if available for the task. */
     private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
             new TaskSystemShortcut.AppInfo(),
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index efe16c5..c31e829 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.AnimatorSet;
@@ -39,7 +38,8 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.ClipAnimationHelper;
@@ -195,4 +195,13 @@
             }
         }
     }
+
+    @Override
+    public void reset() {
+        super.reset();
+
+        // We are moving to home or some other UI with no recents. Switch back to the home client,
+        // the home predictions should have been updated when the activity was resumed.
+        PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index d8aeb35..72d60da 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -82,6 +82,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -751,8 +753,6 @@
 
         unloadVisibleTaskData();
         setCurrentPage(0);
-
-        OverviewCallbacks.get(getContext()).onResetOverview();
     }
 
     /**
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 81565a5..4319b5d 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -58,4 +58,13 @@
     <!-- Annotation shown on an app card in Recents, telling that the app has a usage limit set by
     the user, and a given time is left for it today [CHAR LIMIT=22] -->
     <string name="time_left_for_app"><xliff:g id="time" example="7 minutes">%1$s</xliff:g> left today</string>
+
+    <!-- Accessibility title for the row of all-apps containing app predictions. [CHAR LIMIT=50] -->
+    <string name="title_app_suggestions">App suggestions</string>
+    <!-- Label for the header text of the All Apps section in All Apps view, used to separate Predicted Apps and Actions section from All Apps section. [CHAR_LIMIT=50] -->
+    <string name="all_apps_label">All apps</string>
+    <!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
+    <string name="all_apps_prediction_tip">Your predicted apps</string>
+
+
 </resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
index ef9c5c0..f5573ba 100644
--- a/quickstep/src/com/android/quickstep/OverviewCallbacks.java
+++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
@@ -39,7 +39,5 @@
 
     public void onInitOverviewTransition() { }
 
-    public void onResetOverview() { }
-
     public void closeAllWindows() { }
 }
diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
index 6dff187..ca7711f 100644
--- a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
+++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
@@ -36,6 +36,8 @@
 @SuppressWarnings("unused")
 public class UserEventDispatcherExtension extends UserEventDispatcher {
 
+    public static final int ALL_APPS_PREDICTION_TIPS = 2;
+
     private static final String TAG = "UserEventDispatcher";
 
     public UserEventDispatcherExtension(Context context) { }
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
new file mode 100644
index 0000000..43f6039
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (C) 2019 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.quickstep;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.pm.LauncherActivityInfo;
+import android.os.Process;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.appprediction.PredictionRowView;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.model.AppLaunchTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class AppPredictionsUITests  extends AbstractQuickStepTest {
+    private static final int DEFAULT_APP_LAUNCH_TIMES = 3;
+    private static final String TAG = "AppPredictionsUITests";
+
+    private LauncherActivityInfo mSampleApp1;
+    private LauncherActivityInfo mSampleApp2;
+    private LauncherActivityInfo mSampleApp3;
+
+    private AppPredictor.Callback mCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(mTargetContext)
+                .getActivityList(null, Process.myUserHandle());
+        mSampleApp1 = activities.get(0);
+        mSampleApp2 = activities.get(1);
+        mSampleApp3 = activities.get(2);
+
+        // Disable app tracker
+        AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
+
+        mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback(
+                Client.HOME);
+
+        mDevice.setOrientationNatural();
+    }
+
+    @After
+    public void tearDown() throws Throwable {
+        mDevice.unfreezeRotation();
+    }
+
+    /**
+     * Test that prediction UI is updated as soon as we get predictions from the system
+     */
+    @Test
+    public void testPredictionExistsInAllApps() {
+        mActivityMonitor.startLauncher();
+        mLauncher.pressHome().switchToAllApps();
+
+        // There has not been any update, verify that progress bar is showing
+        waitForLauncherCondition("Prediction is not in loading state", launcher -> {
+            ProgressBar p = findLoadingBar(launcher);
+            return p != null && p.isShown();
+        });
+
+        // Dispatch an update
+        sendPredictionUpdate(mSampleApp1, mSampleApp2);
+        waitForLauncherCondition("Predictions were not updated in loading state",
+                launcher -> getPredictedApp(launcher).size() == 2);
+    }
+
+    /**
+     * Test tat prediction update is deferred if it is already visible
+     */
+    @Test
+    public void testPredictionsDeferredUntilHome() {
+        mActivityMonitor.startLauncher();
+        sendPredictionUpdate(mSampleApp1, mSampleApp2);
+        mLauncher.pressHome().switchToAllApps();
+        waitForLauncherCondition("Predictions were not updated in loading state",
+                launcher -> getPredictedApp(launcher).size() == 2);
+
+        // Update predictions while all-apps is visible
+        sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3);
+        assertEquals(2, getFromLauncher(this::getPredictedApp).size());
+
+        // Go home and go back to all-apps
+        mLauncher.pressHome().switchToAllApps();
+        assertEquals(3, getFromLauncher(this::getPredictedApp).size());
+    }
+
+    public ArrayList<BubbleTextView> getPredictedApp(Launcher launcher) {
+        PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
+                .findFixedRowByType(PredictionRowView.class);
+
+        ArrayList<BubbleTextView> predictedAppViews = new ArrayList<>();
+        for (int i = 0; i < container.getChildCount(); i++) {
+            View view = container.getChildAt(i);
+            if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) {
+                predictedAppViews.add((BubbleTextView) view);
+            }
+        }
+        return predictedAppViews;
+    }
+
+    private ProgressBar findLoadingBar(Launcher launcher) {
+        PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
+                .findFixedRowByType(PredictionRowView.class);
+
+        for (int i = 0; i < container.getChildCount(); i++) {
+            View view = container.getChildAt(i);
+            if (view instanceof ProgressBar) {
+                return (ProgressBar) view;
+            }
+        }
+        return null;
+    }
+
+
+    private void sendPredictionUpdate(LauncherActivityInfo... activities) {
+        getOnUiThread(() -> {
+            List<AppTarget> targets = new ArrayList<>(activities.length);
+            for (LauncherActivityInfo info : activities) {
+                ComponentName cn = info.getComponentName();
+                AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn))
+                        .setTarget(cn.getPackageName(), info.getUser())
+                        .setClassName(cn.getClassName())
+                        .build();
+                targets.add(target);
+            }
+            mCallback.onTargetsAvailable(targets);
+            return null;
+        });
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index fff8f02..d790c04 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2223,10 +2223,6 @@
      */
     public void bindAllApplications(ArrayList<AppInfo> apps) {
         mAppsView.getAppsStore().setApps(apps);
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.bindAllApplications(apps);
-        }
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 34bdb3c..edac516 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -57,16 +57,10 @@
      * Extension points for providing custom behavior on certain user interactions.
      */
     void onLauncherProviderChange();
-    void bindAllApplications(ArrayList<AppInfo> apps);
 
     /**
      * Starts a search with {@param initialQuery}. Return false if search was not started.
      */
     boolean startSearch(
             String initialQuery, boolean selectInitialQuery, Bundle appSearchData);
-
-    /*
-     * Extensions points for adding / replacing some other aspects of the Launcher experience.
-     */
-    boolean hasSettings();
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e7b4ff4..e788ceb 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -213,6 +213,7 @@
         synchronized (mLock) {
             Preconditions.assertUIThread();
             mCallbacks = new WeakReference<>(callbacks);
+            android.util.Log.d("b/131170582", "mCallbacks = " + mCallbacks);
         }
     }
 
@@ -330,6 +331,7 @@
             // Stop any existing loaders first, so they don't set mModelLoaded to true later
             stopLoader();
             mModelLoaded = false;
+            android.util.Log.d("b/131170582", "1 mModelLoaded = " + mModelLoaded);
         }
 
         // Start the loader if launcher is already running, otherwise the loader will run,
@@ -390,6 +392,7 @@
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
             mLoaderTask = null;
+            android.util.Log.d("b/131170582", "1 mLoaderTask = " + mLoaderTask);
             if (oldTask != null) {
                 oldTask.stopLocked();
             }
@@ -400,6 +403,7 @@
         synchronized (mLock) {
             stopLoader();
             mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
+            android.util.Log.d("b/131170582", "2 mLoaderTask = " + mLoaderTask);
             runOnWorkerThread(mLoaderTask);
         }
     }
@@ -444,6 +448,7 @@
                 mTask = task;
                 mIsLoaderTaskRunning = true;
                 mModelLoaded = false;
+                android.util.Log.d("b/131170582", "2 mModelLoaded = " + mModelLoaded);
             }
         }
 
@@ -451,6 +456,7 @@
             synchronized (mLock) {
                 // Everything loaded bind the data.
                 mModelLoaded = true;
+                android.util.Log.d("b/131170582", "3 mModelLoaded = " + mModelLoaded);
             }
         }
 
@@ -460,6 +466,7 @@
                 // If we are still the last one to be scheduled, remove ourselves.
                 if (mLoaderTask == mTask) {
                     mLoaderTask = null;
+                    android.util.Log.d("b/131170582", "3 mLoaderTask = " + mLoaderTask);
                 }
                 mIsLoaderTaskRunning = false;
             }
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 4de082e..07ddccb 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -20,12 +20,12 @@
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
-import static android.view.ViewConfiguration.getLongPressTimeout;
 
 import static com.android.launcher3.LauncherState.NORMAL;
 
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
@@ -45,7 +45,8 @@
 /**
  * Helper class to handle touch on empty space in workspace and show options popup on long press
  */
-public class WorkspaceTouchListener implements OnTouchListener, Runnable {
+public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListener
+        implements OnTouchListener {
 
     /**
      * STATE_PENDING_PARENT_INFORM is the state between longPress performed & the next motionEvent.
@@ -66,16 +67,21 @@
 
     private int mLongPressState = STATE_CANCELLED;
 
+    private final GestureDetector mGestureDetector;
+
     public WorkspaceTouchListener(Launcher launcher, Workspace workspace) {
         mLauncher = launcher;
         mWorkspace = workspace;
         // Use twice the touch slop as we are looking for long press which is more
         // likely to cause movement.
         mTouchSlop = 2 * ViewConfiguration.get(launcher).getScaledTouchSlop();
+        mGestureDetector = new GestureDetector(workspace.getContext(), this);
     }
 
     @Override
     public boolean onTouch(View view, MotionEvent ev) {
+        mGestureDetector.onTouchEvent(ev);
+
         int action = ev.getActionMasked();
         if (action == ACTION_DOWN) {
             // Check if we can handle long press.
@@ -97,7 +103,6 @@
             if (handleLongPress) {
                 mLongPressState = STATE_REQUESTED;
                 mTouchDownPoint.set(ev.getX(), ev.getY());
-                mWorkspace.postDelayed(this, getLongPressTimeout());
             }
 
             mWorkspace.onTouchEvent(ev);
@@ -143,9 +148,6 @@
             }
         }
 
-        if (action == ACTION_UP || action == ACTION_CANCEL) {
-            cancelLongPress();
-        }
         return result;
     }
 
@@ -155,12 +157,11 @@
     }
 
     private void cancelLongPress() {
-        mWorkspace.removeCallbacks(this);
         mLongPressState = STATE_CANCELLED;
     }
 
     @Override
-    public void run() {
+    public void onLongPress(MotionEvent event) {
         if (mLongPressState == STATE_REQUESTED) {
             if (canHandleLongPress()) {
                 mLongPressState = STATE_PENDING_PARENT_INFORM;
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 5747db1..2ee0328 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -22,6 +22,8 @@
 
 import java.util.concurrent.ExecutionException;
 
+import androidx.annotation.VisibleForTesting;
+
 /**
  * Utility class for defining singletons which are initiated on main thread.
  */
@@ -53,6 +55,11 @@
         return mValue;
     }
 
+    @VisibleForTesting
+    public void initializeForTesting(T value) {
+        mValue = value;
+    }
+
     public interface ObjectProvider<T> {
 
         T get(Context context);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 71e7880..a37218b 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -168,7 +168,7 @@
     }
 
     @After
-    public void tearDown() {
+    public void verifyLauncherState() {
         try {
             // Limits UI tests affecting tests running after them.
             waitForModelLoaded();
@@ -234,12 +234,8 @@
 
     protected void resetLoaderState() {
         try {
-            mMainThreadExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    LauncherAppState.getInstance(mTargetContext).getModel().forceReload();
-                }
-            });
+            mMainThreadExecutor.execute(
+                    () -> LauncherAppState.getInstance(mTargetContext).getModel().forceReload());
         } catch (Throwable t) {
             throw new IllegalArgumentException(t);
         }
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 918e6e2..af50190 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -103,8 +103,6 @@
         if (mSessionId > -1) {
             mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         }
-
-        super.tearDown();
     }
 
     @Test