1/*
2 * Copyright (C) 2015 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.internal.policy;
18
19import static android.view.WindowManager.DOCKED_INVALID;
20import static android.view.WindowManager.DOCKED_LEFT;
21import static android.view.WindowManager.DOCKED_RIGHT;
22
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.graphics.Rect;
27import android.hardware.display.DisplayManager;
28import android.view.Display;
29import android.view.DisplayInfo;
30
31import java.util.ArrayList;
32
33/**
34 * Calculates the snap targets and the snap position given a position and a velocity. All positions
35 * here are to be interpreted as the left/top edge of the divider rectangle.
36 *
37 * @hide
38 */
39public class DividerSnapAlgorithm {
40
41    private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
42    private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
43
44    /**
45     * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
46     */
47    private static final int SNAP_MODE_16_9 = 0;
48
49    /**
50     * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
51     */
52    private static final int SNAP_FIXED_RATIO = 1;
53
54    /**
55     * 1 snap target: 1:1
56     */
57    private static final int SNAP_ONLY_1_1 = 2;
58
59    /**
60     * 1 snap target: minimized height, (1 - minimized height)
61     */
62    private static final int SNAP_MODE_MINIMIZED = 3;
63
64    private final float mMinFlingVelocityPxPerSecond;
65    private final float mMinDismissVelocityPxPerSecond;
66    private final int mDisplayWidth;
67    private final int mDisplayHeight;
68    private final int mDividerSize;
69    private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
70    private final Rect mInsets = new Rect();
71    private final int mSnapMode;
72    private final int mMinimalSizeResizableTask;
73    private final int mTaskHeightInMinimizedMode;
74    private final float mFixedRatio;
75    private boolean mIsHorizontalDivision;
76
77    /** The first target which is still splitting the screen */
78    private final SnapTarget mFirstSplitTarget;
79
80    /** The last target which is still splitting the screen */
81    private final SnapTarget mLastSplitTarget;
82
83    private final SnapTarget mDismissStartTarget;
84    private final SnapTarget mDismissEndTarget;
85    private final SnapTarget mMiddleTarget;
86
87    public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
88        DisplayInfo displayInfo = new DisplayInfo();
89        ctx.getSystemService(DisplayManager.class).getDisplay(
90                Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
91        int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
92                com.android.internal.R.dimen.docked_stack_divider_thickness);
93        int dividerInsets = ctx.getResources().getDimensionPixelSize(
94                com.android.internal.R.dimen.docked_stack_divider_insets);
95        return new DividerSnapAlgorithm(ctx.getResources(),
96                displayInfo.logicalWidth, displayInfo.logicalHeight,
97                dividerWindowWidth - 2 * dividerInsets,
98                ctx.getApplicationContext().getResources().getConfiguration().orientation
99                        == Configuration.ORIENTATION_PORTRAIT,
100                insets);
101    }
102
103    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
104            boolean isHorizontalDivision, Rect insets) {
105        this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
106                DOCKED_INVALID, false);
107    }
108
109    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
110        boolean isHorizontalDivision, Rect insets, int dockSide) {
111        this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
112            dockSide, false);
113    }
114
115    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
116            boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) {
117        mMinFlingVelocityPxPerSecond =
118                MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
119        mMinDismissVelocityPxPerSecond =
120                MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
121        mDividerSize = dividerSize;
122        mDisplayWidth = displayWidth;
123        mDisplayHeight = displayHeight;
124        mIsHorizontalDivision = isHorizontalDivision;
125        mInsets.set(insets);
126        mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED :
127                res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode);
128        mFixedRatio = res.getFraction(
129                com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
130        mMinimalSizeResizableTask = res.getDimensionPixelSize(
131                com.android.internal.R.dimen.default_minimal_size_resizable_task);
132        mTaskHeightInMinimizedMode = res.getDimensionPixelSize(
133                com.android.internal.R.dimen.task_height_of_minimized_mode);
134        calculateTargets(isHorizontalDivision, dockSide);
135        mFirstSplitTarget = mTargets.get(1);
136        mLastSplitTarget = mTargets.get(mTargets.size() - 2);
137        mDismissStartTarget = mTargets.get(0);
138        mDismissEndTarget = mTargets.get(mTargets.size() - 1);
139        mMiddleTarget = mTargets.get(mTargets.size() / 2);
140    }
141
142    /**
143     * @return whether it's feasible to enable split screen in the current configuration, i.e. when
144     *         snapping in the middle both tasks are larger than the minimal task size.
145     */
146    public boolean isSplitScreenFeasible() {
147        int statusBarSize = mInsets.top;
148        int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
149        int size = mIsHorizontalDivision
150                ? mDisplayHeight
151                : mDisplayWidth;
152        int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
153        return availableSpace / 2 >= mMinimalSizeResizableTask;
154    }
155
156    public SnapTarget calculateSnapTarget(int position, float velocity) {
157        return calculateSnapTarget(position, velocity, true /* hardDismiss */);
158    }
159
160    /**
161     * @param position the top/left position of the divider
162     * @param velocity current dragging velocity
163     * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
164     */
165    public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
166        if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
167            return mDismissStartTarget;
168        }
169        if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
170            return mDismissEndTarget;
171        }
172        if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
173            return snap(position, hardDismiss);
174        }
175        if (velocity < 0) {
176            return mFirstSplitTarget;
177        } else {
178            return mLastSplitTarget;
179        }
180    }
181
182    public SnapTarget calculateNonDismissingSnapTarget(int position) {
183        SnapTarget target = snap(position, false /* hardDismiss */);
184        if (target == mDismissStartTarget) {
185            return mFirstSplitTarget;
186        } else if (target == mDismissEndTarget) {
187            return mLastSplitTarget;
188        } else {
189            return target;
190        }
191    }
192
193    public float calculateDismissingFraction(int position) {
194        if (position < mFirstSplitTarget.position) {
195            return 1f - (float) (position - getStartInset())
196                    / (mFirstSplitTarget.position - getStartInset());
197        } else if (position > mLastSplitTarget.position) {
198            return (float) (position - mLastSplitTarget.position)
199                    / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
200        }
201        return 0f;
202    }
203
204    public SnapTarget getClosestDismissTarget(int position) {
205        if (position < mFirstSplitTarget.position) {
206            return mDismissStartTarget;
207        } else if (position > mLastSplitTarget.position) {
208            return mDismissEndTarget;
209        } else if (position - mDismissStartTarget.position
210                < mDismissEndTarget.position - position) {
211            return mDismissStartTarget;
212        } else {
213            return mDismissEndTarget;
214        }
215    }
216
217    public SnapTarget getFirstSplitTarget() {
218        return mFirstSplitTarget;
219    }
220
221    public SnapTarget getLastSplitTarget() {
222        return mLastSplitTarget;
223    }
224
225    public SnapTarget getDismissStartTarget() {
226        return mDismissStartTarget;
227    }
228
229    public SnapTarget getDismissEndTarget() {
230        return mDismissEndTarget;
231    }
232
233    private int getStartInset() {
234        if (mIsHorizontalDivision) {
235            return mInsets.top;
236        } else {
237            return mInsets.left;
238        }
239    }
240
241    private int getEndInset() {
242        if (mIsHorizontalDivision) {
243            return mInsets.bottom;
244        } else {
245            return mInsets.right;
246        }
247    }
248
249    private SnapTarget snap(int position, boolean hardDismiss) {
250        int minIndex = -1;
251        float minDistance = Float.MAX_VALUE;
252        int size = mTargets.size();
253        for (int i = 0; i < size; i++) {
254            SnapTarget target = mTargets.get(i);
255            float distance = Math.abs(position - target.position);
256            if (hardDismiss) {
257                distance /= target.distanceMultiplier;
258            }
259            if (distance < minDistance) {
260                minIndex = i;
261                minDistance = distance;
262            }
263        }
264        return mTargets.get(minIndex);
265    }
266
267    private void calculateTargets(boolean isHorizontalDivision, int dockedSide) {
268        mTargets.clear();
269        int dividerMax = isHorizontalDivision
270                ? mDisplayHeight
271                : mDisplayWidth;
272        int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
273        int startPos = -mDividerSize;
274        if (dockedSide == DOCKED_RIGHT) {
275            startPos += mInsets.left;
276        }
277        mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START,
278                0.35f));
279        switch (mSnapMode) {
280            case SNAP_MODE_16_9:
281                addRatio16_9Targets(isHorizontalDivision, dividerMax);
282                break;
283            case SNAP_FIXED_RATIO:
284                addFixedDivisionTargets(isHorizontalDivision, dividerMax);
285                break;
286            case SNAP_ONLY_1_1:
287                addMiddleTarget(isHorizontalDivision);
288                break;
289            case SNAP_MODE_MINIMIZED:
290                addMinimizedTarget(isHorizontalDivision, dockedSide);
291                break;
292        }
293        mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
294                SnapTarget.FLAG_DISMISS_END, 0.35f));
295    }
296
297    private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
298            int bottomPosition, int dividerMax) {
299        maybeAddTarget(topPosition, topPosition - mInsets.top);
300        addMiddleTarget(isHorizontalDivision);
301        maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom
302                - (bottomPosition + mDividerSize));
303    }
304
305    private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
306        int start = isHorizontalDivision ? mInsets.top : mInsets.left;
307        int end = isHorizontalDivision
308                ? mDisplayHeight - mInsets.bottom
309                : mDisplayWidth - mInsets.right;
310        int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
311        int topPosition = start + size;
312        int bottomPosition = end - size - mDividerSize;
313        addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
314    }
315
316    private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
317        int start = isHorizontalDivision ? mInsets.top : mInsets.left;
318        int end = isHorizontalDivision
319                ? mDisplayHeight - mInsets.bottom
320                : mDisplayWidth - mInsets.right;
321        int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
322        int endOther = isHorizontalDivision
323                ? mDisplayWidth - mInsets.right
324                : mDisplayHeight - mInsets.bottom;
325        float size = 9.0f / 16.0f * (endOther - startOther);
326        int sizeInt = (int) Math.floor(size);
327        int topPosition = start + sizeInt;
328        int bottomPosition = end - sizeInt - mDividerSize;
329        addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
330    }
331
332    /**
333     * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
334     * meets the minimal size requirement.
335     */
336    private void maybeAddTarget(int position, int smallerSize) {
337        if (smallerSize >= mMinimalSizeResizableTask) {
338            mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
339        }
340    }
341
342    private void addMiddleTarget(boolean isHorizontalDivision) {
343        int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
344                mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
345        mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
346    }
347
348    private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
349        // In portrait offset the position by the statusbar height, in landscape add the statusbar
350        // height as well to match portrait offset
351        int position = mTaskHeightInMinimizedMode + mInsets.top;
352        if (!isHorizontalDivision) {
353            if (dockedSide == DOCKED_LEFT) {
354                position += mInsets.left;
355            } else if (dockedSide == DOCKED_RIGHT) {
356                position = mDisplayWidth - position - mInsets.right - mDividerSize;
357            }
358        }
359        mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
360    }
361
362    public SnapTarget getMiddleTarget() {
363        return mMiddleTarget;
364    }
365
366    public SnapTarget getNextTarget(SnapTarget snapTarget) {
367        int index = mTargets.indexOf(snapTarget);
368        if (index != -1 && index < mTargets.size() - 1) {
369            return mTargets.get(index + 1);
370        }
371        return snapTarget;
372    }
373
374    public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
375        int index = mTargets.indexOf(snapTarget);
376        if (index != -1 && index > 0) {
377            return mTargets.get(index - 1);
378        }
379        return snapTarget;
380    }
381
382    /**
383     * @return whether or not there are more than 1 split targets that do not include the two
384     * dismiss targets, used in deciding to display the middle target for accessibility
385     */
386    public boolean showMiddleSplitTargetForAccessibility() {
387        return (mTargets.size() - 2) > 1;
388    }
389
390    public boolean isFirstSplitTargetAvailable() {
391        return mFirstSplitTarget != mMiddleTarget;
392    }
393
394    public boolean isLastSplitTargetAvailable() {
395        return mLastSplitTarget != mMiddleTarget;
396    }
397
398    /**
399     * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
400     * if {@param increment} is negative and moves right otherwise.
401     */
402    public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
403        int index = mTargets.indexOf(snapTarget);
404        if (index != -1) {
405            SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
406                    % mTargets.size());
407            if (newTarget == mDismissStartTarget) {
408                return mLastSplitTarget;
409            } else if (newTarget == mDismissEndTarget) {
410                return mFirstSplitTarget;
411            } else {
412                return newTarget;
413            }
414        }
415        return snapTarget;
416    }
417
418    /**
419     * Represents a snap target for the divider.
420     */
421    public static class SnapTarget {
422        public static final int FLAG_NONE = 0;
423
424        /** If the divider reaches this value, the left/top task should be dismissed. */
425        public static final int FLAG_DISMISS_START = 1;
426
427        /** If the divider reaches this value, the right/bottom task should be dismissed */
428        public static final int FLAG_DISMISS_END = 2;
429
430        /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
431        public final int position;
432
433        /**
434         * Like {@link #position}, but used to calculate the task bounds which might be different
435         * from the stack bounds.
436         */
437        public final int taskPosition;
438
439        public final int flag;
440
441        /**
442         * Multiplier used to calculate distance to snap position. The lower this value, the harder
443         * it's to snap on this target
444         */
445        private final float distanceMultiplier;
446
447        public SnapTarget(int position, int taskPosition, int flag) {
448            this(position, taskPosition, flag, 1f);
449        }
450
451        public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
452            this.position = position;
453            this.taskPosition = taskPosition;
454            this.flag = flag;
455            this.distanceMultiplier = distanceMultiplier;
456        }
457    }
458}
459