1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.recents.views;
18
19import android.annotation.IntDef;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.Path;
24import android.graphics.Rect;
25import android.util.ArraySet;
26import android.util.MutableFloat;
27import android.util.SparseArray;
28import android.util.SparseIntArray;
29import android.view.ViewDebug;
30
31import com.android.systemui.R;
32import com.android.systemui.recents.Recents;
33import com.android.systemui.recents.RecentsActivityLaunchState;
34import com.android.systemui.recents.RecentsConfiguration;
35import com.android.systemui.recents.RecentsDebugFlags;
36import com.android.systemui.recents.misc.FreePathInterpolator;
37import com.android.systemui.recents.misc.SystemServicesProxy;
38import com.android.systemui.recents.misc.Utilities;
39import com.android.systemui.recents.model.Task;
40import com.android.systemui.recents.model.TaskStack;
41import com.android.systemui.recents.views.grid.TaskGridLayoutAlgorithm;
42
43import java.io.PrintWriter;
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.util.ArrayList;
47import java.util.List;
48
49/**
50 * Used to describe a visible range that can be normalized to [0, 1].
51 */
52class Range {
53    final float relativeMin;
54    final float relativeMax;
55    float origin;
56    float min;
57    float max;
58
59    public Range(float relMin, float relMax) {
60        min = relativeMin = relMin;
61        max = relativeMax = relMax;
62    }
63
64    /**
65     * Offsets this range to a given absolute position.
66     */
67    public void offset(float x) {
68        this.origin = x;
69        min = x + relativeMin;
70        max = x + relativeMax;
71    }
72
73    /**
74     * Returns x normalized to the range 0 to 1 such that 0 = min, 0.5 = origin and 1 = max
75     *
76     * @param x is an absolute value in the same domain as origin
77     */
78    public float getNormalizedX(float x) {
79        if (x < origin) {
80            return 0.5f + 0.5f * (x - origin) / -relativeMin;
81        } else {
82            return 0.5f + 0.5f * (x - origin) / relativeMax;
83        }
84    }
85
86    /**
87     * Given a normalized {@param x} value in this range, projected onto the full range to get an
88     * absolute value about the given {@param origin}.
89     */
90    public float getAbsoluteX(float normX) {
91        if (normX < 0.5f) {
92            return (normX - 0.5f) / 0.5f * -relativeMin;
93        } else {
94            return (normX - 0.5f) / 0.5f * relativeMax;
95        }
96    }
97
98    /**
99     * Returns whether a value at an absolute x would be within range.
100     */
101    public boolean isInRange(float absX) {
102        return (absX >= Math.floor(min)) && (absX <= Math.ceil(max));
103    }
104}
105
106/**
107 * The layout logic for a TaskStackView.  This layout needs to be able to calculate the stack layout
108 * without an activity-specific context only with the information passed in.  This layout can have
109 * two states focused and unfocused, and in the focused state, there is a task that is displayed
110 * more prominently in the stack.
111 */
112public class TaskStackLayoutAlgorithm {
113
114    private static final String TAG = "TaskStackLayoutAlgorithm";
115
116    // The distribution of view bounds alpha
117    // XXX: This is a hack because you can currently set the max alpha to be > 1f
118    public static final float OUTLINE_ALPHA_MIN_VALUE = 0f;
119    public static final float OUTLINE_ALPHA_MAX_VALUE = 2f;
120
121    // The medium/maximum dim on the tasks
122    private static final float MED_DIM = 0.15f;
123    private static final float MAX_DIM = 0.25f;
124
125    // The various focus states
126    public static final int STATE_FOCUSED = 1;
127    public static final int STATE_UNFOCUSED = 0;
128
129    // The side that an offset is anchored
130    @Retention(RetentionPolicy.SOURCE)
131    @IntDef({FROM_TOP, FROM_BOTTOM})
132    public @interface AnchorSide {}
133    private static final int FROM_TOP = 0;
134    private static final int FROM_BOTTOM = 1;
135
136    // The extent that we care about when calculating fractions
137    @Retention(RetentionPolicy.SOURCE)
138    @IntDef({WIDTH, HEIGHT})
139    public @interface Extent {}
140    private static final int WIDTH = 0;
141    private static final int HEIGHT = 1;
142
143    public interface TaskStackLayoutAlgorithmCallbacks {
144        void onFocusStateChanged(int prevFocusState, int curFocusState);
145    }
146
147    /**
148     * The various stack/freeform states.
149     */
150    public static class StackState {
151
152        public static final StackState FREEFORM_ONLY = new StackState(1f, 255);
153        public static final StackState STACK_ONLY = new StackState(0f, 0);
154        public static final StackState SPLIT = new StackState(0.5f, 255);
155
156        public final float freeformHeightPct;
157        public final int freeformBackgroundAlpha;
158
159        /**
160         * @param freeformHeightPct the percentage of the stack height (not including paddings) to
161         *                          allocate to the freeform workspace
162         * @param freeformBackgroundAlpha the background alpha for the freeform workspace
163         */
164        private StackState(float freeformHeightPct, int freeformBackgroundAlpha) {
165            this.freeformHeightPct = freeformHeightPct;
166            this.freeformBackgroundAlpha = freeformBackgroundAlpha;
167        }
168
169        /**
170         * Resolves the stack state for the layout given a task stack.
171         */
172        public static StackState getStackStateForStack(TaskStack stack) {
173            SystemServicesProxy ssp = Recents.getSystemServices();
174            boolean hasFreeformWorkspaces = ssp.hasFreeformWorkspaceSupport();
175            int freeformCount = stack.getFreeformTaskCount();
176            int stackCount = stack.getStackTaskCount();
177            if (hasFreeformWorkspaces && stackCount > 0 && freeformCount > 0) {
178                return SPLIT;
179            } else if (hasFreeformWorkspaces && freeformCount > 0) {
180                return FREEFORM_ONLY;
181            } else {
182                return STACK_ONLY;
183            }
184        }
185
186        /**
187         * Computes the freeform and stack rect for this state.
188         *
189         * @param freeformRectOut the freeform rect to be written out
190         * @param stackRectOut the stack rect, we only write out the top of the stack
191         * @param taskStackBounds the full rect that the freeform rect can take up
192         */
193        public void computeRects(Rect freeformRectOut, Rect stackRectOut,
194                Rect taskStackBounds, int topMargin, int freeformGap, int stackBottomOffset) {
195            // The freeform height is the visible height (not including system insets) - padding
196            // above freeform and below stack - gap between the freeform and stack
197            int availableHeight = taskStackBounds.height() - topMargin - stackBottomOffset;
198            int ffPaddedHeight = (int) (availableHeight * freeformHeightPct);
199            int ffHeight = Math.max(0, ffPaddedHeight - freeformGap);
200            freeformRectOut.set(taskStackBounds.left,
201                    taskStackBounds.top + topMargin,
202                    taskStackBounds.right,
203                    taskStackBounds.top + topMargin + ffHeight);
204            stackRectOut.set(taskStackBounds.left,
205                    taskStackBounds.top,
206                    taskStackBounds.right,
207                    taskStackBounds.bottom);
208            if (ffPaddedHeight > 0) {
209                stackRectOut.top += ffPaddedHeight;
210            } else {
211                stackRectOut.top += topMargin;
212            }
213        }
214    }
215
216    /**
217     * @return True if we should use the grid layout.
218     */
219    boolean useGridLayout() {
220        return Recents.getConfiguration().isGridEnabled;
221    }
222
223    // A report of the visibility state of the stack
224    public class VisibilityReport {
225        public int numVisibleTasks;
226        public int numVisibleThumbnails;
227
228        /** Package level ctor */
229        VisibilityReport(int tasks, int thumbnails) {
230            numVisibleTasks = tasks;
231            numVisibleThumbnails = thumbnails;
232        }
233    }
234
235    Context mContext;
236    private StackState mState = StackState.SPLIT;
237    private TaskStackLayoutAlgorithmCallbacks mCb;
238
239    // The task bounds (untransformed) for layout.  This rect is anchored at mTaskRoot.
240    @ViewDebug.ExportedProperty(category="recents")
241    public Rect mTaskRect = new Rect();
242    // The freeform workspace bounds, inset by the top system insets and is a fixed height
243    @ViewDebug.ExportedProperty(category="recents")
244    public Rect mFreeformRect = new Rect();
245    // The stack bounds, inset from the top system insets, and runs to the bottom of the screen
246    @ViewDebug.ExportedProperty(category="recents")
247    public Rect mStackRect = new Rect();
248    // This is the current system insets
249    @ViewDebug.ExportedProperty(category="recents")
250    public Rect mSystemInsets = new Rect();
251
252    // The visible ranges when the stack is focused and unfocused
253    private Range mUnfocusedRange;
254    private Range mFocusedRange;
255
256    // This is the bounds of the stack action above the stack rect
257    @ViewDebug.ExportedProperty(category="recents")
258    private Rect mStackActionButtonRect = new Rect();
259    // The base top margin for the stack from the system insets
260    @ViewDebug.ExportedProperty(category="recents")
261    private int mBaseTopMargin;
262    // The base side margin for the stack from the system insets
263    @ViewDebug.ExportedProperty(category="recents")
264    private int mBaseSideMargin;
265    // The base bottom margin for the stack from the system insets
266    @ViewDebug.ExportedProperty(category="recents")
267    private int mBaseBottomMargin;
268    private int mMinMargin;
269
270    // The gap between the freeform and stack layouts
271    @ViewDebug.ExportedProperty(category="recents")
272    private int mFreeformStackGap;
273
274    // The initial offset that the focused task is from the top
275    @ViewDebug.ExportedProperty(category="recents")
276    private int mInitialTopOffset;
277    private int mBaseInitialTopOffset;
278    // The initial offset that the launch-from task is from the bottom
279    @ViewDebug.ExportedProperty(category="recents")
280    private int mInitialBottomOffset;
281    private int mBaseInitialBottomOffset;
282
283    // The height between the top margin and the top of the focused task
284    @ViewDebug.ExportedProperty(category="recents")
285    private int mFocusedTopPeekHeight;
286    // The height between the bottom margin and the top of task in front of the focused task
287    @ViewDebug.ExportedProperty(category="recents")
288    private int mFocusedBottomPeekHeight;
289
290    // The offset from the bottom of the stack to the bottom of the bounds when the stack is
291    // scrolled to the front
292    @ViewDebug.ExportedProperty(category="recents")
293    private int mStackBottomOffset;
294
295    /** The height, in pixels, of each task view's title bar. */
296    private int mTitleBarHeight;
297
298    // The paths defining the motion of the tasks when the stack is focused and unfocused
299    private Path mUnfocusedCurve;
300    private Path mFocusedCurve;
301    private FreePathInterpolator mUnfocusedCurveInterpolator;
302    private FreePathInterpolator mFocusedCurveInterpolator;
303
304    // The paths defining the distribution of the dim to apply to tasks in the stack when focused
305    // and unfocused
306    private Path mUnfocusedDimCurve;
307    private Path mFocusedDimCurve;
308    private FreePathInterpolator mUnfocusedDimCurveInterpolator;
309    private FreePathInterpolator mFocusedDimCurveInterpolator;
310
311    // The state of the stack focus (0..1), which controls the transition of the stack from the
312    // focused to non-focused state
313    @ViewDebug.ExportedProperty(category="recents")
314    private int mFocusState;
315
316    // The smallest scroll progress, at this value, the back most task will be visible
317    @ViewDebug.ExportedProperty(category="recents")
318    float mMinScrollP;
319    // The largest scroll progress, at this value, the front most task will be visible above the
320    // navigation bar
321    @ViewDebug.ExportedProperty(category="recents")
322    float mMaxScrollP;
323    // The initial progress that the scroller is set when you first enter recents
324    @ViewDebug.ExportedProperty(category="recents")
325    float mInitialScrollP;
326    // The task progress for the front-most task in the stack
327    @ViewDebug.ExportedProperty(category="recents")
328    float mFrontMostTaskP;
329
330    // The last computed task counts
331    @ViewDebug.ExportedProperty(category="recents")
332    int mNumStackTasks;
333    @ViewDebug.ExportedProperty(category="recents")
334    int mNumFreeformTasks;
335
336    // The min/max z translations
337    @ViewDebug.ExportedProperty(category="recents")
338    int mMinTranslationZ;
339    @ViewDebug.ExportedProperty(category="recents")
340    public int mMaxTranslationZ;
341
342    // Optimization, allows for quick lookup of task -> index
343    private SparseIntArray mTaskIndexMap = new SparseIntArray();
344    private SparseArray<Float> mTaskIndexOverrideMap = new SparseArray<>();
345
346    // The freeform workspace layout
347    FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
348    TaskGridLayoutAlgorithm mTaskGridLayoutAlgorithm;
349
350    // The transform to place TaskViews at the front and back of the stack respectively
351    TaskViewTransform mBackOfStackTransform = new TaskViewTransform();
352    TaskViewTransform mFrontOfStackTransform = new TaskViewTransform();
353
354    public TaskStackLayoutAlgorithm(Context context, TaskStackLayoutAlgorithmCallbacks cb) {
355        Resources res = context.getResources();
356        mContext = context;
357        mCb = cb;
358        mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm(context);
359        mTaskGridLayoutAlgorithm = new TaskGridLayoutAlgorithm(context);
360        reloadOnConfigurationChange(context);
361    }
362
363    /**
364     * Reloads the layout for the current configuration.
365     */
366    public void reloadOnConfigurationChange(Context context) {
367        Resources res = context.getResources();
368        mFocusedRange = new Range(res.getFloat(R.integer.recents_layout_focused_range_min),
369                res.getFloat(R.integer.recents_layout_focused_range_max));
370        mUnfocusedRange = new Range(res.getFloat(R.integer.recents_layout_unfocused_range_min),
371                res.getFloat(R.integer.recents_layout_unfocused_range_max));
372        mFocusState = getInitialFocusState();
373        mFocusedTopPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_top_peek_size);
374        mFocusedBottomPeekHeight =
375                res.getDimensionPixelSize(R.dimen.recents_layout_bottom_peek_size);
376        mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_min);
377        mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_max);
378        mBaseInitialTopOffset = getDimensionForDevice(context,
379                R.dimen.recents_layout_initial_top_offset_phone_port,
380                R.dimen.recents_layout_initial_top_offset_phone_land,
381                R.dimen.recents_layout_initial_top_offset_tablet,
382                R.dimen.recents_layout_initial_top_offset_tablet,
383                R.dimen.recents_layout_initial_top_offset_tablet,
384                R.dimen.recents_layout_initial_top_offset_tablet,
385                R.dimen.recents_layout_initial_top_offset_tablet);
386        mBaseInitialBottomOffset = getDimensionForDevice(context,
387                R.dimen.recents_layout_initial_bottom_offset_phone_port,
388                R.dimen.recents_layout_initial_bottom_offset_phone_land,
389                R.dimen.recents_layout_initial_bottom_offset_tablet,
390                R.dimen.recents_layout_initial_bottom_offset_tablet,
391                R.dimen.recents_layout_initial_bottom_offset_tablet,
392                R.dimen.recents_layout_initial_bottom_offset_tablet,
393                R.dimen.recents_layout_initial_bottom_offset_tablet);
394        mFreeformLayoutAlgorithm.reloadOnConfigurationChange(context);
395        mTaskGridLayoutAlgorithm.reloadOnConfigurationChange(context);
396        mMinMargin = res.getDimensionPixelSize(R.dimen.recents_layout_min_margin);
397        mBaseTopMargin = getDimensionForDevice(context,
398                R.dimen.recents_layout_top_margin_phone,
399                R.dimen.recents_layout_top_margin_tablet,
400                R.dimen.recents_layout_top_margin_tablet_xlarge,
401                R.dimen.recents_layout_top_margin_tablet);
402        mBaseSideMargin = getDimensionForDevice(context,
403                R.dimen.recents_layout_side_margin_phone,
404                R.dimen.recents_layout_side_margin_tablet,
405                R.dimen.recents_layout_side_margin_tablet_xlarge,
406                R.dimen.recents_layout_side_margin_tablet);
407        mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin);
408        mFreeformStackGap =
409                res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin);
410        mTitleBarHeight = getDimensionForDevice(mContext,
411                R.dimen.recents_task_view_header_height,
412                R.dimen.recents_task_view_header_height,
413                R.dimen.recents_task_view_header_height,
414                R.dimen.recents_task_view_header_height_tablet_land,
415                R.dimen.recents_task_view_header_height,
416                R.dimen.recents_task_view_header_height_tablet_land,
417                R.dimen.recents_grid_task_view_header_height);
418    }
419
420    /**
421     * Resets this layout when the stack view is reset.
422     */
423    public void reset() {
424        mTaskIndexOverrideMap.clear();
425        setFocusState(getInitialFocusState());
426    }
427
428    /**
429     * Sets the system insets.
430     */
431    public boolean setSystemInsets(Rect systemInsets) {
432        boolean changed = !mSystemInsets.equals(systemInsets);
433        mSystemInsets.set(systemInsets);
434        mTaskGridLayoutAlgorithm.setSystemInsets(systemInsets);
435        return changed;
436    }
437
438    /**
439     * Sets the focused state.
440     */
441    public void setFocusState(int focusState) {
442        int prevFocusState = mFocusState;
443        mFocusState = focusState;
444        updateFrontBackTransforms();
445        if (mCb != null) {
446            mCb.onFocusStateChanged(prevFocusState, focusState);
447        }
448    }
449
450    /**
451     * Gets the focused state.
452     */
453    public int getFocusState() {
454        return mFocusState;
455    }
456
457    /**
458     * Computes the stack and task rects.  The given task stack bounds already has the top/right
459     * insets and left/right padding already applied.
460     */
461    public void initialize(Rect displayRect, Rect windowRect, Rect taskStackBounds,
462            StackState state) {
463        Rect lastStackRect = new Rect(mStackRect);
464
465        int topMargin = getScaleForExtent(windowRect, displayRect, mBaseTopMargin, mMinMargin, HEIGHT);
466        int bottomMargin = getScaleForExtent(windowRect, displayRect, mBaseBottomMargin, mMinMargin,
467                HEIGHT);
468        mInitialTopOffset = getScaleForExtent(windowRect, displayRect, mBaseInitialTopOffset,
469                mMinMargin, HEIGHT);
470        mInitialBottomOffset = mBaseInitialBottomOffset;
471
472        // Compute the stack bounds
473        mState = state;
474        mStackBottomOffset = mSystemInsets.bottom + bottomMargin;
475        state.computeRects(mFreeformRect, mStackRect, taskStackBounds, topMargin,
476                mFreeformStackGap, mStackBottomOffset);
477
478        // The stack action button will take the full un-padded header space above the stack
479        mStackActionButtonRect.set(mStackRect.left, mStackRect.top - topMargin,
480                mStackRect.right, mStackRect.top + mFocusedTopPeekHeight);
481
482        // Anchor the task rect top aligned to the stack rect
483        int height = mStackRect.height() - mInitialTopOffset - mStackBottomOffset;
484        mTaskRect.set(mStackRect.left, mStackRect.top, mStackRect.right, mStackRect.top + height);
485
486        // Short circuit here if the stack rects haven't changed so we don't do all the work below
487        if (!lastStackRect.equals(mStackRect)) {
488            // Reinitialize the focused and unfocused curves
489            mUnfocusedCurve = constructUnfocusedCurve();
490            mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve);
491            mFocusedCurve = constructFocusedCurve();
492            mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve);
493            mUnfocusedDimCurve = constructUnfocusedDimCurve();
494            mUnfocusedDimCurveInterpolator = new FreePathInterpolator(mUnfocusedDimCurve);
495            mFocusedDimCurve = constructFocusedDimCurve();
496            mFocusedDimCurveInterpolator = new FreePathInterpolator(mFocusedDimCurve);
497
498            updateFrontBackTransforms();
499        }
500
501        // Initialize the grid layout
502        mTaskGridLayoutAlgorithm.initialize(windowRect);
503    }
504
505    /**
506     * Computes the minimum and maximum scroll progress values and the progress values for each task
507     * in the stack.
508     */
509    void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) {
510        SystemServicesProxy ssp = Recents.getSystemServices();
511        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
512
513        // Clear the progress map
514        mTaskIndexMap.clear();
515
516        // Return early if we have no tasks
517        ArrayList<Task> tasks = stack.getStackTasks();
518        if (tasks.isEmpty()) {
519            mFrontMostTaskP = 0;
520            mMinScrollP = mMaxScrollP = mInitialScrollP = 0;
521            mNumStackTasks = mNumFreeformTasks = 0;
522            return;
523        }
524
525        // Filter the set of freeform and stack tasks
526        ArrayList<Task> freeformTasks = new ArrayList<>();
527        ArrayList<Task> stackTasks = new ArrayList<>();
528        for (int i = 0; i < tasks.size(); i++) {
529            Task task = tasks.get(i);
530            if (ignoreTasksSet.contains(task.key)) {
531                continue;
532            }
533            if (task.isFreeformTask()) {
534                freeformTasks.add(task);
535            } else {
536                stackTasks.add(task);
537            }
538        }
539        mNumStackTasks = stackTasks.size();
540        mNumFreeformTasks = freeformTasks.size();
541
542        // Put each of the tasks in the progress map at a fixed index (does not need to actually
543        // map to a scroll position, just by index)
544        int taskCount = stackTasks.size();
545        for (int i = 0; i < taskCount; i++) {
546            Task task = stackTasks.get(i);
547            mTaskIndexMap.put(task.key.id, i);
548        }
549
550        // Update the freeform tasks
551        if (!freeformTasks.isEmpty()) {
552            mFreeformLayoutAlgorithm.update(freeformTasks, this);
553        }
554
555        // Calculate the min/max/initial scroll
556        Task launchTask = stack.getLaunchTarget();
557        int launchTaskIndex = launchTask != null
558                ? stack.indexOfStackTask(launchTask)
559                : mNumStackTasks - 1;
560        if (getInitialFocusState() == STATE_FOCUSED) {
561            int maxBottomOffset = mStackBottomOffset + mTaskRect.height();
562            float maxBottomNormX = getNormalizedXFromFocusedY(maxBottomOffset, FROM_BOTTOM);
563            mFocusedRange.offset(0f);
564            mMinScrollP = 0;
565            mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) -
566                    Math.max(0, mFocusedRange.getAbsoluteX(maxBottomNormX)));
567            if (launchState.launchedFromHome) {
568                mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP);
569            } else {
570                mInitialScrollP = Utilities.clamp(launchTaskIndex - 1, mMinScrollP, mMaxScrollP);
571            }
572        } else if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
573            // If there is one stack task, ignore the min/max/initial scroll positions
574            mMinScrollP = 0;
575            mMaxScrollP = 0;
576            mInitialScrollP = 0;
577        } else {
578            // Set the max scroll to be the point where the front most task is visible with the
579            // stack bottom offset
580            int maxBottomOffset = mStackBottomOffset + mTaskRect.height();
581            float maxBottomNormX = getNormalizedXFromUnfocusedY(maxBottomOffset, FROM_BOTTOM);
582            mUnfocusedRange.offset(0f);
583            mMinScrollP = 0;
584            mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) -
585                    Math.max(0, mUnfocusedRange.getAbsoluteX(maxBottomNormX)));
586            boolean scrollToFront = launchState.launchedFromHome ||
587                    launchState.launchedViaDockGesture;
588            if (launchState.launchedFromBlacklistedApp) {
589                mInitialScrollP = mMaxScrollP;
590            } else if (launchState.launchedWithAltTab) {
591                mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP);
592            } else if (scrollToFront) {
593                mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP);
594            } else {
595                // We are overriding the initial two task positions, so set the initial scroll
596                // position to match the second task (aka focused task) position
597                float initialTopNormX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP);
598                mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, (mNumStackTasks - 2))
599                        - Math.max(0, mUnfocusedRange.getAbsoluteX(initialTopNormX)));
600            }
601        }
602    }
603
604    /**
605     * Creates task overrides to ensure the initial stack layout if necessary.
606     */
607    public void setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront) {
608        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
609
610        mTaskIndexOverrideMap.clear();
611
612        boolean scrollToFront = launchState.launchedFromHome ||
613                launchState.launchedFromBlacklistedApp ||
614                launchState.launchedViaDockGesture;
615        if (getInitialFocusState() == STATE_UNFOCUSED && mNumStackTasks > 1) {
616            if (ignoreScrollToFront || (!launchState.launchedWithAltTab && !scrollToFront)) {
617                // Set the initial scroll to the predefined state (which differs from the stack)
618                float [] initialNormX = null;
619                float minBottomTaskNormX = getNormalizedXFromUnfocusedY(mSystemInsets.bottom +
620                        mInitialBottomOffset, FROM_BOTTOM);
621                float maxBottomTaskNormX = getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight +
622                        mTaskRect.height() - mMinMargin, FROM_TOP);
623                if (mNumStackTasks <= 2) {
624                    // For small stacks, position the tasks so that they are top aligned to under
625                    // the action button, but ensure that it is at least a certain offset from the
626                    // bottom of the stack
627                    initialNormX = new float[] {
628                            Math.min(maxBottomTaskNormX, minBottomTaskNormX),
629                            getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight, FROM_TOP)
630                    };
631                } else {
632                    initialNormX = new float[] {
633                            minBottomTaskNormX,
634                            getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP)
635                    };
636                }
637
638                mUnfocusedRange.offset(0f);
639                List<Task> tasks = stack.getStackTasks();
640                int taskCount = tasks.size();
641                for (int i = taskCount - 1; i >= 0; i--) {
642                    int indexFromFront = taskCount - i - 1;
643                    if (indexFromFront >= initialNormX.length) {
644                        break;
645                    }
646                    float newTaskProgress = mInitialScrollP +
647                            mUnfocusedRange.getAbsoluteX(initialNormX[indexFromFront]);
648                    mTaskIndexOverrideMap.put(tasks.get(i).key.id, newTaskProgress);
649                }
650            }
651        }
652    }
653
654    /**
655     * Adds and override task progress for the given task when transitioning from focused to
656     * unfocused state.
657     */
658    public void addUnfocusedTaskOverride(Task task, float stackScroll) {
659        if (mFocusState != STATE_UNFOCUSED) {
660            mFocusedRange.offset(stackScroll);
661            mUnfocusedRange.offset(stackScroll);
662            float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id));
663            float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX);
664            float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY);
665            float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX);
666            if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) {
667                mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress);
668            }
669        }
670    }
671
672    /**
673     * Adds and override task progress for the given task when transitioning from focused to
674     * unfocused state.
675     */
676    public void addUnfocusedTaskOverride(TaskView taskView, float stackScroll) {
677        mFocusedRange.offset(stackScroll);
678        mUnfocusedRange.offset(stackScroll);
679
680        Task task = taskView.getTask();
681        int top = taskView.getTop() - mTaskRect.top;
682        float focusedRangeX = getNormalizedXFromFocusedY(top, FROM_TOP);
683        float unfocusedRangeX = getNormalizedXFromUnfocusedY(top, FROM_TOP);
684        float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX);
685        if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) {
686            mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress);
687        }
688    }
689
690    public void clearUnfocusedTaskOverrides() {
691        mTaskIndexOverrideMap.clear();
692    }
693
694    /**
695     * Updates this stack when a scroll happens.
696     *
697     */
698    public float updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll,
699            float lastStackScroll) {
700        if (targetStackScroll == lastStackScroll) {
701            return targetStackScroll;
702        }
703
704        float deltaScroll = targetStackScroll - lastStackScroll;
705        float deltaTargetScroll = targetStackScroll - lastTargetStackScroll;
706        float newScroll = targetStackScroll;
707        mUnfocusedRange.offset(targetStackScroll);
708        for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) {
709            int taskId = mTaskIndexOverrideMap.keyAt(i);
710            float x = mTaskIndexMap.get(taskId);
711            float overrideX = mTaskIndexOverrideMap.get(taskId, 0f);
712            float newOverrideX = overrideX + deltaScroll;
713            if (isInvalidOverrideX(x, overrideX, newOverrideX)) {
714                // Remove the override once we reach the original task index
715                mTaskIndexOverrideMap.removeAt(i);
716            } else if ((overrideX >= x && deltaScroll <= 0f) ||
717                    (overrideX <= x && deltaScroll >= 0f)) {
718                // Scrolling from override x towards x, then lock the task in place
719                mTaskIndexOverrideMap.put(taskId, newOverrideX);
720            } else {
721                // Scrolling override x away from x, we should still move the scroll towards x
722                newScroll = lastStackScroll;
723                newOverrideX = overrideX - deltaTargetScroll;
724                if (isInvalidOverrideX(x, overrideX, newOverrideX)) {
725                    mTaskIndexOverrideMap.removeAt(i);
726                } else{
727                    mTaskIndexOverrideMap.put(taskId, newOverrideX);
728                }
729            }
730        }
731        return newScroll;
732    }
733
734    private boolean isInvalidOverrideX(float x, float overrideX, float newOverrideX) {
735        boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f ||
736                mUnfocusedRange.getNormalizedX(newOverrideX) > 1f;
737        return outOfBounds || (overrideX >= x && x >= newOverrideX) ||
738                (overrideX <= x && x <= newOverrideX);
739    }
740
741    /**
742     * Returns the default focus state.
743     */
744    public int getInitialFocusState() {
745        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
746        RecentsDebugFlags debugFlags = Recents.getDebugFlags();
747        if (debugFlags.isPagingEnabled() || launchState.launchedWithAltTab) {
748            return STATE_FOCUSED;
749        } else {
750            return STATE_UNFOCUSED;
751        }
752    }
753
754    public Rect getStackActionButtonRect() {
755        return useGridLayout()
756                ? mTaskGridLayoutAlgorithm.getStackActionButtonRect() : mStackActionButtonRect;
757    }
758
759    /**
760     * Returns the TaskViewTransform that would put the task just off the back of the stack.
761     */
762    public TaskViewTransform getBackOfStackTransform() {
763        return mBackOfStackTransform;
764    }
765
766    /**
767     * Returns the TaskViewTransform that would put the task just off the front of the stack.
768     */
769    public TaskViewTransform getFrontOfStackTransform() {
770        return mFrontOfStackTransform;
771    }
772
773    /**
774     * Returns the current stack state.
775     */
776    public StackState getStackState() {
777        return mState;
778    }
779
780    /**
781     * Returns whether this stack layout has been initialized.
782     */
783    public boolean isInitialized() {
784        return !mStackRect.isEmpty();
785    }
786
787    /**
788     * Computes the maximum number of visible tasks and thumbnails when the scroll is at the initial
789     * stack scroll.  Requires that update() is called first.
790     */
791    public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
792        // Ensure minimum visibility count
793        if (tasks.size() <= 1) {
794            return new VisibilityReport(1, 1);
795        }
796
797        // Quick return when there are no stack tasks
798        if (mNumStackTasks == 0) {
799            return new VisibilityReport(Math.max(mNumFreeformTasks, 1),
800                    Math.max(mNumFreeformTasks, 1));
801        }
802
803        // Otherwise, walk backwards in the stack and count the number of tasks and visible
804        // thumbnails and add that to the total freeform task count
805        TaskViewTransform tmpTransform = new TaskViewTransform();
806        Range currentRange = getInitialFocusState() > 0f ? mFocusedRange : mUnfocusedRange;
807        currentRange.offset(mInitialScrollP);
808        int taskBarHeight = mContext.getResources().getDimensionPixelSize(
809                R.dimen.recents_task_view_header_height);
810        int numVisibleTasks = Math.max(mNumFreeformTasks, 1);
811        int numVisibleThumbnails = Math.max(mNumFreeformTasks, 1);
812        float prevScreenY = Integer.MAX_VALUE;
813        for (int i = tasks.size() - 1; i >= 0; i--) {
814            Task task = tasks.get(i);
815
816            // Skip freeform
817            if (task.isFreeformTask()) {
818                continue;
819            }
820
821            // Skip invisible
822            float taskProgress = getStackScrollForTask(task);
823            if (!currentRange.isInRange(taskProgress)) {
824                continue;
825            }
826
827            boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
828            if (isFrontMostTaskInGroup) {
829                getStackTransform(taskProgress, taskProgress, mInitialScrollP, mFocusState,
830                        tmpTransform, null, false /* ignoreSingleTaskCase */,
831                        false /* forceUpdate */);
832                float screenY = tmpTransform.rect.top;
833                boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
834                if (hasVisibleThumbnail) {
835                    numVisibleThumbnails++;
836                    numVisibleTasks++;
837                    prevScreenY = screenY;
838                } else {
839                    // Once we hit the next front most task that does not have a visible thumbnail,
840                    // walk through remaining visible set
841                    for (int j = i; j >= 0; j--) {
842                        numVisibleTasks++;
843                        taskProgress = getStackScrollForTask(tasks.get(j));
844                        if (!currentRange.isInRange(taskProgress)) {
845                            continue;
846                        }
847                    }
848                    break;
849                }
850            } else if (!isFrontMostTaskInGroup) {
851                // Affiliated task, no thumbnail
852                numVisibleTasks++;
853            }
854        }
855        return new VisibilityReport(numVisibleTasks, numVisibleThumbnails);
856    }
857
858    /**
859     * Returns the transform for the given task.  This transform is relative to the mTaskRect, which
860     * is what the view is measured and laid out with.
861     */
862    public TaskViewTransform getStackTransform(Task task, float stackScroll,
863            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
864        return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform,
865                false /* forceUpdate */, false /* ignoreTaskOverrides */);
866    }
867
868    public TaskViewTransform getStackTransform(Task task, float stackScroll,
869            TaskViewTransform transformOut, TaskViewTransform frontTransform,
870            boolean ignoreTaskOverrides) {
871        return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform,
872                false /* forceUpdate */, ignoreTaskOverrides);
873    }
874
875    public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState,
876            TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate,
877            boolean ignoreTaskOverrides) {
878        if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
879            mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
880            return transformOut;
881        } else if (useGridLayout()) {
882            int taskIndex = mTaskIndexMap.get(task.key.id);
883            int taskCount = mTaskIndexMap.size();
884            mTaskGridLayoutAlgorithm.getTransform(taskIndex, taskCount, transformOut, this);
885            return transformOut;
886        } else {
887            // Return early if we have an invalid index
888            int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1);
889            if (task == null || nonOverrideTaskProgress == -1) {
890                transformOut.reset();
891                return transformOut;
892            }
893            float taskProgress = ignoreTaskOverrides
894                    ? nonOverrideTaskProgress
895                    : getStackScrollForTask(task);
896            getStackTransform(taskProgress, nonOverrideTaskProgress, stackScroll, focusState,
897                    transformOut, frontTransform, false /* ignoreSingleTaskCase */, forceUpdate);
898            return transformOut;
899        }
900    }
901
902    /**
903     * Like {@link #getStackTransform}, but in screen coordinates
904     */
905    public TaskViewTransform getStackTransformScreenCoordinates(Task task, float stackScroll,
906            TaskViewTransform transformOut, TaskViewTransform frontTransform,
907            Rect windowOverrideRect) {
908        TaskViewTransform transform = getStackTransform(task, stackScroll, mFocusState,
909                transformOut, frontTransform, true /* forceUpdate */,
910                false /* ignoreTaskOverrides */);
911        return transformToScreenCoordinates(transform, windowOverrideRect);
912    }
913
914    /**
915     * Transforms the given {@param transformOut} to the screen coordinates, overriding the current
916     * window rectangle with {@param windowOverrideRect} if non-null.
917     */
918    TaskViewTransform transformToScreenCoordinates(TaskViewTransform transformOut,
919            Rect windowOverrideRect) {
920        Rect windowRect = windowOverrideRect != null
921                ? windowOverrideRect
922                : Recents.getSystemServices().getWindowRect();
923        transformOut.rect.offset(windowRect.left, windowRect.top);
924        if (useGridLayout()) {
925            // Draw the thumbnail a little lower to perfectly coincide with the view we are
926            // transitioning to, where the header bar has already been drawn.
927            transformOut.rect.offset(0, mTitleBarHeight);
928        }
929        return transformOut;
930    }
931
932    /**
933     * Update/get the transform.
934     *
935     * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take
936     *                             into account the special single-task case.  This is only used
937     *                             internally to ensure that we can calculate the transform for any
938     *                             position in the stack.
939     */
940    public void getStackTransform(float taskProgress, float nonOverrideTaskProgress,
941            float stackScroll, int focusState, TaskViewTransform transformOut,
942            TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate) {
943        SystemServicesProxy ssp = Recents.getSystemServices();
944
945        // Ensure that the task is in range
946        mUnfocusedRange.offset(stackScroll);
947        mFocusedRange.offset(stackScroll);
948        boolean unfocusedVisible = mUnfocusedRange.isInRange(taskProgress);
949        boolean focusedVisible = mFocusedRange.isInRange(taskProgress);
950
951        // Skip if the task is not visible
952        if (!forceUpdate && !unfocusedVisible && !focusedVisible) {
953            transformOut.reset();
954            return;
955        }
956
957        // Map the absolute task progress to the normalized x at the stack scroll.  We use this to
958        // calculate positions along the curve.
959        mUnfocusedRange.offset(stackScroll);
960        mFocusedRange.offset(stackScroll);
961        float unfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress);
962        float focusedRangeX = mFocusedRange.getNormalizedX(taskProgress);
963
964        // Map the absolute task progress to the normalized x at the bounded stack scroll.  We use
965        // this to calculate bounded properties, like translationZ and outline alpha.
966        float boundedStackScroll = Utilities.clamp(stackScroll, mMinScrollP, mMaxScrollP);
967        mUnfocusedRange.offset(boundedStackScroll);
968        mFocusedRange.offset(boundedStackScroll);
969        float boundedScrollUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress);
970        float boundedScrollUnfocusedNonOverrideRangeX =
971                mUnfocusedRange.getNormalizedX(nonOverrideTaskProgress);
972
973        // Map the absolute task progress to the normalized x at the upper bounded stack scroll.
974        // We use this to calculate the dim, which is bounded only on one end.
975        float lowerBoundedStackScroll = Utilities.clamp(stackScroll, -Float.MAX_VALUE, mMaxScrollP);
976        mUnfocusedRange.offset(lowerBoundedStackScroll);
977        mFocusedRange.offset(lowerBoundedStackScroll);
978        float lowerBoundedUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress);
979        float lowerBoundedFocusedRangeX = mFocusedRange.getNormalizedX(taskProgress);
980
981        int x = (mStackRect.width() - mTaskRect.width()) / 2;
982        int y;
983        float z;
984        float dimAlpha;
985        float viewOutlineAlpha;
986        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) {
987            // When there is exactly one task, then decouple the task from the stack and just move
988            // in screen space
989            float tmpP = (mMinScrollP - stackScroll) / mNumStackTasks;
990            int centerYOffset = (mStackRect.top - mTaskRect.top) +
991                    (mStackRect.height() - mSystemInsets.bottom - mTaskRect.height()) / 2;
992            y = centerYOffset + getYForDeltaP(tmpP, 0);
993            z = mMaxTranslationZ;
994            dimAlpha = 0f;
995            viewOutlineAlpha = OUTLINE_ALPHA_MIN_VALUE +
996                    (OUTLINE_ALPHA_MAX_VALUE - OUTLINE_ALPHA_MIN_VALUE) / 2f;
997
998        } else {
999            // Otherwise, update the task to the stack layout
1000            int unfocusedY = (int) ((1f - mUnfocusedCurveInterpolator.getInterpolation(
1001                    unfocusedRangeX)) * mStackRect.height());
1002            int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation(
1003                    focusedRangeX)) * mStackRect.height());
1004            float unfocusedDim = mUnfocusedDimCurveInterpolator.getInterpolation(
1005                    lowerBoundedUnfocusedRangeX);
1006            float focusedDim = mFocusedDimCurveInterpolator.getInterpolation(
1007                    lowerBoundedFocusedRangeX);
1008
1009            // Special case, because we override the initial task positions differently for small
1010            // stacks, we clamp the dim to 0 in the initial position, and then only modulate the
1011            // dim when the task is scrolled back towards the top of the screen
1012            if (mNumStackTasks <= 2 && nonOverrideTaskProgress == 0f) {
1013                if (boundedScrollUnfocusedRangeX >= 0.5f) {
1014                    unfocusedDim = 0f;
1015                } else {
1016                    float offset = mUnfocusedDimCurveInterpolator.getInterpolation(0.5f);
1017                    unfocusedDim -= offset;
1018                    unfocusedDim *= MAX_DIM / (MAX_DIM - offset);
1019                }
1020            }
1021
1022            y = (mStackRect.top - mTaskRect.top) +
1023                    (int) Utilities.mapRange(focusState, unfocusedY, focusedY);
1024            z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedNonOverrideRangeX),
1025                    mMinTranslationZ, mMaxTranslationZ);
1026            dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim);
1027            viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX),
1028                    OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE);
1029        }
1030
1031        // Fill out the transform
1032        transformOut.scale = 1f;
1033        transformOut.alpha = 1f;
1034        transformOut.translationZ = z;
1035        transformOut.dimAlpha = dimAlpha;
1036        transformOut.viewOutlineAlpha = viewOutlineAlpha;
1037        transformOut.rect.set(mTaskRect);
1038        transformOut.rect.offset(x, y);
1039        Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
1040        transformOut.visible = (transformOut.rect.top < mStackRect.bottom) &&
1041                (frontTransform == null || transformOut.rect.top != frontTransform.rect.top);
1042    }
1043
1044    /**
1045     * Returns the untransformed task view bounds.
1046     */
1047    public Rect getUntransformedTaskViewBounds() {
1048        return new Rect(mTaskRect);
1049    }
1050
1051    /**
1052     * Returns the scroll progress to scroll to such that the top of the task is at the top of the
1053     * stack.
1054     */
1055    float getStackScrollForTask(Task t) {
1056        Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null);
1057        if (overrideP == null) {
1058            return (float) mTaskIndexMap.get(t.key.id, 0);
1059        }
1060        return overrideP;
1061    }
1062
1063    /**
1064     * Returns the original scroll progress to scroll to such that the top of the task is at the top
1065     * of the stack.
1066     */
1067    float getStackScrollForTaskIgnoreOverrides(Task t) {
1068        return (float) mTaskIndexMap.get(t.key.id, 0);
1069    }
1070
1071    /**
1072     * Returns the scroll progress to scroll to such that the top of the task at the initial top
1073     * offset (which is at the task's brightest point).
1074     */
1075    float getStackScrollForTaskAtInitialOffset(Task t) {
1076        float normX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP);
1077        mUnfocusedRange.offset(0f);
1078        return Utilities.clamp((float) mTaskIndexMap.get(t.key.id, 0) - Math.max(0,
1079                mUnfocusedRange.getAbsoluteX(normX)), mMinScrollP, mMaxScrollP);
1080    }
1081
1082    /**
1083     * Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc
1084     * length of the curve.  We know the curve is mostly flat, so we just map the length of the
1085     * screen along the arc-length proportionally (1/arclength).
1086     */
1087    public float getDeltaPForY(int downY, int y) {
1088        float deltaP = (float) (y - downY) / mStackRect.height() *
1089                mUnfocusedCurveInterpolator.getArcLength();
1090        return -deltaP;
1091    }
1092
1093    /**
1094     * This is the inverse of {@link #getDeltaPForY}.  Given a movement along the arc length
1095     * of the curve, map back to the screen y.
1096     */
1097    public int getYForDeltaP(float downScrollP, float p) {
1098        int y = (int) ((p - downScrollP) * mStackRect.height() *
1099                (1f / mUnfocusedCurveInterpolator.getArcLength()));
1100        return -y;
1101    }
1102
1103    /**
1104     * Returns the task stack bounds in the current orientation.  This rect takes into account the
1105     * top and right system insets (but not the bottom inset) and left/right paddings, but _not_
1106     * the top/bottom padding or insets.
1107     */
1108    public void getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int leftInset,
1109            int rightInset, Rect taskStackBounds) {
1110        taskStackBounds.set(windowRect.left + leftInset, windowRect.top + topInset,
1111                windowRect.right - rightInset, windowRect.bottom);
1112
1113        // Ensure that the new width is at most the smaller display edge size
1114        int sideMargin = getScaleForExtent(windowRect, displayRect, mBaseSideMargin, mMinMargin,
1115                WIDTH);
1116        int targetStackWidth = taskStackBounds.width() - 2 * sideMargin;
1117        if (Utilities.getAppConfiguration(mContext).orientation
1118                == Configuration.ORIENTATION_LANDSCAPE) {
1119            // If we are in landscape, calculate the width of the stack in portrait and ensure that
1120            // we are not larger than that size
1121            Rect portraitDisplayRect = new Rect(0, 0,
1122                    Math.min(displayRect.width(), displayRect.height()),
1123                    Math.max(displayRect.width(), displayRect.height()));
1124            int portraitSideMargin = getScaleForExtent(portraitDisplayRect, portraitDisplayRect,
1125                    mBaseSideMargin, mMinMargin, WIDTH);
1126            targetStackWidth = Math.min(targetStackWidth,
1127                    portraitDisplayRect.width() - 2 * portraitSideMargin);
1128        }
1129        taskStackBounds.inset((taskStackBounds.width() - targetStackWidth) / 2, 0);
1130    }
1131
1132    /**
1133     * Retrieves resources that are constant regardless of the current configuration of the device.
1134     */
1135    public static int getDimensionForDevice(Context ctx, int phoneResId,
1136            int tabletResId, int xlargeTabletResId, int gridLayoutResId) {
1137        return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId,
1138                xlargeTabletResId, xlargeTabletResId, gridLayoutResId);
1139    }
1140
1141    /**
1142     * Retrieves resources that are constant regardless of the current configuration of the device.
1143     */
1144    public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId,
1145            int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId,
1146            int xlargeTabletLandResId, int gridLayoutResId) {
1147        RecentsConfiguration config = Recents.getConfiguration();
1148        Resources res = ctx.getResources();
1149        boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation ==
1150                Configuration.ORIENTATION_LANDSCAPE;
1151        if (config.isGridEnabled) {
1152            return res.getDimensionPixelSize(gridLayoutResId);
1153        } else if (config.isXLargeScreen) {
1154            return res.getDimensionPixelSize(isLandscape
1155                    ? xlargeTabletLandResId
1156                    : xlargeTabletPortResId);
1157        } else if (config.isLargeScreen) {
1158            return res.getDimensionPixelSize(isLandscape
1159                    ? tabletLandResId
1160                    : tabletPortResId);
1161        } else {
1162            return res.getDimensionPixelSize(isLandscape
1163                    ? phoneLandResId
1164                    : phonePortResId);
1165        }
1166    }
1167
1168    /**
1169     * Returns the normalized x on the unfocused curve given an absolute Y position (relative to the
1170     * stack height).
1171     */
1172    private float getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide) {
1173        float offset = (fromSide == FROM_TOP)
1174                ? mStackRect.height() - y
1175                : y;
1176        float offsetPct = offset / mStackRect.height();
1177        return mUnfocusedCurveInterpolator.getX(offsetPct);
1178    }
1179
1180    /**
1181     * Returns the normalized x on the focused curve given an absolute Y position (relative to the
1182     * stack height).
1183     */
1184    private float getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide) {
1185        float offset = (fromSide == FROM_TOP)
1186                ? mStackRect.height() - y
1187                : y;
1188        float offsetPct = offset / mStackRect.height();
1189        return mFocusedCurveInterpolator.getX(offsetPct);
1190    }
1191
1192    /**
1193     * Creates a new path for the focused curve.
1194     */
1195    private Path constructFocusedCurve() {
1196        // Initialize the focused curve. This curve is a piecewise curve composed of several
1197        // linear pieces that goes from (0,1) through (0.5, peek height offset),
1198        // (0.5, bottom task offsets), and (1,0).
1199        float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height();
1200        float bottomPeekHeightPct = (float) (mStackBottomOffset + mFocusedBottomPeekHeight) /
1201                mStackRect.height();
1202        float minBottomPeekHeightPct = (float) (mFocusedTopPeekHeight + mTaskRect.height() -
1203                mMinMargin) / mStackRect.height();
1204        Path p = new Path();
1205        p.moveTo(0f, 1f);
1206        p.lineTo(0.5f, 1f - topPeekHeightPct);
1207        p.lineTo(1f - (0.5f / mFocusedRange.relativeMax), Math.max(1f - minBottomPeekHeightPct,
1208                bottomPeekHeightPct));
1209        p.lineTo(1f, 0f);
1210        return p;
1211    }
1212
1213    /**
1214     * Creates a new path for the unfocused curve.
1215     */
1216    private Path constructUnfocusedCurve() {
1217        // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic
1218        // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0).  This
1219        // ensures that we match the range, at which 0.5 represents the stack scroll at the current
1220        // task progress.  Because the height offset can change depending on a resource, we compute
1221        // the control point of the second bezier such that between it and a first known point,
1222        // there is a tangent at (0.5, peek height offset).
1223        float cpoint1X = 0.4f;
1224        float cpoint1Y = 0.975f;
1225        float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height();
1226        float slope = ((1f - topPeekHeightPct) - cpoint1Y) / (0.5f - cpoint1X);
1227        float b = 1f - slope * cpoint1X;
1228        float cpoint2X = 0.65f;
1229        float cpoint2Y = slope * cpoint2X + b;
1230        Path p = new Path();
1231        p.moveTo(0f, 1f);
1232        p.cubicTo(0f, 1f, cpoint1X, cpoint1Y, 0.5f, 1f - topPeekHeightPct);
1233        p.cubicTo(0.5f, 1f - topPeekHeightPct, cpoint2X, cpoint2Y, 1f, 0f);
1234        return p;
1235    }
1236
1237    /**
1238     * Creates a new path for the focused dim curve.
1239     */
1240    private Path constructFocusedDimCurve() {
1241        Path p = new Path();
1242        // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused
1243        // task), then goes back to max dim at the next task
1244        p.moveTo(0f, MAX_DIM);
1245        p.lineTo(0.5f, 0f);
1246        p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM);
1247        p.lineTo(1f, MAX_DIM);
1248        return p;
1249    }
1250
1251    /**
1252     * Creates a new path for the unfocused dim curve.
1253     */
1254    private Path constructUnfocusedDimCurve() {
1255        float focusX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP);
1256        float cpoint2X = focusX + (1f - focusX) / 2;
1257        Path p = new Path();
1258        // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused
1259        // task), then goes back to max dim towards the front of the stack
1260        p.moveTo(0f, MAX_DIM);
1261        p.cubicTo(focusX * 0.5f, MAX_DIM, focusX * 0.75f, MAX_DIM * 0.75f, focusX, 0f);
1262        p.cubicTo(cpoint2X, 0f, cpoint2X, MED_DIM, 1f, MED_DIM);
1263        return p;
1264    }
1265
1266    /**
1267     * Scales the given {@param value} to the scale of the {@param instance} rect relative to the
1268     * {@param other} rect in the {@param extent} side.
1269     */
1270    private int getScaleForExtent(Rect instance, Rect other, int value, int minValue,
1271                                  @Extent int extent) {
1272        if (extent == WIDTH) {
1273            float scale = Utilities.clamp01((float) instance.width() / other.width());
1274            return Math.max(minValue, (int) (scale * value));
1275        } else if (extent == HEIGHT) {
1276            float scale = Utilities.clamp01((float) instance.height() / other.height());
1277            return Math.max(minValue, (int) (scale * value));
1278        }
1279        return value;
1280    }
1281
1282    /**
1283     * Updates the current transforms that would put a TaskView at the front and back of the stack.
1284     */
1285    private void updateFrontBackTransforms() {
1286        // Return early if we have not yet initialized
1287        if (mStackRect.isEmpty()) {
1288            return;
1289        }
1290
1291        float min = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMin,
1292                mFocusedRange.relativeMin);
1293        float max = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMax,
1294                mFocusedRange.relativeMax);
1295        getStackTransform(min, min, 0f, mFocusState, mBackOfStackTransform, null,
1296                true /* ignoreSingleTaskCase */, true /* forceUpdate */);
1297        getStackTransform(max, max, 0f, mFocusState, mFrontOfStackTransform, null,
1298                true /* ignoreSingleTaskCase */, true /* forceUpdate */);
1299        mBackOfStackTransform.visible = true;
1300        mFrontOfStackTransform.visible = true;
1301    }
1302
1303    /**
1304     * Returns the proper task rectangle according to the current grid state.
1305     */
1306    public Rect getTaskRect() {
1307        return useGridLayout() ? mTaskGridLayoutAlgorithm.getTaskGridRect() : mTaskRect;
1308    }
1309
1310    public void dump(String prefix, PrintWriter writer) {
1311        String innerPrefix = prefix + "  ";
1312
1313        writer.print(prefix); writer.print(TAG);
1314        writer.write(" numStackTasks="); writer.print(mNumStackTasks);
1315        writer.println();
1316
1317        writer.print(innerPrefix);
1318        writer.print("insets="); writer.print(Utilities.dumpRect(mSystemInsets));
1319        writer.print(" stack="); writer.print(Utilities.dumpRect(mStackRect));
1320        writer.print(" task="); writer.print(Utilities.dumpRect(mTaskRect));
1321        writer.print(" freeform="); writer.print(Utilities.dumpRect(mFreeformRect));
1322        writer.print(" actionButton="); writer.print(Utilities.dumpRect(mStackActionButtonRect));
1323        writer.println();
1324
1325        writer.print(innerPrefix);
1326        writer.print("minScroll="); writer.print(mMinScrollP);
1327        writer.print(" maxScroll="); writer.print(mMaxScrollP);
1328        writer.print(" initialScroll="); writer.print(mInitialScrollP);
1329        writer.println();
1330
1331        writer.print(innerPrefix);
1332        writer.print("focusState="); writer.print(mFocusState);
1333        writer.println();
1334
1335        if (mTaskIndexOverrideMap.size() > 0) {
1336            for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) {
1337                int taskId = mTaskIndexOverrideMap.keyAt(i);
1338                float x = mTaskIndexMap.get(taskId);
1339                float overrideX = mTaskIndexOverrideMap.get(taskId, 0f);
1340
1341                writer.print(innerPrefix);
1342                writer.print("taskId= "); writer.print(taskId);
1343                writer.print(" x= "); writer.print(x);
1344                writer.print(" overrideX= "); writer.print(overrideX);
1345                writer.println();
1346            }
1347        }
1348    }
1349}