Simplifying and removing unnecessary checks from app-drag tests
am: ab472e8c06

Change-Id: I084294421d3493f39bf6360a77bc3f3be800073e
diff --git a/go/quickstep/res/drawable/default_thumbnail.xml b/go/quickstep/res/drawable/default_thumbnail.xml
new file mode 100644
index 0000000..0a2dbf0
--- /dev/null
+++ b/go/quickstep/res/drawable/default_thumbnail.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/darker_gray"/>
+    <corners android:radius="2dp"/>
+</shape>
diff --git a/go/quickstep/res/drawable/empty_content_box.xml b/go/quickstep/res/drawable/empty_content_box.xml
new file mode 100644
index 0000000..a488388
--- /dev/null
+++ b/go/quickstep/res/drawable/empty_content_box.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/transparent"/>
+    <stroke android:color="@android:color/white" android:width="4px"/>
+    <corners android:radius="2dp"/>
+</shape>
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index d1d697c..c228bb9 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -131,10 +131,11 @@
             return anim;
         }
 
-        View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
+        View thumbnailView = mRecentsView.getBottomThumbnailView();
         if (thumbnailView == null) {
-            // TODO: We should either 1) guarantee the view is loaded before attempting this
-            // or 2) have a backup animation.
+            // This can be null if there were previously 0 tasks and the recycler view has not had
+            // enough time to take in the data change, bind a new view, and lay out the new view.
+            // TODO: Have a fallback to animate to
             if (Log.isLoggable(TAG, Log.WARN)) {
                 Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
             }
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 674fcae..02cbf4e 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -15,12 +15,10 @@
  */
 package com.android.quickstep;
 
-import android.util.ArrayMap;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
 
 import com.android.launcher3.R;
@@ -39,7 +37,6 @@
     private static final int MAX_TASKS_TO_DISPLAY = 6;
     private static final String TAG = "TaskAdapter";
     private final TaskListLoader mLoader;
-    private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
     private TaskActionController mTaskActionController;
     private boolean mIsShowingLoadingUi;
 
@@ -63,16 +60,6 @@
         mIsShowingLoadingUi = isShowingLoadingUi;
     }
 
-    /**
-     * Get task item view for a given task id if it's attached to the view.
-     *
-     * @param taskId task id to search for
-     * @return corresponding task item view if it's attached, null otherwise
-     */
-    public @Nullable TaskItemView getTaskItemView(int taskId) {
-        return mTaskIdToViewMap.get(taskId);
-    }
-
     @Override
     public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
@@ -94,7 +81,7 @@
             return;
         }
         Task task = tasks.get(position);
-        holder.bindTask(task);
+        holder.bindTask(task, false /* willAnimate */);
         mLoader.loadTaskIconAndLabel(task, () -> {
             // Ensure holder still has the same task.
             if (Objects.equals(task, holder.getTask())) {
@@ -110,19 +97,10 @@
     }
 
     @Override
-    public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
-        if (holder.getTask() == null) {
-            return;
-        }
-        mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
-    }
-
-    @Override
-    public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
-        if (holder.getTask() == null) {
-            return;
-        }
-        mTaskIdToViewMap.remove(holder.getTask().key.id);
+    public void onBindViewHolder(@NonNull TaskHolder holder, int position,
+            @NonNull List<Object> payloads) {
+        // TODO: Bind task in preparation for animation. For now, we apply UI changes immediately.
+        super.onBindViewHolder(holder, position, payloads);
     }
 
     @Override
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index 98dc989..a3fa5c1 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep;
 
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
@@ -40,13 +43,28 @@
     }
 
     /**
-     * Bind a task to the holder, resetting the view and preparing it for content to load in.
+     * Bind the task model to the holder. This will take the current task content in the task
+     * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
+     * the content to animate to at a later time. If the task does not have all its content loaded,
+     * the view will prepare appropriate default placeholders and it is the callers responsibility
+     * to change them at a later time.
+     *
+     * Regardless of whether it is animating, input handlers will be bound immediately (see
+     * {@link TaskActionController}).
      *
      * @param task the task to bind to the view
+     * @param willAnimate true if UI should animate in later, false if it should apply immediately
      */
-    public void bindTask(Task task) {
+    public void bindTask(@NonNull Task task, boolean willAnimate) {
         mTask = task;
-        mTaskItemView.resetTaskItemView();
+        Bitmap thumbnail = (task.thumbnail != null) ? task.thumbnail.thumbnail : null;
+        if (willAnimate) {
+            mTaskItemView.startContentAnimation(task.icon, thumbnail, task.titleDescription);
+        } else {
+            mTaskItemView.setIcon(task.icon);
+            mTaskItemView.setThumbnail(thumbnail);
+            mTaskItemView.setLabel(task.titleDescription);
+        }
     }
 
     /**
@@ -55,10 +73,7 @@
      */
     public void bindEmptyUi() {
         mTask = null;
-        // TODO: Set the task view to a loading, empty UI.
-        // Temporarily using the one below for visual confirmation but should be swapped out to new
-        // UI later.
-        mTaskItemView.resetTaskItemView();
+        mTaskItemView.resetToEmptyUi();
     }
 
     /**
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index c06b6ec..59755bc 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -24,14 +24,12 @@
 import android.animation.PropertyValuesHolder;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewDebug;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.LayoutAnimationController;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
@@ -40,6 +38,7 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
 
 import com.android.launcher3.R;
 import com.android.quickstep.RecentsToActivityHelper;
@@ -90,7 +89,6 @@
     private final TaskListLoader mTaskLoader;
     private final TaskAdapter mTaskAdapter;
     private final TaskActionController mTaskActionController;
-    private final LayoutAnimationController mLayoutAnimation;
 
     private RecentsToActivityHelper mActivityHelper;
     private RecyclerView mTaskRecyclerView;
@@ -98,6 +96,9 @@
     private View mContentView;
     private View mClearAllView;
     private boolean mTransitionedFromApp;
+    private AnimatorSet mLayoutAnimation;
+    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
+
 
     public IconRecentsView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -106,7 +107,6 @@
         mTaskAdapter = new TaskAdapter(mTaskLoader);
         mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
         mTaskAdapter.setActionController(mTaskActionController);
-        mLayoutAnimation = createLayoutAnimation();
     }
 
     @Override
@@ -120,7 +120,20 @@
             ItemTouchHelper helper = new ItemTouchHelper(
                     new TaskSwipeCallback(mTaskActionController));
             helper.attachToRecyclerView(mTaskRecyclerView);
-            mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
+            mTaskRecyclerView.addOnChildAttachStateChangeListener(
+                    new OnChildAttachStateChangeListener() {
+                        @Override
+                        public void onChildViewAttachedToWindow(@NonNull View view) {
+                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
+                                // Child view was added that is not part of current layout animation
+                                // so restart the animation.
+                                animateFadeInLayoutAnimation();
+                            }
+                        }
+
+                        @Override
+                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
+                    });
 
             mEmptyView = findViewById(R.id.recent_task_empty_view);
             mContentView = findViewById(R.id.recent_task_content_view);
@@ -165,8 +178,7 @@
      * becomes visible.
      */
     public void onBeginTransitionToOverview() {
-        mTaskRecyclerView.scheduleLayoutAnimation();
-
+        scheduleFadeInLayoutAnimation();
         // Load any task changes
         if (!mTaskLoader.needsToLoad()) {
             return;
@@ -213,16 +225,15 @@
     }
 
     /**
-     * Get the thumbnail view associated with a task for the purposes of animation.
+     * Get the bottom most thumbnail view to animate to.
      *
-     * @param taskId task id of thumbnail view to get
-     * @return the thumbnail view for the task if attached, null otherwise
+     * @return the thumbnail view if laid out
      */
-    public @Nullable View getThumbnailViewForTask(int taskId) {
-        TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
-        if (view == null) {
+    public @Nullable View getBottomThumbnailView() {
+        if (mTaskRecyclerView.getChildCount() == 0) {
             return null;
         }
+        TaskItemView view = (TaskItemView) mTaskRecyclerView.getChildAt(0);
         return view.getThumbnailView();
     }
 
@@ -338,17 +349,56 @@
                 });
     }
 
-    private static LayoutAnimationController createLayoutAnimation() {
-        AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
+    /**
+     * Schedule a one-shot layout animation on the next layout. Separate from
+     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
+     * view properties themselves, allowing more controllable behavior and making it easier to
+     * manage when the animation conflicts with another animation.
+     */
+    private void scheduleFadeInLayoutAnimation() {
+        ViewTreeObserver viewTreeObserver = mTaskRecyclerView.getViewTreeObserver();
+        viewTreeObserver.addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        animateFadeInLayoutAnimation();
+                        viewTreeObserver.removeOnGlobalLayoutListener(this);
+                    }
+                });
+    }
 
-        Animation alphaAnim = new AlphaAnimation(0, 1);
-        alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
-        anim.addAnimation(alphaAnim);
-
-        LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
-        layoutAnim.setDelay(
-                (float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
-
-        return layoutAnim;
+    /**
+     * Start animating the layout animation where items fade in.
+     */
+    private void animateFadeInLayoutAnimation() {
+        if (mLayoutAnimation != null) {
+            // If layout animation still in progress, cancel and restart.
+            mLayoutAnimation.cancel();
+        }
+        TaskItemView[] views = getTaskViews();
+        int delay = 0;
+        mLayoutAnimation = new AnimatorSet();
+        for (TaskItemView view : views) {
+            view.setAlpha(0.0f);
+            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
+            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
+            alphaAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    view.setAlpha(1.0f);
+                    mLayingOutViews.remove(view);
+                }
+            });
+            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
+            mLayoutAnimation.play(alphaAnim);
+            mLayingOutViews.add(view);
+        }
+        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLayoutAnimation = null;
+            }
+        });
+        mLayoutAnimation.start();
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index d831b20..572747b 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -16,15 +16,18 @@
 package com.android.quickstep.views;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
@@ -34,16 +37,41 @@
  */
 public final class TaskItemView extends LinearLayout {
 
+    private static final String EMPTY_LABEL = "";
     private static final String DEFAULT_LABEL = "...";
     private final Drawable mDefaultIcon;
+    private final Drawable mDefaultThumbnail;
+    private final TaskLayerDrawable mIconDrawable;
+    private final TaskLayerDrawable mThumbnailDrawable;
     private TextView mLabelView;
     private ImageView mIconView;
     private ImageView mThumbnailView;
+    private float mContentTransitionProgress;
+
+    /**
+     * Property representing the content transition progress of the view. 1.0f represents that the
+     * currently bound icon, thumbnail, and label are fully animated in and visible.
+     */
+    public static FloatProperty CONTENT_TRANSITION_PROGRESS =
+            new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
+                @Override
+                public void setValue(TaskItemView view, float progress) {
+                    view.setContentTransitionProgress(progress);
+                }
+
+                @Override
+                public Float get(TaskItemView view) {
+                    return view.mContentTransitionProgress;
+                }
+            };
 
     public TaskItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mDefaultIcon = context.getResources().getDrawable(
-                android.R.drawable.sym_def_app_icon, context.getTheme());
+        Resources res = context.getResources();
+        mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
+        mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
+        mIconDrawable = new TaskLayerDrawable(context);
+        mThumbnailDrawable = new TaskLayerDrawable(context);
     }
 
     @Override
@@ -52,15 +80,21 @@
         mLabelView = findViewById(R.id.task_label);
         mThumbnailView = findViewById(R.id.task_thumbnail);
         mIconView = findViewById(R.id.task_icon);
+
+        mThumbnailView.setImageDrawable(mThumbnailDrawable);
+        mIconView.setImageDrawable(mIconDrawable);
+
+        resetToEmptyUi();
+        CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
     }
 
     /**
-     * Resets task item view to default values.
+     * Resets task item view to empty, loading UI.
      */
-    public void resetTaskItemView() {
-        setLabel(DEFAULT_LABEL);
-        setIcon(null);
-        setThumbnail(null);
+    public void resetToEmptyUi() {
+        mIconDrawable.resetDrawable();
+        mThumbnailDrawable.resetDrawable();
+        setLabel(EMPTY_LABEL);
     }
 
     /**
@@ -69,11 +103,8 @@
      * @param label task label
      */
     public void setLabel(@Nullable String label) {
-        if (label == null) {
-            mLabelView.setText(DEFAULT_LABEL);
-            return;
-        }
-        mLabelView.setText(label);
+        mLabelView.setText(getSafeLabel(label));
+        // TODO: Animation for label
     }
 
     /**
@@ -86,11 +117,7 @@
         // 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);
+        mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
     }
 
     /**
@@ -99,16 +126,48 @@
      * @param thumbnail task thumbnail for the task
      */
     public void setThumbnail(@Nullable Bitmap thumbnail) {
-        if (thumbnail == null) {
-            mThumbnailView.setImageBitmap(null);
-            mThumbnailView.setBackgroundColor(Color.GRAY);
-            return;
-        }
-        mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
-        mThumbnailView.setImageBitmap(thumbnail);
+        mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnail));
     }
 
     public View getThumbnailView() {
         return mThumbnailView;
     }
+
+    /**
+     * Start a new animation from the current task content to the specified new content. The caller
+     * is responsible for the actual animation control via the property
+     * {@link #CONTENT_TRANSITION_PROGRESS}.
+     *
+     * @param endIcon the icon to animate to
+     * @param endThumbnail the thumbnail to animate to
+     * @param endLabel the label to animate to
+     */
+    public void startContentAnimation(@Nullable Drawable endIcon, @Nullable Bitmap endThumbnail,
+            @Nullable String endLabel) {
+        mIconDrawable.startNewTransition(getSafeIcon(endIcon));
+        mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
+        // TODO: Animation for label
+
+        setContentTransitionProgress(0.0f);
+    }
+
+    private void setContentTransitionProgress(float progress) {
+        mContentTransitionProgress = progress;
+        mIconDrawable.setTransitionProgress(progress);
+        mThumbnailDrawable.setTransitionProgress(progress);
+        // TODO: Animation for label
+    }
+
+    private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
+        return (icon != null) ? icon : mDefaultIcon;
+    }
+
+    private @NonNull Drawable getSafeThumbnail(@Nullable Bitmap thumbnail) {
+        return (thumbnail != null) ? new BitmapDrawable(getResources(), thumbnail)
+                                   : mDefaultThumbnail;
+    }
+
+    private @NonNull String getSafeLabel(@Nullable String label) {
+        return (label != null) ? label : DEFAULT_LABEL;
+    }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
new file mode 100644
index 0000000..98b66b9
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
@@ -0,0 +1,101 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+
+/**
+ * A layer drawable for task content that transitions between two drawables by crossfading. Similar
+ * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
+ * progress and provides a default, empty drawable.
+ */
+public final class TaskLayerDrawable extends LayerDrawable {
+    private final Drawable mEmptyDrawable;
+    private float mProgress;
+
+    public TaskLayerDrawable(Context context) {
+        super(new Drawable[0]);
+
+        // Use empty drawable for both layers initially.
+        mEmptyDrawable = context.getResources().getDrawable(
+                R.drawable.empty_content_box, context.getTheme());
+        addLayer(mEmptyDrawable);
+        addLayer(mEmptyDrawable);
+        setTransitionProgress(1.0f);
+    }
+
+    /**
+     * Immediately set the front-most drawable layer.
+     *
+     * @param drawable drawable to set
+     */
+    public void setCurrentDrawable(@NonNull Drawable drawable) {
+        setDrawable(0, drawable);
+        applyTransitionProgress(mProgress);
+    }
+
+    /**
+     * Immediately reset the drawable to showing the empty drawable.
+     */
+    public void resetDrawable() {
+        setCurrentDrawable(mEmptyDrawable);
+    }
+
+    /**
+     * Prepare to start animating the transition by pushing the current drawable to the back and
+     * setting a new drawable to the front layer and making it invisible.
+     *
+     * @param endDrawable drawable to animate to
+     */
+    public void startNewTransition(@NonNull Drawable endDrawable) {
+        Drawable oldDrawable = getDrawable(0);
+        setDrawable(1, oldDrawable);
+        setDrawable(0, endDrawable);
+        setTransitionProgress(0.0f);
+    }
+
+    /**
+     * Set the progress of the transition animation to crossfade the two drawables.
+     *
+     * @param progress current transition progress between 0 (front view invisible) and 1
+     *                 (front view visible)
+     */
+    public void setTransitionProgress(float progress) {
+        if (progress > 1 || progress < 0) {
+            throw new IllegalArgumentException("Transition progress should be between 0 and 1");
+        }
+        mProgress = progress;
+        applyTransitionProgress(progress);
+    }
+
+    private void applyTransitionProgress(float progress) {
+        int drawableAlpha = (int) (progress * 255);
+        getDrawable(0).setAlpha(drawableAlpha);
+        if (getDrawable(0) != getDrawable(1)) {
+            // Only do this if it's a different drawable so that it fades out.
+            // Otherwise, we'd just be overwriting the front drawable's alpha.
+            getDrawable(1).setAlpha(255 - drawableAlpha);
+        }
+        invalidateSelf();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 446fb39..b9791bf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -59,7 +59,6 @@
     private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
 
     private Task mTask;
-    private ImageView mImage;
     private TextView mText;
 
     public DigitalWellBeingToast(Context context, AttributeSet attrs) {
@@ -75,7 +74,6 @@
         super.onFinishInflate();
 
         mText = findViewById(R.id.digital_well_being_remaining_time);
-        mImage = findViewById(R.id.digital_well_being_hourglass);
     }
 
     public void initialize(Task task, InitializeCallback callback) {
@@ -103,8 +101,6 @@
                 } else {
                     setVisibility(VISIBLE);
                     mText.setText(getText(appRemainingTimeMs));
-                    mImage.setImageResource(appRemainingTimeMs > 0 ?
-                            R.drawable.hourglass_top : R.drawable.hourglass_bottom);
                 }
 
                 callback.call(getContentDescriptionForTask(
diff --git a/quickstep/res/drawable/hourglass_bottom.xml b/quickstep/res/drawable/hourglass_bottom.xml
deleted file mode 100644
index b5ef008..0000000
--- a/quickstep/res/drawable/hourglass_bottom.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24"
-        android:viewportHeight="24">
-    <group>
-        <clip-path android:pathData="M0,0H24V24H0Z M 0,0"/>
-        <path
-            android:fillColor="#FFFFFFFF"
-            android:pathData="M6,2V8H6l4,4L6,16H6v6H18V16h0l-4,-4,4,-4h0V2Zm6,9.5,-4,-4V4h8V7.5Z"/>
-    </group>
-</vector>
diff --git a/quickstep/res/drawable/hourglass_top.xml b/quickstep/res/drawable/ic_hourglass_top.xml
similarity index 100%
rename from quickstep/res/drawable/hourglass_top.xml
rename to quickstep/res/drawable/ic_hourglass_top.xml
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index f96a66f..1d1c272 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -49,6 +49,7 @@
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:layout_marginEnd="8dp"
+            android:src="@drawable/ic_hourglass_top"
         />
         <TextView
             android:id="@+id/digital_well_being_remaining_time"