@Override
  public void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask) {
    // Remove the view associated with this task, we can't rely on updateTransforms
    // to work here because the task is no longer in the list
    TaskView tv = getChildViewForTask(removedTask);
    if (tv != null) {
      mViewPool.returnViewToPool(tv);
    }

    // Notify the callback that we've removed the task and it can clean up after it
    mCb.onTaskViewDismissed(removedTask);

    // Get the stack scroll of the task to anchor to (since we are removing something, the front
    // most task will be our anchor task)
    Task anchorTask = null;
    float prevAnchorTaskScroll = 0;
    boolean pullStackForward = stack.getTaskCount() > 0;
    if (pullStackForward) {
      anchorTask = mStack.getFrontMostTask();
      prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
    }

    // Update the min/max scroll and animate other task views into their new positions
    updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome);

    // Offset the stack by as much as the anchor task would otherwise move back
    if (pullStackForward) {
      float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
      mStackScroller.setStackScroll(
          mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll));
      mStackScroller.boundScroll();
    }

    // Animate all the tasks into place
    requestSynchronizeStackViewsWithModel(200);

    // Update the new front most task
    if (newFrontMostTask != null) {
      TaskView frontTv = getChildViewForTask(newFrontMostTask);
      if (frontTv != null) {
        frontTv.onTaskBound(newFrontMostTask);
      }
    }

    // If there are no remaining tasks, then either unfilter the current stack, or just close
    // the activity if there are no filtered stacks
    if (mStack.getTaskCount() == 0) {
      boolean shouldFinishActivity = true;
      if (mStack.hasFilteredTasks()) {
        mStack.unfilterTasks();
        shouldFinishActivity = (mStack.getTaskCount() == 0);
      }
      if (shouldFinishActivity) {
        mCb.onAllTaskViewsDismissed();
      }
    }
  }
  /** Prepares the header bar layout. */
  void reloadHeaderBarLayout() {
    Resources res = mContext.getResources();
    mWindowRect = mSystemServicesProxy.getWindowRect();
    mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
    mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height);
    mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width);
    mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy);
    mConfig.updateOnConfigurationChange();
    Rect searchBarBounds = new Rect();
    // Try and pre-emptively bind the search widget on startup to ensure that we
    // have the right thumbnail bounds to animate to.
    // Note: We have to reload the widget id before we get the task stack bounds below
    if (mSystemServicesProxy.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) {
      mConfig.getSearchBarBounds(
          mWindowRect.width(), mWindowRect.height(), mStatusBarHeight, searchBarBounds);
    }
    mConfig.getAvailableTaskStackBounds(
        mWindowRect.width(),
        mWindowRect.height(),
        mStatusBarHeight,
        (mConfig.hasTransposedNavBar ? mNavBarWidth : 0),
        searchBarBounds,
        mTaskStackBounds);
    if (mConfig.isLandscape && mConfig.hasTransposedNavBar) {
      mSystemInsets.set(0, mStatusBarHeight, mNavBarWidth, 0);
    } else {
      mSystemInsets.set(0, mStatusBarHeight, 0, mNavBarHeight);
    }

    // Inflate the header bar layout so that we can rebind and draw it for the transition
    TaskStack stack = new TaskStack();
    mDummyStackView = new TaskStackView(mContext, stack);
    TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm();
    Rect taskStackBounds = new Rect(mTaskStackBounds);
    taskStackBounds.bottom -= mSystemInsets.bottom;
    algo.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds);
    Rect taskViewSize = algo.getUntransformedTaskViewSize();
    int taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height);
    synchronized (mHeaderBarLock) {
      mHeaderBar =
          (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null, false);
      mHeaderBar.measure(
          View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY),
          View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY));
      mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight);
    }
  }
  /** Requests this task stacks to start it's enter-recents animation */
  public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
    // If we are still waiting to layout, then just defer until then
    if (mAwaitingFirstLayout) {
      mStartEnterAnimationRequestedAfterLayout = true;
      mStartEnterAnimationContext = ctx;
      return;
    }

    if (mStack.getTaskCount() > 0) {
      // Find the launch target task
      Task launchTargetTask = null;
      int childCount = getChildCount();
      for (int i = childCount - 1; i >= 0; i--) {
        TaskView tv = (TaskView) getChildAt(i);
        Task task = tv.getTask();
        if (task.isLaunchTarget) {
          launchTargetTask = task;
          break;
        }
      }

      // Animate all the task views into view
      for (int i = childCount - 1; i >= 0; i--) {
        TaskView tv = (TaskView) getChildAt(i);
        Task task = tv.getTask();
        ctx.currentTaskTransform = new TaskViewTransform();
        ctx.currentStackViewIndex = i;
        ctx.currentStackViewCount = childCount;
        ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect;
        ctx.currentTaskOccludesLaunchTarget =
            (launchTargetTask != null)
                && launchTargetTask.group.isTaskAboveTask(task, launchTargetTask);
        ctx.updateListener = mRequestUpdateClippingListener;
        mLayoutAlgorithm.getStackTransform(
            task, mStackScroller.getStackScroll(), ctx.currentTaskTransform, null);
        tv.startEnterRecentsAnimation(ctx);
      }

      // Add a runnable to the post animation ref counter to clear all the views
      ctx.postAnimationTrigger.addLastDecrementRunnable(
          new Runnable() {
            @Override
            public void run() {
              mStartEnterAnimationCompleted = true;
              // Start dozing
              mUIDozeTrigger.startDozing();
              // Focus the first view if accessibility is enabled
              RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
              SystemServicesProxy ssp = loader.getSystemServicesProxy();
              int childCount = getChildCount();
              if (childCount > 0 && ssp.isTouchExplorationEnabled()) {
                TaskView tv = ((TaskView) getChildAt(childCount - 1));
                tv.requestAccessibilityFocus();
                mPrevAccessibilityFocusedIndex = mStack.indexOfTask(tv.getTask());
              }
            }
          });
    }
  }
  /** Updates the min and max virtual scroll bounds */
  void updateMinMaxScroll(
      boolean boundScrollToNewMinMax, boolean launchedWithAltTab, boolean launchedFromHome) {
    // Compute the min and max scroll values
    mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab, launchedFromHome);

    // Debug logging
    if (boundScrollToNewMinMax) {
      mStackScroller.boundScroll();
    }
  }
  /** Computes the stack and task rects */
  public void computeRects(
      int windowWidth,
      int windowHeight,
      Rect taskStackBounds,
      boolean launchedWithAltTab,
      boolean launchedFromHome) {
    // Compute the rects in the stack algorithm
    mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds);

    // Update the scroll bounds
    updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome);
  }
  /** Initializes the Recents. */
  @ProxyFromPrimaryToCurrentUser
  @Override
  public void start() {
    if (sInstance == null) {
      sInstance = this;
    }
    RecentsTaskLoader.initialize(mContext);
    mInflater = LayoutInflater.from(mContext);
    mSystemServicesProxy = new SystemServicesProxy(mContext);
    mHandler = new Handler();
    mTaskStackBounds = new Rect();
    mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId);

    // Register the task stack listener
    mTaskStackListener = new TaskStackListenerImpl(mHandler);
    mSystemServicesProxy.registerTaskStackListener(mTaskStackListener);

    // Only the owner has the callback to update the SysUI visibility flags, so all non-owner
    // instances of AlternateRecentsComponent needs to notify the owner when the visibility
    // changes.
    if (mSystemServicesProxy.isForegroundUserOwner()) {
      mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver();
      IntentFilter filter = new IntentFilter();
      filter.addAction(Recents.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER);
      filter.addAction(Recents.ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER);
      mContext.registerReceiverAsUser(
          mProxyBroadcastReceiver, UserHandle.CURRENT, filter, null, mHandler);
    }

    // Initialize some static datastructures
    TaskStackViewLayoutAlgorithm.initializeCurve();
    // Load the header bar layout
    reloadHeaderBarLayout();

    // When we start, preload the data associated with the previous recent tasks.
    // We can use a new plan since the caches will be the same.
    RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
    RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
    loader.preloadTasks(plan, true /* isTopTaskHome */);
    RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
    launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize();
    launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
    launchOpts.onlyLoadForCache = true;
    loader.loadTasks(mContext, plan, launchOpts);
    putComponent(Recents.class, this);
  }
  /** 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;

      // Focus the view if possible, otherwise, focus the view after we scroll into position
      Task t = mStack.getTasks().get(taskIndex);
      TaskView tv = getChildViewForTask(t);
      Runnable postScrollRunnable = null;
      if (tv != null) {
        tv.setFocusedTask();
      } else {
        postScrollRunnable =
            new Runnable() {
              @Override
              public void run() {
                Task t = mStack.getTasks().get(mFocusedTaskIndex);
                TaskView tv = getChildViewForTask(t);
                if (tv != null) {
                  tv.setFocusedTask();
                }
              }
            };
      }

      // Scroll the view into position (just center it in the curve)
      if (scrollToNewPosition) {
        float newScroll = mLayoutAlgorithm.getStackScrollForTask(t) - 0.5f;
        newScroll = mStackScroller.getBoundedStackScroll(newScroll);
        mStackScroller.animateScroll(
            mStackScroller.getStackScroll(), newScroll, postScrollRunnable);
      } else {
        if (postScrollRunnable != null) {
          postScrollRunnable.run();
        }
      }
    }
  }
  /** Synchronizes the views with the model */
  boolean synchronizeStackViewsWithModel() {
    if (mStackViewsDirty) {
      RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
      SystemServicesProxy ssp = loader.getSystemServicesProxy();

      // Get all the task transforms
      ArrayList<Task> tasks = mStack.getTasks();
      float stackScroll = mStackScroller.getStackScroll();
      int[] visibleRange = mTmpVisibleRange;
      boolean isValidVisibleRange =
          updateStackTransforms(mCurrentTaskTransforms, tasks, stackScroll, visibleRange, false);
      if (mDebugOverlay != null) {
        mDebugOverlay.setText("vis[" + visibleRange[1] + "-" + visibleRange[0] + "]");
      }

      // Return all the invisible children to the pool
      mTmpTaskViewMap.clear();
      int childCount = getChildCount();
      for (int i = childCount - 1; i >= 0; i--) {
        TaskView tv = (TaskView) getChildAt(i);
        Task task = tv.getTask();
        int taskIndex = mStack.indexOfTask(task);
        if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) {
          mTmpTaskViewMap.put(task, tv);
        } else {
          mViewPool.returnViewToPool(tv);
        }
      }

      // Pick up all the newly visible children and update all the existing children
      for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) {
        Task task = tasks.get(i);
        TaskViewTransform transform = mCurrentTaskTransforms.get(i);
        TaskView tv = mTmpTaskViewMap.get(task);
        int taskIndex = mStack.indexOfTask(task);

        if (tv == null) {
          tv = mViewPool.pickUpViewFromPool(task, task);

          if (mStackViewsAnimationDuration > 0) {
            // For items in the list, put them in start animating them from the
            // approriate ends of the list where they are expected to appear
            if (Float.compare(transform.p, 0f) <= 0) {
              mLayoutAlgorithm.getStackTransform(0f, 0f, mTmpTransform, null);
            } else {
              mLayoutAlgorithm.getStackTransform(1f, 0f, mTmpTransform, null);
            }
            tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0);
          }
        }

        // Animate the task into place
        tv.updateViewPropertiesToTaskTransform(
            mCurrentTaskTransforms.get(taskIndex),
            mStackViewsAnimationDuration,
            mRequestUpdateClippingListener);

        // Request accessibility focus on the next view if we removed the task
        // that previously held accessibility focus
        childCount = getChildCount();
        if (childCount > 0 && ssp.isTouchExplorationEnabled()) {
          TaskView atv = (TaskView) getChildAt(childCount - 1);
          int indexOfTask = mStack.indexOfTask(atv.getTask());
          if (mPrevAccessibilityFocusedIndex != indexOfTask) {
            tv.requestAccessibilityFocus();
            mPrevAccessibilityFocusedIndex = indexOfTask;
          }
        }
      }

      // Reset the request-synchronize params
      mStackViewsAnimationDuration = 0;
      mStackViewsDirty = false;
      mStackViewsClipDirty = true;
      return true;
    }
    return false;
  }
  /** Gets the stack transforms of a list of tasks, and returns the visible range of tasks. */
  private boolean updateStackTransforms(
      ArrayList<TaskViewTransform> taskTransforms,
      ArrayList<Task> tasks,
      float stackScroll,
      int[] visibleRangeOut,
      boolean boundTranslationsToRect) {
    // XXX: We should be intelligent about where to look for the visible stack range using the
    //      current stack scroll.
    // XXX: We should log extra cases like the ones below where we don't expect to hit very often
    // XXX: Print out approximately how many indices we have to go through to find the first visible
    // transform

    int taskTransformCount = taskTransforms.size();
    int taskCount = tasks.size();
    int frontMostVisibleIndex = -1;
    int backMostVisibleIndex = -1;

    // We can reuse the task transforms where possible to reduce object allocation
    if (taskTransformCount < taskCount) {
      // If there are less transforms than tasks, then add as many transforms as necessary
      for (int i = taskTransformCount; i < taskCount; i++) {
        taskTransforms.add(new TaskViewTransform());
      }
    } else if (taskTransformCount > taskCount) {
      // If there are more transforms than tasks, then just subset the transform list
      taskTransforms.subList(0, taskCount);
    }

    // Update the stack transforms
    TaskViewTransform prevTransform = null;
    for (int i = taskCount - 1; i >= 0; i--) {
      TaskViewTransform transform =
          mLayoutAlgorithm.getStackTransform(
              tasks.get(i), stackScroll, taskTransforms.get(i), prevTransform);
      if (transform.visible) {
        if (frontMostVisibleIndex < 0) {
          frontMostVisibleIndex = i;
        }
        backMostVisibleIndex = i;
      } else {
        if (backMostVisibleIndex != -1) {
          // We've reached the end of the visible range, so going down the rest of the
          // stack, we can just reset the transforms accordingly
          while (i >= 0) {
            taskTransforms.get(i).reset();
            i--;
          }
          break;
        }
      }

      if (boundTranslationsToRect) {
        transform.translationY =
            Math.min(transform.translationY, mLayoutAlgorithm.mViewRect.bottom);
      }
      prevTransform = transform;
    }
    if (visibleRangeOut != null) {
      visibleRangeOut[0] = frontMostVisibleIndex;
      visibleRangeOut[1] = backMostVisibleIndex;
    }
    return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1;
  }