• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 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.wm.shell.common
18 
19 import android.graphics.Rect
20 import android.util.Log
21 import com.android.wm.shell.common.FloatingContentCoordinator.FloatingContent
22 import java.util.HashMap
23 
24 /** Tag for debug logging. */
25 private const val TAG = "FloatingCoordinator"
26 
27 /**
28  * Coordinates the positions and movement of floating content, such as PIP and Bubbles, to ensure
29  * that they don't overlap. If content does overlap due to content appearing or moving, the
30  * coordinator will ask content to move to resolve the conflict.
31  *
32  * After implementing [FloatingContent], content should call [onContentAdded] to begin coordination.
33  * Subsequently, call [onContentMoved] whenever the content moves, and the coordinator will move
34  * other content out of the way. [onContentRemoved] should be called when the content is removed or
35  * no longer visible.
36  */
37 
38 class FloatingContentCoordinator constructor() {
39     /**
40      * Represents a piece of floating content, such as PIP or the Bubbles stack. Provides methods
41      * that allow the [FloatingContentCoordinator] to determine the current location of the content,
42      * as well as the ability to ask it to move out of the way of other content.
43      *
44      * The default implementation of [calculateNewBoundsOnOverlap] moves the content up or down,
45      * depending on the position of the conflicting content. You can override this method if you
46      * want your own custom conflict resolution logic.
47      */
48     interface FloatingContent {
49 
50         /**
51          * Return the bounds claimed by this content. This should include the bounds occupied by the
52          * content itself, as well as any padding, if desired. The coordinator will ensure that no
53          * other content is located within these bounds.
54          *
55          * If the content is animating, this method should return the bounds to which the content is
56          * animating. If that animation is cancelled, or updated, be sure that your implementation
57          * of this method returns the appropriate bounds, and call [onContentMoved] so that the
58          * coordinator moves other content out of the way.
59          */
60         fun getFloatingBoundsOnScreen(): Rect
61 
62         /**
63          * Return the area within which this floating content is allowed to move. When resolving
64          * conflicts, the coordinator will never ask your content to move to a position where any
65          * part of the content would be out of these bounds.
66          */
67         fun getAllowedFloatingBoundsRegion(): Rect
68 
69         /**
70          * Called when the coordinator needs this content to move to the given bounds. It's up to
71          * you how to do that.
72          *
73          * Note that if you start an animation to these bounds, [getFloatingBoundsOnScreen] should
74          * return the destination bounds, not the in-progress animated bounds. This is so the
75          * coordinator knows where floating content is going to be and can resolve conflicts
76          * accordingly.
77          */
78         fun moveToBounds(bounds: Rect)
79 
80         /**
81          * Called by the coordinator when it needs to find a new home for this floating content,
82          * because a new or moving piece of content is now overlapping with it.
83          *
84          * [findAreaForContentVertically] and [findAreaForContentAboveOrBelow] are helpful utility
85          * functions that will find new bounds for your content automatically. Unless you require
86          * specific conflict resolution logic, these should be sufficient. By default, this method
87          * delegates to [findAreaForContentVertically].
88          *
89          * @param overlappingContentBounds The bounds of the other piece of content, which
90          * necessitated this content's relocation. Your new position must not overlap with these
91          * bounds.
92          * @param otherContentBounds The bounds of any other pieces of floating content. Your new
93          * position must not overlap with any of these either. These bounds are guaranteed to be
94          * non-overlapping.
95          * @return The new bounds for this content.
96          */
97         @JvmDefault
98         fun calculateNewBoundsOnOverlap(
99             overlappingContentBounds: Rect,
100             otherContentBounds: List<Rect>
101         ): Rect {
102             return findAreaForContentVertically(
103                     getFloatingBoundsOnScreen(),
104                     overlappingContentBounds,
105                     otherContentBounds,
106                     getAllowedFloatingBoundsRegion())
107         }
108     }
109 
110     /** The bounds of all pieces of floating content added to the coordinator. */
111     private val allContentBounds: MutableMap<FloatingContent, Rect> = HashMap()
112 
113     /**
114      * Whether we are currently resolving conflicts by asking content to move. If we are, we'll
115      * temporarily ignore calls to [onContentMoved] - those calls are from the content that is
116      * moving to new, conflict-free bounds, so we don't need to perform conflict detection
117      * calculations in response.
118      */
119     private var currentlyResolvingConflicts = false
120 
121     /**
122      * Makes the coordinator aware of a new piece of floating content, and moves any existing
123      * content out of the way, if necessary.
124      *
125      * If you don't want your new content to move existing content, use [getOccupiedBounds] to find
126      * an unoccupied area, and move the content there before calling this method.
127      */
128     fun onContentAdded(newContent: FloatingContent) {
129         updateContentBounds()
130         allContentBounds[newContent] = newContent.getFloatingBoundsOnScreen()
131         maybeMoveConflictingContent(newContent)
132     }
133 
134     /**
135      * Called to notify the coordinator that a piece of floating content has moved (or is animating)
136      * to a new position, and that any conflicting floating content should be moved out of the way.
137      *
138      * The coordinator will call [FloatingContent.getFloatingBoundsOnScreen] to find the new bounds
139      * for the moving content. If you're animating the content, be sure that your implementation of
140      * getFloatingBoundsOnScreen returns the bounds to which it's animating, not the content's
141      * current bounds.
142      *
143      * If the animation moving this content is cancelled or updated, you'll need to call this method
144      * again, to ensure that content is moved out of the way of the latest bounds.
145      *
146      * @param content The content that has moved.
147      */
148     fun onContentMoved(content: FloatingContent) {
149 
150         // Ignore calls when we are currently resolving conflicts, since those calls are from
151         // content that is moving to new, conflict-free bounds.
152         if (currentlyResolvingConflicts) {
153             return
154         }
155 
156         if (!allContentBounds.containsKey(content)) {
157             Log.wtf(TAG, "Received onContentMoved call before onContentAdded! " +
158                     "This should never happen.")
159             return
160         }
161 
162         updateContentBounds()
163         maybeMoveConflictingContent(content)
164     }
165 
166     /**
167      * Called to notify the coordinator that a piece of floating content has been removed or is no
168      * longer visible.
169      */
170     fun onContentRemoved(removedContent: FloatingContent) {
171         allContentBounds.remove(removedContent)
172     }
173 
174     /**
175      * Returns a set of Rects that represent the bounds of all of the floating content on the
176      * screen.
177      *
178      * [onContentAdded] will move existing content out of the way if the added content intersects
179      * existing content. That's fine - but if your specific starting position is not important, you
180      * can use this function to find unoccupied space for your content before calling
181      * [onContentAdded], so that moving existing content isn't necessary.
182      */
183     fun getOccupiedBounds(): Collection<Rect> {
184         return allContentBounds.values
185     }
186 
187     /**
188      * Identifies any pieces of content that are now overlapping with the given content, and asks
189      * them to move out of the way.
190      */
191     private fun maybeMoveConflictingContent(fromContent: FloatingContent) {
192         currentlyResolvingConflicts = true
193 
194         val conflictingNewBounds = allContentBounds[fromContent]!!
195         allContentBounds
196                 // Filter to content that intersects with the new bounds. That's content that needs
197                 // to move.
198                 .filter { (content, bounds) ->
199                     content != fromContent && Rect.intersects(conflictingNewBounds, bounds) }
200                 // Tell that content to get out of the way, and save the bounds it says it's moving
201                 // (or animating) to.
202                 .forEach { (content, bounds) ->
203                     val newBounds = content.calculateNewBoundsOnOverlap(
204                             conflictingNewBounds,
205                             // Pass all of the content bounds except the bounds of the
206                             // content we're asking to move, and the conflicting new bounds
207                             // (since those are passed separately).
208                             otherContentBounds = allContentBounds.values
209                                     .minus(bounds)
210                                     .minus(conflictingNewBounds))
211 
212                     // If the new bounds are empty, it means there's no non-overlapping position
213                     // that is in bounds. Just leave the content where it is. This should normally
214                     // not happen, but sometimes content like PIP reports incorrect bounds
215                     // temporarily.
216                     if (!newBounds.isEmpty) {
217                         content.moveToBounds(newBounds)
218                         allContentBounds[content] = content.getFloatingBoundsOnScreen()
219                     }
220                 }
221 
222         currentlyResolvingConflicts = false
223     }
224 
225     /**
226      * Update [allContentBounds] by calling [FloatingContent.getFloatingBoundsOnScreen] for all
227      * content and saving the result.
228      */
229     private fun updateContentBounds() {
230         allContentBounds.keys.forEach { allContentBounds[it] = it.getFloatingBoundsOnScreen() }
231     }
232 
233     companion object {
234         /**
235          * Finds new bounds for the given content, either above or below its current position. The
236          * new bounds won't intersect with the newly overlapping rect or the exclusion rects, and
237          * will be within the allowed bounds unless no possible position exists.
238          *
239          * You can use this method to help find a new position for your content when the coordinator
240          * calls [FloatingContent.moveToAreaExcluding].
241          *
242          * @param contentRect The bounds of the content for which we're finding a new home.
243          * @param newlyOverlappingRect The bounds of the content that forced this relocation by
244          * intersecting with the content we now need to move. If the overlapping content is
245          * overlapping the top half of this content, we'll try to move this content downward if
246          * possible (since the other content is 'pushing' it down), and vice versa.
247          * @param exclusionRects Any other areas that we need to avoid when finding a new home for
248          * the content. These areas must be non-overlapping with each other.
249          * @param allowedBounds The area within which we're allowed to find new bounds for the
250          * content.
251          * @return New bounds for the content that don't intersect the exclusion rects or the
252          * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds
253          * position exists.
254          */
255         @JvmStatic
256         fun findAreaForContentVertically(
257             contentRect: Rect,
258             newlyOverlappingRect: Rect,
259             exclusionRects: Collection<Rect>,
260             allowedBounds: Rect
261         ): Rect {
262             // If the newly overlapping Rect's center is above the content's center, we'll prefer to
263             // find a space for this content that is below the overlapping content, since it's
264             // 'pushing' it down. This may not be possible due to to screen bounds, in which case
265             // we'll find space in the other direction.
266             val overlappingContentPushingDown =
267                     newlyOverlappingRect.centerY() < contentRect.centerY()
268 
269             // Filter to exclusion rects that are above or below the content that we're finding a
270             // place for. Then, split into two lists - rects above the content, and rects below it.
271             var (rectsToAvoidAbove, rectsToAvoidBelow) = exclusionRects
272                     .filter { rectToAvoid -> rectsIntersectVertically(rectToAvoid, contentRect) }
273                     .partition { rectToAvoid -> rectToAvoid.top < contentRect.top }
274 
275             // Lazily calculate the closest possible new tops for the content, above and below its
276             // current location.
277             val newContentBoundsAbove by lazy {
278                 findAreaForContentAboveOrBelow(
279                         contentRect,
280                         exclusionRects = rectsToAvoidAbove.plus(newlyOverlappingRect),
281                         findAbove = true)
282             }
283             val newContentBoundsBelow by lazy {
284                 findAreaForContentAboveOrBelow(
285                         contentRect,
286                         exclusionRects = rectsToAvoidBelow.plus(newlyOverlappingRect),
287                         findAbove = false)
288             }
289 
290             val positionAboveInBounds by lazy { allowedBounds.contains(newContentBoundsAbove) }
291             val positionBelowInBounds by lazy { allowedBounds.contains(newContentBoundsBelow) }
292 
293             // Use the 'below' position if the content is being overlapped from the top, unless it's
294             // out of bounds. Also use it if the content is being overlapped from the bottom, but
295             // the 'above' position is out of bounds. Otherwise, use the 'above' position.
296             val usePositionBelow =
297                     overlappingContentPushingDown && positionBelowInBounds ||
298                             !overlappingContentPushingDown && !positionAboveInBounds
299 
300             // Return the content rect, but offset to reflect the new position.
301             val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove
302 
303             // If the new bounds are within the allowed bounds, return them. If not, it means that
304             // there are no legal new bounds. This can happen if the new content's bounds are too
305             // large (for example, full-screen PIP). Since there is no reasonable action to take
306             // here, return an empty Rect and we will just not move the content.
307             return if (allowedBounds.contains(newBounds)) newBounds else Rect()
308         }
309 
310         /**
311          * Finds a new position for the given content, either above or below its current position
312          * depending on whether [findAbove] is true or false, respectively. This new position will
313          * not intersect with any of the [exclusionRects].
314          *
315          * This method is useful as a helper method for implementing your own conflict resolution
316          * logic. Otherwise, you'd want to use [findAreaForContentVertically], which takes screen
317          * bounds and conflicting bounds' location into account when deciding whether to move to new
318          * bounds above or below the current bounds.
319          *
320          * @param contentRect The content we're finding an area for.
321          * @param exclusionRects The areas we need to avoid when finding a new area for the content.
322          * These areas must be non-overlapping with each other.
323          * @param findAbove Whether we are finding an area above the content's current position,
324          * rather than an area below it.
325          */
326         fun findAreaForContentAboveOrBelow(
327             contentRect: Rect,
328             exclusionRects: Collection<Rect>,
329             findAbove: Boolean
330         ): Rect {
331             // Sort the rects, since we want to move the content as little as possible. We'll
332             // start with the rects closest to the content and move outward. If we're finding an
333             // area above the content, that means we sort in reverse order to search the rects
334             // from highest to lowest y-value.
335             val sortedExclusionRects =
336                     exclusionRects.sortedBy { if (findAbove) -it.top else it.top }
337 
338             val proposedNewBounds = Rect(contentRect)
339             for (exclusionRect in sortedExclusionRects) {
340                 // If the proposed new bounds don't intersect with this exclusion rect, that
341                 // means there's room for the content here. We know this because the rects are
342                 // sorted and non-overlapping, so any subsequent exclusion rects would be higher
343                 // (or lower) than this one and can't possibly intersect if this one doesn't.
344                 if (!Rect.intersects(proposedNewBounds, exclusionRect)) {
345                     break
346                 } else {
347                     // Otherwise, we need to keep searching for new bounds. If we're finding an
348                     // area above, propose new bounds that place the content just above the
349                     // exclusion rect. If we're finding an area below, propose new bounds that
350                     // place the content just below the exclusion rect.
351                     val verticalOffset =
352                             if (findAbove) -contentRect.height() else exclusionRect.height()
353                     proposedNewBounds.offsetTo(
354                             proposedNewBounds.left,
355                             exclusionRect.top + verticalOffset)
356                 }
357             }
358 
359             return proposedNewBounds
360         }
361 
362         /** Returns whether or not the two Rects share any of the same space on the X axis. */
363         private fun rectsIntersectVertically(r1: Rect, r2: Rect): Boolean {
364             return (r1.left >= r2.left && r1.left <= r2.right) ||
365                     (r1.right <= r2.right && r1.right >= r2.left)
366         }
367     }
368 }
369