Merge "Temporarily ignoring flaky test checking mDevice.pressRecentApps()" into ub-launcher3-master
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
index b2d495b..77b287b 100644
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskActionController.java
@@ -71,7 +71,6 @@
      * Clears all tasks and updates the model and view.
      */
     public void clearAllTasks() {
-        // TODO: Play an animation so transition is more natural.
         int count = mAdapter.getItemCount();
         ActivityManagerWrapper.getInstance().removeAllRecentTasks();
         mLoader.clearAllTasks();
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index e56cc51..c98eca6 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -75,8 +75,20 @@
             // Task list has updated.
             return;
         }
-        holder.bindTask(tasks.get(position));
-
+        Task task = tasks.get(position);
+        holder.bindTask(task);
+        mLoader.loadTaskIconAndLabel(task, () -> {
+            // Ensure holder still has the same task.
+            if (task.equals(holder.getTask())) {
+                holder.getTaskItemView().setIcon(task.icon);
+                holder.getTaskItemView().setLabel(task.titleDescription);
+            }
+        });
+        mLoader.loadTaskThumbnail(task, () -> {
+            if (task.equals(holder.getTask())) {
+                holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
+            }
+        });
     }
 
     @Override
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index a89229f..744afd7 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -35,17 +35,18 @@
         mTaskItemView = itemView;
     }
 
+    public TaskItemView getTaskItemView() {
+        return mTaskItemView;
+    }
+
     /**
-     * Bind task content to the view. This includes the task icon and title as well as binding
-     * input handlers such as which task to launch/remove.
+     * Bind a task to the holder, resetting the view and preparing it for content to load in.
      *
      * @param task the task to bind to the view
      */
     public void bindTask(Task task) {
         mTask = task;
-        mTaskItemView.setLabel(task.titleDescription);
-        mTaskItemView.setIcon(task.icon);
-        mTaskItemView.setThumbnail(task.thumbnail.thumbnail);
+        mTaskItemView.resetTaskItemView();
     }
 
     /**
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
index c86c24e..1234989 100644
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
@@ -25,7 +25,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 
 /**
@@ -39,35 +38,48 @@
 
     private ArrayList<Task> mTaskList = new ArrayList<>();
     private int mTaskListChangeId;
+    private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
+        Task foundTask = null;
+        for (Task task : mTaskList) {
+            if (task.key.id == taskId) {
+                foundTask = task;
+                break;
+            }
+        }
+        if (foundTask != null) {
+            foundTask.thumbnail = thumbnailData;
+        }
+        return foundTask;
+    };
 
     public TaskListLoader(Context context) {
         mRecentsModel = RecentsModel.INSTANCE.get(context);
+        mRecentsModel.addThumbnailChangeListener(listener);
     }
 
     /**
-     * Returns the current task list as of the last completed load (see
-     * {@link #loadTaskList}) as a read-only list. This list of tasks is guaranteed to always have
-     * all its task content loaded.
+     * Returns the current task list as of the last completed load (see {@link #loadTaskList}) as a
+     * read-only list. This list of tasks is not guaranteed to have all content loaded.
      *
-     * @return the current list of tasks w/ all content loaded
+     * @return the current list of tasks
      */
     public List<Task> getCurrentTaskList() {
         return Collections.unmodifiableList(mTaskList);
     }
 
     /**
-     * Fetches the most recent tasks and updates the task list asynchronously. In addition it
-     * loads the content for each task (icon and label). The callback and task list being updated
-     * only occur when all task content is fully loaded and up-to-date.
+     * Fetches the most recent tasks and updates the task list asynchronously. This call does not
+     * provide guarantees the task content (icon, thumbnail, label) are loaded but will fill in
+     * what it has. May run the callback immediately if there have been no changes in the task
+     * list.
      *
-     * @param onTasksLoadedCallback callback for when the tasks are fully loaded. Done on the UI
-     *                              thread
+     * @param onLoadedCallback callback to run when task list is loaded
      */
-    public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onTasksLoadedCallback) {
+    public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onLoadedCallback) {
         if (mRecentsModel.isTaskListValid(mTaskListChangeId)) {
             // Current task list is already up to date. No need to update.
-            if (onTasksLoadedCallback != null) {
-                onTasksLoadedCallback.accept(mTaskList);
+            if (onLoadedCallback != null) {
+                onLoadedCallback.accept(mTaskList);
             }
             return;
         }
@@ -76,16 +88,46 @@
             // Reverse tasks to put most recent at the bottom of the view
             Collections.reverse(tasks);
             // Load task content
-            loadTaskContents(tasks, () -> {
-                mTaskList = tasks;
-                if (onTasksLoadedCallback != null) {
-                    onTasksLoadedCallback.accept(mTaskList);
+            for (Task task : tasks) {
+                int loadedPos = mTaskList.indexOf(task);
+                if (loadedPos == -1) {
+                    continue;
                 }
-            });
+                Task loadedTask = mTaskList.get(loadedPos);
+                task.icon = loadedTask.icon;
+                task.titleDescription = loadedTask.titleDescription;
+                task.thumbnail = loadedTask.thumbnail;
+            }
+            mTaskList = tasks;
+            onLoadedCallback.accept(tasks);
         });
     }
 
     /**
+     * Load task icon and label asynchronously if it is not already loaded in the task. If the task
+     * already has an icon, this calls the callback immediately.
+     *
+     * @param task task to update with icon + label
+     * @param onLoadedCallback callback to run when task has icon and label
+     */
+    public void loadTaskIconAndLabel(Task task, @Nullable Runnable onLoadedCallback) {
+        mRecentsModel.getIconCache().updateIconInBackground(task,
+                loadedTask -> onLoadedCallback.run());
+    }
+
+    /**
+     * Load thumbnail asynchronously if not already loaded in the task. If the task already has a
+     * thumbnail or if the thumbnail is cached, this calls the callback immediately.
+     *
+     * @param task task to update with the thumbnail
+     * @param onLoadedCallback callback to run when task has thumbnail
+     */
+    public void loadTaskThumbnail(Task task, @Nullable Runnable onLoadedCallback) {
+        mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
+                thumbnail -> onLoadedCallback.run());
+    }
+
+    /**
      * Removes the task from the current task list.
      */
     void removeTask(Task task) {
@@ -98,42 +140,4 @@
     void clearAllTasks() {
         mTaskList.clear();
     }
-
-    /**
-     * Loads task content for a list of tasks, including the label, icon, and thumbnail. For content
-     * that isn't cached, load the content asynchronously in the background.
-     *
-     * @param tasksToLoad list of tasks that need to load their content
-     * @param onFullyLoadedCallback runnable to run after all tasks have loaded their content
-     */
-    private void loadTaskContents(ArrayList<Task> tasksToLoad,
-            @Nullable Runnable onFullyLoadedCallback) {
-        // Make two load requests per task, one for the icon/title and one for the thumbnail.
-        AtomicInteger loadRequestsCount = new AtomicInteger(tasksToLoad.size() * 2);
-        Runnable itemLoadedRunnable = () -> {
-            if (loadRequestsCount.decrementAndGet() == 0 && onFullyLoadedCallback != null) {
-                onFullyLoadedCallback.run();
-            }
-        };
-        for (Task task : tasksToLoad) {
-            // Load icon and title.
-            int index = mTaskList.indexOf(task);
-            if (index >= 0) {
-                // If we've already loaded the task and have its content then just copy it over.
-                Task loadedTask = mTaskList.get(index);
-                task.titleDescription = loadedTask.titleDescription;
-                task.icon = loadedTask.icon;
-                itemLoadedRunnable.run();
-            } else {
-                // Otherwise, load the content in the background.
-                mRecentsModel.getIconCache().updateIconInBackground(task,
-                        loadedTask -> itemLoadedRunnable.run());
-            }
-
-            // Load the thumbnail. May return immediately and synchronously if the thumbnail is
-            // cached.
-            mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
-                    thumbnail -> itemLoadedRunnable.run());
-        }
-    }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index a1d62c2..1e01725 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -19,6 +19,10 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -65,6 +69,10 @@
                 }
             };
     private static final long CROSSFADE_DURATION = 300;
+    private static final long ITEM_ANIMATE_OUT_DURATION = 150;
+    private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
+    private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
+    private static final long CLEAR_ALL_FADE_DELAY = 120;
 
     /**
      * A ratio representing the view's relative placement within its padded space. For example, 0
@@ -119,7 +127,7 @@
             });
 
             View clearAllView = findViewById(R.id.clear_all_button);
-            clearAllView.setOnClickListener(v -> mTaskActionController.clearAllTasks());
+            clearAllView.setOnClickListener(v -> animateClearAllTasks());
         }
     }
 
@@ -195,6 +203,76 @@
     }
 
     /**
+     * Clear all tasks and animate out.
+     */
+    private void animateClearAllTasks() {
+        TaskItemView[] itemViews = getTaskViews();
+
+        AnimatorSet clearAnim = new AnimatorSet();
+        long currentDelay = 0;
+
+        // Animate each item view to the right and fade out.
+        for (TaskItemView itemView : itemViews) {
+            PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
+                    0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
+            PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
+            ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
+                    transXproperty, alphaProperty);
+            itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
+            itemAnim.setStartDelay(currentDelay);
+
+            clearAnim.play(itemAnim);
+            currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
+        }
+
+        // Animate view fading and leave recents when faded enough.
+        ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
+                .setDuration(CROSSFADE_DURATION);
+        contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
+        contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            private boolean mLeftRecents = false;
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
+                // Leave recents while fading out.
+                if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
+                    mActivityHelper.leaveRecents();
+                    mLeftRecents = true;
+                }
+            }
+        });
+
+        clearAnim.play(contentAlpha);
+        clearAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                for (TaskItemView itemView : itemViews) {
+                    itemView.setTranslationX(0);
+                    itemView.setAlpha(1.0f);
+                }
+                mContentView.setVisibility(GONE);
+                mTaskActionController.clearAllTasks();
+            }
+        });
+        clearAnim.start();
+    }
+
+    /**
+     * Get attached task item views ordered by most recent.
+     *
+     * @return array of attached task item views
+     */
+    private TaskItemView[] getTaskViews() {
+        int taskCount = mTaskRecyclerView.getChildCount();
+        TaskItemView[] itemViews = new TaskItemView[taskCount];
+        for (int i = 0; i < taskCount; i ++) {
+            itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
+        }
+        return itemViews;
+    }
+
+    /**
      * Update the content view so that the appropriate view is shown based off the current list
      * of tasks.
      */
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index 373f107..d831b20 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.View;
@@ -24,6 +25,8 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
 
 /**
@@ -31,12 +34,16 @@
  */
 public final class TaskItemView extends LinearLayout {
 
+    private static final String DEFAULT_LABEL = "...";
+    private final Drawable mDefaultIcon;
     private TextView mLabelView;
     private ImageView mIconView;
     private ImageView mThumbnailView;
 
     public TaskItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mDefaultIcon = context.getResources().getDrawable(
+                android.R.drawable.sym_def_app_icon, context.getTheme());
     }
 
     @Override
@@ -48,33 +55,56 @@
     }
 
     /**
-     * Set the label for the task item.
+     * Resets task item view to default values.
+     */
+    public void resetTaskItemView() {
+        setLabel(DEFAULT_LABEL);
+        setIcon(null);
+        setThumbnail(null);
+    }
+
+    /**
+     * Set the label for the task item. Sets to a default label if null.
      *
      * @param label task label
      */
-    public void setLabel(String label) {
+    public void setLabel(@Nullable String label) {
+        if (label == null) {
+            mLabelView.setText(DEFAULT_LABEL);
+            return;
+        }
         mLabelView.setText(label);
     }
 
     /**
-     * Set the icon for the task item.
+     * Set the icon for the task item. Sets to a default icon if null.
      *
      * @param icon task icon
      */
-    public void setIcon(Drawable icon) {
+    public void setIcon(@Nullable Drawable icon) {
         // TODO: Scale the icon up based off the padding on the side
         // The icon proper is actually smaller than the drawable and has "padding" on the side for
         // the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
         // view if we want the icon to be flush with the bottom of the thumbnail.
+        if (icon == null) {
+            mIconView.setImageDrawable(mDefaultIcon);
+            return;
+        }
         mIconView.setImageDrawable(icon);
     }
 
     /**
-     * Set the task thumbnail for the task.
+     * Set the task thumbnail for the task. Sets to a default thumbnail if null.
      *
      * @param thumbnail task thumbnail for the task
      */
-    public void setThumbnail(Bitmap thumbnail) {
+    public void setThumbnail(@Nullable Bitmap thumbnail) {
+        if (thumbnail == null) {
+            mThumbnailView.setImageBitmap(null);
+            mThumbnailView.setBackgroundColor(Color.GRAY);
+            return;
+        }
+        mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
         mThumbnailView.setImageBitmap(thumbnail);
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b71f790..00257a5 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -347,12 +347,7 @@
                 log(action = "0-button: from another app");
                 assertTrue("Launcher is visible, don't know how to go home",
                         !mDevice.hasObject(By.pkg(getLauncherPackageName())));
-                final UiObject2 navBar = waitForSystemUiObject("navigation_bar_frame");
-
-                swipe(
-                        navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(),
-                        navBar.getVisibleBounds().centerX(), 0,
-                        BACKGROUND_APP_STATE_ORDINAL, ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
+                mDevice.pressHome();
             }
         } else {
             log(action = "clicking home button");