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