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