Implementing Recents focus states. (Bug 16950262)

- Fixed issue with focus going to send front most task when coming from Home
- Adding shortcut to dismiss a task
- Refactoring code to get secondary overlay and affiliation color
- Throttling alt-tab key presses
- Fixing issue with slivers of the task thumbnail being visible for affiliated tasks

Change-Id: Iaafe408318646a423fd58b51bbe93dbe0f2eed99
diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
index efb7a2c..354eb55 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
@@ -345,8 +345,8 @@
      * Creates the activity options for an app->recents transition.  If this method sets the static
      * screenshot, then we will use that for the transition.
      */
-    ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask) {
-
+    ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask,
+            boolean isTopTaskHome) {
         if (Constants.DebugFlags.App.EnableScreenshotAppTransition) {
             // Recycle the last screenshot
             consumeLastScreenshot();
@@ -365,7 +365,7 @@
         Bitmap firstThumbnail = mSystemServicesProxy.getTaskThumbnail(topTask.id);
         if (firstThumbnail != null) {
             // Update the destination rect
-            Rect toTaskRect = getThumbnailTransitionRect(topTask.id);
+            Rect toTaskRect = getThumbnailTransitionRect(topTask.id, isTopTaskHome);
             if (toTaskRect.width() > 0 && toTaskRect.height() > 0) {
                 // Create the new thumbnail for the animation down
                 // XXX: We should find a way to optimize this so we don't need to create a new bitmap
@@ -389,7 +389,7 @@
     }
 
     /** Returns the transition rect for the given task id. */
-    Rect getThumbnailTransitionRect(int runningTaskId) {
+    Rect getThumbnailTransitionRect(int runningTaskId, boolean isTopTaskHome) {
         // Get the stack of tasks that we are animating into
         TaskStack stack = RecentsTaskLoader.getShallowTaskStack(mSystemServicesProxy, -1);
         if (stack.getTaskCount() == 0) {
@@ -401,7 +401,8 @@
         TaskStackViewLayoutAlgorithm algo = tsv.getStackAlgorithm();
         Rect taskStackBounds = new Rect(mTaskStackBounds);
         taskStackBounds.bottom -= mSystemInsets.bottom;
-        tsv.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds, mTriggeredFromAltTab);
+        tsv.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds,
+                mTriggeredFromAltTab, isTopTaskHome);
         tsv.getScroller().setStackScrollToInitialState();
 
         // Find the running task in the TaskStack
@@ -442,7 +443,7 @@
 
         if (useThumbnailTransition) {
             // Try starting with a thumbnail transition
-            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask);
+            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, isTopTaskHome);
             if (opts != null) {
                 if (sLastScreenshot != null) {
                     startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_FULL_SCREENSHOT);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index 417049c..41e06de 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -61,6 +61,7 @@
 
     RecentsConfiguration mConfig;
     boolean mVisible;
+    long mLastTabKeyEventTime;
 
     // Top level views
     RecentsView mRecentsView;
@@ -512,17 +513,33 @@
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_TAB) {
-            // Focus the next task in the stack
-            final boolean backward = event.isShiftPressed();
-            mRecentsView.focusNextTask(!backward);
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
-            mRecentsView.focusNextTask(true);
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
-            mRecentsView.focusNextTask(false);
-            return true;
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_TAB: {
+                boolean hasRepKeyTimeElapsed = (System.currentTimeMillis() -
+                        mLastTabKeyEventTime) > mConfig.altTabKeyDelay;
+                if (event.getRepeatCount() <= 0 || hasRepKeyTimeElapsed) {
+                    // Focus the next task in the stack
+                    final boolean backward = event.isShiftPressed();
+                    mRecentsView.focusNextTask(!backward);
+                    mLastTabKeyEventTime = System.currentTimeMillis();
+                }
+                return true;
+            }
+            case KeyEvent.KEYCODE_DPAD_UP: {
+                mRecentsView.focusNextTask(true);
+                return true;
+            }
+            case KeyEvent.KEYCODE_DPAD_DOWN: {
+                mRecentsView.focusNextTask(false);
+                return true;
+            }
+            case KeyEvent.KEYCODE_DEL:
+            case KeyEvent.KEYCODE_FORWARD_DEL: {
+                mRecentsView.dismissFocusedTask();
+                return true;
+            }
+            default:
+                break;
         }
         // Pass through the debug trigger
         mDebugTrigger.onKeyEvent(keyCode);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
index 65e7076..3f5018d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
@@ -112,6 +112,9 @@
     public boolean launchedFromHome;
     public int launchedToTaskId;
 
+    /** Misc **/
+    public int altTabKeyDelay;
+
     /** Dev options and global settings */
     public boolean lockToAppEnabled;
     public boolean developerOptionsEnabled;
@@ -250,6 +253,9 @@
         // Nav bar scrim
         navBarScrimEnterDuration =
                 res.getInteger(R.integer.recents_nav_bar_scrim_enter_duration);
+
+        // Misc
+        altTabKeyDelay = res.getInteger(R.integer.recents_alt_tab_key_delay);
     }
 
     /** Updates the system insets */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
index bd5df28..4c6b389 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
@@ -89,6 +89,17 @@
         return Math.abs((fgL + 0.05f) / (bgL + 0.05f));
     }
 
+    /** Returns the base color overlaid with another overlay color with a specified alpha. */
+    public static int getColorWithOverlay(int baseColor, int overlayColor, float overlayAlpha) {
+        return Color.rgb(
+            (int) (overlayAlpha * Color.red(baseColor) +
+                    (1f - overlayAlpha) * Color.red(overlayColor)),
+            (int) (overlayAlpha * Color.green(baseColor) +
+                    (1f - overlayAlpha) * Color.green(overlayColor)),
+            (int) (overlayAlpha * Color.blue(baseColor) +
+                    (1f - overlayAlpha) * Color.blue(overlayColor)));
+    }
+
     /** Sets some private shadow properties. */
     public static void setShadowProperty(String property, String value)
             throws IllegalAccessException, InvocationTargetException {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 0269141..98bf151 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -20,6 +20,7 @@
 import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.RecentsConfiguration;
 import com.android.systemui.recents.misc.NamedCounter;
+import com.android.systemui.recents.misc.Utilities;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -433,10 +434,8 @@
                 float alpha = 1f;
                 for (int j = 0; j < taskCount; j++) {
                     Task t = tasksMap.get(group.mTaskKeys.get(j));
-                    t.colorPrimary = Color.rgb(
-                            (int) (alpha * Color.red(affiliationColor) + (1f - alpha) * 0xFF),
-                            (int) (alpha * Color.green(affiliationColor) + (1f - alpha) * 0xFF),
-                            (int) (alpha * Color.blue(affiliationColor) + (1f - alpha) * 0xFF));
+                    t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE,
+                            alpha);
                     alpha -= alphaStep;
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 34e8860..07a7e74 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -316,12 +316,11 @@
     /** Notifies each task view of the user interaction. */
     public void onUserInteraction() {
         // Get the first stack view
-        TaskStackView stackView = null;
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
             if (child != mSearchBar) {
-                stackView = (TaskStackView) child;
+                TaskStackView stackView = (TaskStackView) child;
                 stackView.onUserInteraction();
             }
         }
@@ -330,18 +329,28 @@
     /** Focuses the next task in the first stack view */
     public void focusNextTask(boolean forward) {
         // Get the first stack view
-        TaskStackView stackView = null;
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
             if (child != mSearchBar) {
-                stackView = (TaskStackView) child;
+                TaskStackView stackView = (TaskStackView) child;
+                stackView.focusNextTask(forward);
                 break;
             }
         }
+    }
 
-        if (stackView != null) {
-            stackView.focusNextTask(forward);
+    /** Dismisses the focused task. */
+    public void dismissFocusedTask() {
+        // Get the first stack view
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child != mSearchBar) {
+                TaskStackView stackView = (TaskStackView) child;
+                stackView.dismissFocusedTask();
+                break;
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 46996bb..4fd9136 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -349,9 +349,10 @@
     }
 
     /** Updates the min and max virtual scroll bounds */
-    void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab) {
+    void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab,
+            boolean launchedFromHome) {
         // Compute the min and max scroll values
-        mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab);
+        mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab, launchedFromHome);
 
         // Debug logging
         if (boundScrollToNewMinMax) {
@@ -366,6 +367,9 @@
 
     /** Focuses the task at the specified index in the stack */
     void focusTask(int taskIndex, boolean scrollToNewPosition) {
+        // Return early if the task is already focused
+        if (taskIndex == mFocusedTaskIndex) return;
+
         if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) {
             mFocusedTaskIndex = taskIndex;
 
@@ -406,14 +410,24 @@
     void focusNextTask(boolean forward) {
         // Find the next index to focus
         int numTasks = mStack.getTaskCount();
-        if (mFocusedTaskIndex < 0) {
-            mFocusedTaskIndex = numTasks - 1;
-        }
+        if (numTasks == 0) return;
+
+        int nextFocusIndex = numTasks - 1;
         if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) {
-            mFocusedTaskIndex = Math.max(0, Math.min(numTasks - 1,
+            nextFocusIndex = Math.max(0, Math.min(numTasks - 1,
                     mFocusedTaskIndex + (forward ? -1 : 1)));
         }
-        focusTask(mFocusedTaskIndex, true);
+        focusTask(nextFocusIndex, true);
+    }
+
+    /** Dismisses the focused task. */
+    public void dismissFocusedTask() {
+        // Return early if there is no focused task index
+        if (mFocusedTaskIndex < 0) return;
+
+        Task t = mStack.getTasks().get(mFocusedTaskIndex);
+        TaskView tv = getChildViewForTask(t);
+        tv.dismissTask();
     }
 
     @Override
@@ -436,12 +450,12 @@
 
     /** Computes the stack and task rects */
     public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds,
-                             boolean launchedWithAltTab) {
+            boolean launchedWithAltTab, boolean launchedFromHome) {
         // Compute the rects in the stack algorithm
         mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds);
 
         // Update the scroll bounds
-        updateMinMaxScroll(false, launchedWithAltTab);
+        updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome);
     }
 
     /**
@@ -456,7 +470,8 @@
         // Compute our stack/task rects
         Rect taskStackBounds = new Rect(mTaskStackBounds);
         taskStackBounds.bottom -= mConfig.systemInsets.bottom;
-        computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab);
+        computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab,
+                mConfig.launchedFromHome);
 
         // If this is the first layout, then scroll to the front of the stack and synchronize the
         // stack views immediately to load all the views
@@ -546,7 +561,11 @@
 
         // When Alt-Tabbing, we scroll to and focus the previous task
         if (mConfig.launchedWithAltTab) {
-            focusTask(Math.max(0, mStack.getTaskCount() - 2), false);
+            if (mConfig.launchedFromHome) {
+                focusTask(Math.max(0, mStack.getTaskCount() - 1), false);
+            } else {
+                focusTask(Math.max(0, mStack.getTaskCount() - 2), false);
+            }
         }
     }
 
@@ -661,7 +680,7 @@
         mCb.onTaskViewDismissed(removedTask);
 
         // Update the min/max scroll and animate other task views into their new positions
-        updateMinMaxScroll(true, mConfig.launchedWithAltTab);
+        updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome);
         requestSynchronizeStackViewsWithModel(200);
 
         // Update the new front most task
@@ -859,11 +878,23 @@
     @Override
     public void onTaskViewDismissed(TaskView tv) {
         Task task = tv.getTask();
+        int taskIndex = mStack.indexOfTask(task);
+        boolean taskWasFocused = tv.isFocusedTask();
         // Announce for accessibility
         tv.announceForAccessibility(getContext().getString(R.string.accessibility_recents_item_dismissed,
                 tv.getTask().activityLabel));
         // Remove the task from the view
         mStack.removeTask(task);
+        // If the dismissed task was focused, then we should focus the next task in front
+        if (taskWasFocused) {
+            ArrayList<Task> tasks = mStack.getTasks();
+            int nextTaskIndex = Math.min(tasks.size() - 1, taskIndex);
+            if (nextTaskIndex >= 0) {
+                Task nextTask = tasks.get(nextTaskIndex);
+                TaskView nextTv = getChildViewForTask(nextTask);
+                nextTv.setFocusedTask();
+            }
+        }
     }
 
     @Override
@@ -876,6 +907,13 @@
         requestSynchronizeStackViewsWithModel();
     }
 
+    @Override
+    public void onTaskViewFocusChanged(TaskView tv, boolean focused) {
+        if (focused) {
+            mFocusedTaskIndex = mStack.indexOfTask(tv.getTask());
+        }
+    }
+
     /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
index 6c8de72..495d00b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
@@ -91,7 +91,8 @@
 
     /** Computes the minimum and maximum scroll progress values.  This method may be called before
      * the RecentsConfiguration is set, so we need to pass in the alt-tab state. */
-    void computeMinMaxScroll(ArrayList<Task> tasks, boolean launchedWithAltTab) {
+    void computeMinMaxScroll(ArrayList<Task> tasks, boolean launchedWithAltTab,
+            boolean launchedFromHome) {
         // Clear the progress map
         mTaskProgressMap.clear();
 
@@ -101,10 +102,16 @@
             return;
         }
 
+        // Note that we should account for the scale difference of the offsets at the screen bottom
         int taskHeight = mTaskRect.height();
         float pAtBottomOfStackRect = screenYToCurveProgress(mStackVisibleRect.bottom);
-        float pWithinAffiliateOffset = pAtBottomOfStackRect -
-                screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset);
+        float pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom -
+                mWithinAffiliationOffset);
+        float scale = curveProgressToScale(pWithinAffiliateTop);
+        int scaleYOffset = (int) (((1f - scale) * taskHeight) / 2);
+        pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom -
+                mWithinAffiliationOffset + scaleYOffset);
+        float pWithinAffiliateOffset = pAtBottomOfStackRect - pWithinAffiliateTop;
         float pBetweenAffiliateOffset = pAtBottomOfStackRect -
                 screenYToCurveProgress(mStackVisibleRect.bottom - mBetweenAffiliationOffset);
         float pTaskHeightOffset = pAtBottomOfStackRect -
@@ -133,8 +140,13 @@
         mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset));
         mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f;
         if (launchedWithAltTab) {
-            // Center the second most task, since that will be focused first
-            mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f;
+            if (launchedFromHome) {
+                // Center the top most task, since that will be focused first
+                mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f;
+            } else {
+                // Center the second top most task, since that will be focused first
+                mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f;
+            }
         } else {
             mInitialScrollP = pAtFrontMostCardTop - 0.825f;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
index 0a12dab..2c0dc44 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -133,7 +133,7 @@
         stopBoundScrollAnimation();
 
         mScrollAnimator = ObjectAnimator.ofFloat(this, "stackScroll", curScroll, newScroll);
-        mScrollAnimator.setDuration(250);
+        mScrollAnimator.setDuration(200);
         // We would have to project the difference into the screen coords, and then use that as the
         // duration
 //        mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll -
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 3b9bcc4..236229e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -48,6 +48,7 @@
         public void onTaskViewDismissed(TaskView tv);
         public void onTaskViewClipStateChanged(TaskView tv);
         public void onTaskViewFullScreenTransitionCompleted();
+        public void onTaskViewFocusChanged(TaskView tv, boolean focused);
     }
 
     RecentsConfiguration mConfig;
@@ -62,13 +63,14 @@
     Task mTask;
     boolean mTaskDataLoaded;
     boolean mIsFocused;
+    boolean mFocusAnimationsEnabled;
     boolean mIsFullScreenView;
     boolean mClipViewInStack;
     AnimateableViewBounds mViewBounds;
     Paint mLayerPaint = new Paint();
 
     TaskViewThumbnail mThumbnailView;
-    TaskViewHeader mBarView;
+    TaskViewHeader mHeaderView;
     TaskViewFooter mFooterView;
     View mActionButtonView;
     TaskViewCallbacks mCb;
@@ -124,7 +126,7 @@
     @Override
     protected void onFinishInflate() {
         // Bind the views
-        mBarView = (TaskViewHeader) findViewById(R.id.task_view_bar);
+        mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar);
         mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail);
         mActionButtonView = findViewById(R.id.lock_to_app_fab);
         if (mFooterView != null) {
@@ -138,7 +140,7 @@
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
         // Measure the bar view, thumbnail, and footer
-        mBarView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+        mHeaderView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                 MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY));
         if (mFooterView != null) {
             mFooterView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
@@ -218,11 +220,11 @@
 
     /** Prepares this task view for the enter-recents animations.  This is called earlier in the
      * first layout because the actual animation into recents may take a long time. */
-    public void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask,
+    void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask,
                                              boolean occludesLaunchTarget, int offscreenY) {
         if (mConfig.launchedFromAppWithScreenshot) {
             if (isTaskViewLaunchTargetTask) {
-                mBarView.prepareEnterRecentsAnimation();
+                mHeaderView.prepareEnterRecentsAnimation();
                 // Hide the footer during the transition in, and animate it out afterwards?
                 if (mFooterView != null) {
                     mFooterView.animateFooterVisibility(false, 0);
@@ -234,7 +236,7 @@
         } else if (mConfig.launchedFromAppWithThumbnail) {
             if (isTaskViewLaunchTargetTask) {
                 // Hide the front most task bar view so we can animate it in
-                mBarView.prepareEnterRecentsAnimation();
+                mHeaderView.prepareEnterRecentsAnimation();
                 // Hide the action button if it exists
                 mActionButtonView.setAlpha(0f);
                 // Set the dim to 0 so we can animate it in
@@ -256,8 +258,9 @@
     }
 
     /** Animates this task view as it enters recents */
-    public void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
+    void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
         final TaskViewTransform transform = ctx.currentTaskTransform;
+        int startDelay = 0;
 
         if (mConfig.launchedFromAppWithScreenshot) {
             if (mTask.isLaunchTarget) {
@@ -269,6 +272,7 @@
                 float scaledWindowInsetTop = (int) (taskScale * windowInsetTop);
                 float scaledTranslationY = taskRect.top + transform.translationY -
                         (scaledWindowInsetTop + scaledYOffset);
+                startDelay = mConfig.taskViewEnterFromHomeDelay;
 
                 // Animate the top clip
                 mViewBounds.animateClipTop(windowInsetTop, duration,
@@ -276,7 +280,7 @@
                     @Override
                     public void onAnimationUpdate(ValueAnimator animation) {
                         int y = (Integer) animation.getAnimatedValue();
-                        mBarView.setTranslationY(y);
+                        mHeaderView.setTranslationY(y);
                     }
                 });
                 // Animate the bottom or right clip
@@ -287,7 +291,7 @@
                     mViewBounds.animateClipBottom(getMeasuredHeight() - (windowInsetTop + size), duration);
                 }
                 // Animate the task bar of the first task view
-                mBarView.startEnterRecentsAnimation(0, null);
+                mHeaderView.startEnterRecentsAnimation(0, null);
                 animate()
                         .scaleX(taskScale)
                         .scaleY(taskScale)
@@ -304,9 +308,9 @@
                                 mViewBounds.setClipBottom(0);
                                 mViewBounds.setClipRight(0);
                                 // Reset the bar translation
-                                mBarView.setTranslationY(0);
+                                mHeaderView.setTranslationY(0);
                                 // Enable the thumbnail clip
-                                mThumbnailView.enableTaskBarClip(mBarView);
+                                mThumbnailView.enableTaskBarClip(mHeaderView);
                                 // Animate the footer into view (if it is the front most task)
                                 animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration);
 
@@ -324,7 +328,7 @@
                         .start();
             } else {
                 // Otherwise, just enable the thumbnail clip
-                mThumbnailView.enableTaskBarClip(mBarView);
+                mThumbnailView.enableTaskBarClip(mHeaderView);
 
                 // Animate the footer into view
                 animateFooterVisibility(true, 0);
@@ -334,8 +338,8 @@
         } else if (mConfig.launchedFromAppWithThumbnail) {
             if (mTask.isLaunchTarget) {
                 // Animate the task bar of the first task view
-                mBarView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay,
-                        mThumbnailView.enableTaskBarClipAsRunnable(mBarView));
+                mHeaderView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay,
+                        mThumbnailView.enableTaskBarClipAsRunnable(mHeaderView));
 
                 // Animate the dim into view as well
                 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimFromTaskProgress());
@@ -364,7 +368,7 @@
                         .start();
             } else {
                 // Enable the task bar clip
-                mThumbnailView.enableTaskBarClip(mBarView);
+                mThumbnailView.enableTaskBarClip(mHeaderView);
                 // Animate the task up if it was occluding the launch target
                 if (ctx.currentTaskOccludesLaunchTarget) {
                     setTranslationY(transform.translationY + mConfig.taskViewAffiliateGroupEnterOffsetPx);
@@ -378,7 +382,7 @@
                             .withEndAction(new Runnable() {
                                 @Override
                                 public void run() {
-                                    mThumbnailView.enableTaskBarClip(mBarView);
+                                    mThumbnailView.enableTaskBarClip(mHeaderView);
                                     // Decrement the post animation trigger
                                     ctx.postAnimationTrigger.decrement();
                                 }
@@ -387,6 +391,7 @@
                     ctx.postAnimationTrigger.increment();
                 }
             }
+            startDelay = mConfig.taskBarEnterAnimDelay;
 
         } else if (mConfig.launchedFromHome) {
             // Animate the tasks up
@@ -407,7 +412,7 @@
                     .withEndAction(new Runnable() {
                         @Override
                         public void run() {
-                            mThumbnailView.enableTaskBarClip(mBarView);
+                            mThumbnailView.enableTaskBarClip(mHeaderView);
                             // Decrement the post animation trigger
                             ctx.postAnimationTrigger.decrement();
                         }
@@ -417,18 +422,28 @@
 
             // Animate the footer into view
             animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration);
+            startDelay = delay;
 
         } else {
             // Otherwise, just enable the thumbnail clip
-            mThumbnailView.enableTaskBarClip(mBarView);
+            mThumbnailView.enableTaskBarClip(mHeaderView);
 
             // Animate the footer into view
             animateFooterVisibility(true, 0);
         }
+
+        // Enable the focus animations from this point onwards so that they aren't affected by the
+        // window transitions
+        postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                enableFocusAnimations();
+            }
+        }, (startDelay / 2));
     }
 
     /** Animates this task view as it leaves recents by pressing home. */
-    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
+    void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
         animate()
                 .translationY(ctx.offscreenTranslationY)
                 .setStartDelay(0)
@@ -441,11 +456,11 @@
     }
 
     /** Animates this task view as it exits recents */
-    public void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask,
-                                         boolean occludesLaunchTarget) {
+    void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask,
+            boolean occludesLaunchTarget) {
         if (isLaunchingTask) {
             // Disable the thumbnail clip and animate the bar out
-            mBarView.startLaunchTaskAnimation(mThumbnailView.disableTaskBarClipAsRunnable(), r);
+            mHeaderView.startLaunchTaskAnimation(mThumbnailView.disableTaskBarClipAsRunnable(), r);
 
             // Animate the dim
             if (mDim > 0) {
@@ -464,7 +479,7 @@
                     .start();
         } else {
             // Hide the dismiss button
-            mBarView.startLaunchTaskDismissAnimation();
+            mHeaderView.startLaunchTaskDismissAnimation();
             // If this is another view in the task grouping and is in front of the launch task,
             // animate it away first
             if (occludesLaunchTarget) {
@@ -480,7 +495,7 @@
     }
 
     /** Animates the deletion of this task view */
-    public void startDeleteTaskAnimation(final Runnable r) {
+    void startDeleteTaskAnimation(final Runnable r) {
         // Disabling clipping with the stack while the view is animating away
         setClipViewInStack(false);
 
@@ -508,19 +523,33 @@
     }
 
     /** Animates this task view if the user does not interact with the stack after a certain time. */
-    public void startNoUserInteractionAnimation() {
-        mBarView.startNoUserInteractionAnimation();
+    void startNoUserInteractionAnimation() {
+        mHeaderView.startNoUserInteractionAnimation();
     }
 
     /** Mark this task view that the user does has not interacted with the stack after a certain time. */
-    public void setNoUserInteractionState() {
-        mBarView.setNoUserInteractionState();
+    void setNoUserInteractionState() {
+        mHeaderView.setNoUserInteractionState();
+    }
+
+    /** Dismisses this task. */
+    void dismissTask() {
+        // Animate out the view and call the callback
+        final TaskView tv = this;
+        startDeleteTaskAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mCb.onTaskViewDismissed(tv);
+            }
+        });
+        // Hide the footer
+        animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration);
     }
 
     /** Sets whether this task view is full screen or not. */
     void setIsFullScreen(boolean isFullscreen) {
         mIsFullScreenView = isFullscreen;
-        mBarView.setIsFullscreen(isFullscreen);
+        mHeaderView.setIsFullscreen(isFullscreen);
         if (isFullscreen) {
             // If we are full screen, then disable the bottom outline clip for the footer
             mViewBounds.setOutlineClipBottom(0);
@@ -614,6 +643,12 @@
      */
     public void setFocusedTask() {
         mIsFocused = true;
+        if (mFocusAnimationsEnabled) {
+            // Focus the header bar
+            mHeaderView.onTaskViewFocusChanged(true);
+        }
+        // Call the callback
+        mCb.onTaskViewFocusChanged(this, true);
         // Workaround, we don't always want it focusable in touch mode, but we want the first task
         // to be focused after the enter-recents animation, which can be triggered from either touch
         // or keyboard
@@ -631,6 +666,12 @@
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         if (!gainFocus) {
             mIsFocused = false;
+            if (mFocusAnimationsEnabled) {
+                // Un-focus the header bar
+                mHeaderView.onTaskViewFocusChanged(false);
+            }
+            // Call the callback
+            mCb.onTaskViewFocusChanged(this, false);
             invalidate();
         }
     }
@@ -642,6 +683,16 @@
         return mIsFocused || isFocused();
     }
 
+    /** Enables all focus animations. */
+    void enableFocusAnimations() {
+        boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled;
+        mFocusAnimationsEnabled = true;
+        if (mIsFocused && !wasFocusAnimationsEnabled) {
+            // Re-notify the header if we were focused and animations were not previously enabled
+            mHeaderView.onTaskViewFocusChanged(true);
+        }
+    }
+
     /**** TaskCallbacks Implementation ****/
 
     /** Binds this task view to the task */
@@ -662,24 +713,24 @@
 
     @Override
     public void onTaskDataLoaded() {
-        if (mThumbnailView != null && mBarView != null) {
+        if (mThumbnailView != null && mHeaderView != null) {
             // Bind each of the views to the new task data
             if (mIsFullScreenView) {
                 mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot());
             } else {
                 mThumbnailView.rebindToTask(mTask);
             }
-            mBarView.rebindToTask(mTask);
+            mHeaderView.rebindToTask(mTask);
             // Rebind any listeners
-            mBarView.mApplicationIcon.setOnClickListener(this);
-            mBarView.mDismissButton.setOnClickListener(this);
+            mHeaderView.mApplicationIcon.setOnClickListener(this);
+            mHeaderView.mDismissButton.setOnClickListener(this);
             if (mFooterView != null) {
                 mFooterView.setOnClickListener(this);
             }
             mActionButtonView.setOnClickListener(this);
             if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
                 if (mConfig.developerOptionsEnabled) {
-                    mBarView.mApplicationIcon.setOnLongClickListener(this);
+                    mHeaderView.mApplicationIcon.setOnLongClickListener(this);
                 }
             }
         }
@@ -688,20 +739,20 @@
 
     @Override
     public void onTaskDataUnloaded() {
-        if (mThumbnailView != null && mBarView != null) {
+        if (mThumbnailView != null && mHeaderView != null) {
             // Unbind each of the views from the task data and remove the task callback
             mTask.setCallbacks(null);
             mThumbnailView.unbindFromTask();
-            mBarView.unbindFromTask();
+            mHeaderView.unbindFromTask();
             // Unbind any listeners
-            mBarView.mApplicationIcon.setOnClickListener(null);
-            mBarView.mDismissButton.setOnClickListener(null);
+            mHeaderView.mApplicationIcon.setOnClickListener(null);
+            mHeaderView.mDismissButton.setOnClickListener(null);
             if (mFooterView != null) {
                 mFooterView.setOnClickListener(null);
             }
             mActionButtonView.setOnClickListener(null);
             if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
-                mBarView.mApplicationIcon.setOnLongClickListener(null);
+                mHeaderView.mApplicationIcon.setOnLongClickListener(null);
             }
         }
         mTaskDataLoaded = false;
@@ -734,18 +785,10 @@
         postDelayed(new Runnable() {
             @Override
             public void run() {
-                if (Constants.DebugFlags.App.EnableTaskFiltering && v == mBarView.mApplicationIcon) {
+                if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) {
                     mCb.onTaskViewAppIconClicked(tv);
-                } else if (v == mBarView.mDismissButton) {
-                    // Animate out the view and call the callback
-                    startDeleteTaskAnimation(new Runnable() {
-                        @Override
-                        public void run() {
-                            mCb.onTaskViewDismissed(tv);
-                        }
-                    });
-                    // Hide the footer
-                    tv.animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration);
+                } else if (v == mHeaderView.mDismissButton) {
+                    dismissTask();
                 } else {
                     mCb.onTaskViewClicked(tv, tv.getTask(),
                             (v == mFooterView || v == mActionButtonView));
@@ -758,7 +801,7 @@
 
     @Override
     public boolean onLongClick(View v) {
-        if (v == mBarView.mApplicationIcon) {
+        if (v == mHeaderView.mApplicationIcon) {
             mCb.onTaskViewAppInfoClicked(this);
             return true;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index 03fc16e..4b09549 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -16,9 +16,16 @@
 
 package com.android.systemui.recents.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
@@ -28,12 +35,14 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
 import com.android.systemui.R;
 import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
 
 
@@ -46,10 +55,15 @@
     ImageView mApplicationIcon;
     TextView mActivityDescription;
 
+    RippleDrawable mBackground;
+    ColorDrawable mBackgroundColor;
     Drawable mLightDismissDrawable;
     Drawable mDarkDismissDrawable;
+    ValueAnimator mBackgroundColorAnimator;
 
     boolean mIsFullscreen;
+    boolean mCurrentPrimaryColorIsDark;
+    int mCurrentPrimaryColor;
 
     static Paint sHighlightPaint;
 
@@ -69,6 +83,13 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         mConfig = RecentsConfiguration.getInstance();
         setWillNotDraw(false);
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight());
+            }
+        });
 
         // Load the dismiss resources
         Resources res = context.getResources();
@@ -107,6 +128,15 @@
                 mApplicationIcon.setBackground(null);
             }
         }
+
+        mBackgroundColor = new ColorDrawable(0);
+        // Copy the ripple drawable since we are going to be manipulating it
+        mBackground = (RippleDrawable)
+                getResources().getDrawable(R.drawable.recents_task_view_header_bg);
+        mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable();
+        mBackground.setColor(ColorStateList.valueOf(0));
+        mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColor);
+        setBackground(mBackground);
     }
 
     @Override
@@ -130,6 +160,12 @@
         return false;
     }
 
+    /** Returns the secondary color for a primary color. */
+    int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) {
+        int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK;
+        return Utilities.getColorWithOverlay(primaryColor, overlayColor, 0.8f);
+    }
+
     /** Binds the bar view to the task */
     void rebindToTask(Task t) {
         // If an activity icon is defined, then we use that as the primary icon to show in the bar,
@@ -147,8 +183,10 @@
         int existingBgColor = (getBackground() instanceof ColorDrawable) ?
                 ((ColorDrawable) getBackground()).getColor() : 0;
         if (existingBgColor != t.colorPrimary) {
-            setBackgroundColor(t.colorPrimary);
+            mBackgroundColor.setColor(t.colorPrimary);
         }
+        mCurrentPrimaryColor = t.colorPrimary;
+        mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor;
         mActivityDescription.setTextColor(t.useLightOnPrimaryColor ?
                 mConfig.taskBarViewLightTextColor : mConfig.taskBarViewDarkTextColor);
         mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ?
@@ -165,12 +203,12 @@
 
     /** Prepares this task view for the enter-recents animations.  This is called earlier in the
      * first layout because the actual animation into recents may take a long time. */
-    public void prepareEnterRecentsAnimation() {
+    void prepareEnterRecentsAnimation() {
         setVisibility(View.INVISIBLE);
     }
 
     /** Animates this task bar as it enters recents */
-    public void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) {
+    void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) {
         // Animate the task bar of the first task view
         setVisibility(View.VISIBLE);
         setTranslationY(-getMeasuredHeight());
@@ -184,7 +222,7 @@
     }
 
     /** Animates this task bar as it exits recents */
-    public void startLaunchTaskAnimation(Runnable preAnimRunnable, final Runnable postAnimRunnable) {
+    void startLaunchTaskAnimation(Runnable preAnimRunnable, final Runnable postAnimRunnable) {
         // Animate the task bar out of the first task view
         animate()
                 .translationY(-getMeasuredHeight())
@@ -202,7 +240,7 @@
     }
 
     /** Animates this task bar dismiss button when launching a task. */
-    public void startLaunchTaskDismissAnimation() {
+    void startLaunchTaskDismissAnimation() {
         if (mDismissButton.getVisibility() == View.VISIBLE) {
             mDismissButton.animate().cancel();
             mDismissButton.animate()
@@ -216,7 +254,7 @@
     }
 
     /** Animates this task bar if the user does not interact with the stack after a certain time. */
-    public void startNoUserInteractionAnimation() {
+    void startNoUserInteractionAnimation() {
         mDismissButton.setVisibility(View.VISIBLE);
         mDismissButton.setAlpha(0f);
         mDismissButton.animate()
@@ -229,11 +267,78 @@
     }
 
     /** Mark this task view that the user does has not interacted with the stack after a certain time. */
-    public void setNoUserInteractionState() {
+    void setNoUserInteractionState() {
         if (mDismissButton.getVisibility() != View.VISIBLE) {
             mDismissButton.animate().cancel();
             mDismissButton.setVisibility(View.VISIBLE);
             mDismissButton.setAlpha(1f);
         }
     }
+
+    /** Notifies the associated TaskView has been focused. */
+    void onTaskViewFocusChanged(boolean focused) {
+        boolean isRunning = false;
+        if (mBackgroundColorAnimator != null) {
+            isRunning = mBackgroundColorAnimator.isRunning();
+            mBackgroundColorAnimator.removeAllUpdateListeners();
+            mBackgroundColorAnimator.cancel();
+        }
+        if (focused) {
+            int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
+            int[][] states = new int[][] {
+                    new int[] { android.R.attr.state_enabled },
+                    new int[] { android.R.attr.state_pressed }
+            };
+            int[] newStates = new int[]{
+                    android.R.attr.state_enabled,
+                    android.R.attr.state_pressed
+            };
+            int[] colors = new int[] {
+                    secondaryColor,
+                    secondaryColor
+            };
+            mBackground.setColor(new ColorStateList(states, colors));
+            mBackground.setState(newStates);
+            // Pulse the background color
+            int currentColor = mBackgroundColor.getColor();
+            int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
+            mBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), lightPrimaryColor,
+                    currentColor);
+            mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    mBackground.setState(new int[] {});
+                }
+            });
+            mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    mBackgroundColor.setColor((Integer) animation.getAnimatedValue());
+                }
+            });
+            mBackgroundColorAnimator.setRepeatCount(ValueAnimator.INFINITE);
+            mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+            mBackgroundColorAnimator.setStartDelay(750);
+            mBackgroundColorAnimator.setDuration(750);
+            mBackgroundColorAnimator.start();
+        } else {
+            if (isRunning) {
+                // Restore the background color
+                int currentColor = mBackgroundColor.getColor();
+                mBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), currentColor,
+                        mCurrentPrimaryColor);
+                mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        mBackgroundColor.setColor((Integer) animation.getAnimatedValue());
+                    }
+                });
+                mBackgroundColorAnimator.setRepeatCount(0);
+                mBackgroundColorAnimator.setDuration(150);
+                mBackgroundColorAnimator.start();
+            } else {
+                mBackground.setState(new int[] {});
+            }
+        }
+    }
 }