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         fun calculateNewBoundsOnOverlap(
98             overlappingContentBounds: Rect,
99             otherContentBounds: List<Rect>
100         ): Rect {
101             return findAreaForContentVertically(
102                     getFloatingBoundsOnScreen(),
103                     overlappingContentBounds,
104                     otherContentBounds,
105                     getAllowedFloatingBoundsRegion())
106         }
107     }
108 
109     /** The bounds of all pieces of floating content added to the coordinator. */
110     private val allContentBounds: MutableMap<FloatingContent, Rect> = HashMap()
111 
112     /**
113      * Whether we are currently resolving conflicts by asking content to move. If we are, we'll
114      * temporarily ignore calls to [onContentMoved] - those calls are from the content that is
115      * moving to new, conflict-free bounds, so we don't need to perform conflict detection
116      * calculations in response.
117      */
118     private var currentlyResolvingConflicts = false
119 
120     /**
121      * Makes the coordinator aware of a new piece of floating content, and moves any existing
122      * content out of the way, if necessary.
123      *
124      * If you don't want your new content to move existing content, use [getOccupiedBounds] to find
125      * an unoccupied area, and move the content there before calling this method.
126      */
127     fun onContentAdded(newContent: FloatingContent) {
128         updateContentBounds()
129         allContentBounds[newContent] = newContent.getFloatingBoundsOnScreen()
130         maybeMoveConflictingContent(newContent)
131     }
132 
133     /**
134      * Called to notify the coordinator that a piece of floating content has moved (or is animating)
135      * to a new position, and that any conflicting floating content should be moved out of the way.
136      *
137      * The coordinator will call [FloatingContent.getFloatingBoundsOnScreen] to find the new bounds
138      * for the moving content. If you're animating the content, be sure that your implementation of
139      * getFloatingBoundsOnScreen returns the bounds to which it's animating, not the content's
140      * current bounds.
141      *
142      * If the animation moving this content is cancelled or updated, you'll need to call this method
143      * again, to ensure that content is moved out of the way of the latest bounds.
144      *
145      * @param content The content that has moved.
146      */
147     fun onContentMoved(content: FloatingContent) {
148 
149         // Ignore calls when we are currently resolving conflicts, since those calls are from
150         // content that is moving to new, conflict-free bounds.
151         if (currentlyResolvingConflicts) {
152             return
153         }
154 
155         if (!allContentBounds.containsKey(content)) {
156             Log.wtf(TAG, "Received onContentMoved call before onContentAdded! " +
157                     "This should never happen.")
158             return
159         }
160 
161         updateContentBounds()
162         maybeMoveConflictingContent(content)
163     }
164 
165     /**
166      * Called to notify the coordinator that a piece of floating content has been removed or is no
167      * longer visible.
168      */
169     fun onContentRemoved(removedContent: FloatingContent) {
170         allContentBounds.remove(removedContent)
171     }
172 
173     /**
174      * Returns a set of Rects that represent the bounds of all of the floating content on the
175      * screen.
176      *
177      * [onContentAdded] will move existing content out of the way if the added content intersects
178      * existing content. That's fine - but if your specific starting position is not important, you
179      * can use this function to find unoccupied space for your content before calling
180      * [onContentAdded], so that moving existing content isn't necessary.
181      */
182     fun getOccupiedBounds(): Collection<Rect> {
183         return allContentBounds.values
184     }
185 
186     /**
187      * Identifies any pieces of content that are now overlapping with the given content, and asks
188      * them to move out of the way.
189      */
190     private fun maybeMoveConflictingContent(fromContent: FloatingContent) {
191         currentlyResolvingConflicts = true
192 
193         val conflictingNewBounds = allContentBounds[fromContent]!!
194         allContentBounds
195                 // Filter to content that intersects with the new bounds. That's content that needs
196                 // to move.
197                 .filter { (content, bounds) ->
198                     content != fromContent && Rect.intersects(conflictingNewBounds, bounds) }
199                 // Tell that content to get out of the way, and save the bounds it says it's moving
200                 // (or animating) to.
201                 .forEach { (content, bounds) ->
202                     val newBounds = content.calculateNewBoundsOnOverlap(
203                             conflictingNewBounds,
204                             // Pass all of the content bounds except the bounds of the
205                             // content we're asking to move, and the conflicting new bounds
206                             // (since those are passed separately).
207                             otherContentBounds = allContentBounds.values
208                                     .minus(bounds)
209                                     .minus(conflictingNewBounds))
210 
211                     // If the new bounds are empty, it means there's no non-overlapping position
212                     // that is in bounds. Just leave the content where it is. This should normally
213                     // not happen, but sometimes content like PIP reports incorrect bounds
214                     // temporarily.
215                     if (!newBounds.isEmpty) {
216                         content.moveToBounds(newBounds)
217                         allContentBounds[content] = content.getFloatingBoundsOnScreen()
218                     }
219                 }
220 
221         currentlyResolvingConflicts = false
222     }
223 
224     /**
225      * Update [allContentBounds] by calling [FloatingContent.getFloatingBoundsOnScreen] for all
226      * content and saving the result.
227      */
228     private fun updateContentBounds() {
229         allContentBounds.keys.forEach { allContentBounds[it] = it.getFloatingBoundsOnScreen() }
230     }
231 
232     companion object {
233         /**
234          * Finds new bounds for the given content, either above or below its current position. The
235          * new bounds won't intersect with the newly overlapping rect or the exclusion rects, and
236          * will be within the allowed bounds unless no possible position exists.
237          *
238          * You can use this method to help find a new position for your content when the coordinator
239          * calls [FloatingContent.moveToAreaExcluding].
240          *
241          * @param contentRect The bounds of the content for which we're finding a new home.
242          * @param newlyOverlappingRect The bounds of the content that forced this relocation by
243          * intersecting with the content we now need to move. If the overlapping content is
244          * overlapping the top half of this content, we'll try to move this content downward if
245          * possible (since the other content is 'pushing' it down), and vice versa.
246          * @param exclusionRects Any other areas that we need to avoid when finding a new home for
247          * the content. These areas must be non-overlapping with each other.
248          * @param allowedBounds The area within which we're allowed to find new bounds for the
249          * content.
250          * @return New bounds for the content that don't intersect the exclusion rects or the
251          * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds
252          * position exists.
253          */
254         @JvmStatic
255         fun findAreaForContentVertically(
256             contentRect: Rect,
257             newlyOverlappingRect: Rect,
258             exclusionRects: Collection<Rect>,
259             allowedBounds: Rect
260         ): Rect {
261             // If the newly overlapping Rect's center is above the content's center, we'll prefer to
262             // find a space for this content that is below the overlapping content, since it's
263             // 'pushing' it down. This may not be possible due to to screen bounds, in which case
264             // we'll find space in the other direction.
265             val overlappingContentPushingDown =
266                     newlyOverlappingRect.centerY() < contentRect.centerY()
267 
268             // Filter to exclusion rects that are above or below the content that we're finding a
269             // place for. Then, split into two lists - rects above the content, and rects below it.
270             var (rectsToAvoidAbove, rectsToAvoidBelow) = exclusionRects
271                     .filter { rectToAvoid -> rectsIntersectVertically(rectToAvoid, contentRect) }
272                     .partition { rectToAvoid -> rectToAvoid.top < contentRect.top }
273 
274             // Lazily calculate the closest possible new tops for the content, above and below its
275             // current location.
276             val newContentBoundsAbove by lazy {
277                 findAreaForContentAboveOrBelow(
278                         contentRect,
279                         exclusionRects = rectsToAvoidAbove.plus(newlyOverlappingRect),
280                         findAbove = true)
281             }
282             val newContentBoundsBelow by lazy {
283                 findAreaForContentAboveOrBelow(
284                         contentRect,
285                         exclusionRects = rectsToAvoidBelow.plus(newlyOverlappingRect),
286                         findAbove = false)
287             }
288 
289             val positionAboveInBounds by lazy { allowedBounds.contains(newContentBoundsAbove) }
290             val positionBelowInBounds by lazy { allowedBounds.contains(newContentBoundsBelow) }
291 
292             // Use the 'below' position if the content is being overlapped from the top, unless it's
293             // out of bounds. Also use it if the content is being overlapped from the bottom, but
294             // the 'above' position is out of bounds. Otherwise, use the 'above' position.
295             val usePositionBelow =
296                     overlappingContentPushingDown && positionBelowInBounds ||
297                             !overlappingContentPushingDown && !positionAboveInBounds
298 
299             // Return the content rect, but offset to reflect the new position.
300             val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove
301 
302             // If the new bounds are within the allowed bounds, return them. If not, it means that
303             // there are no legal new bounds. This can happen if the new content's bounds are too
304             // large (for example, full-screen PIP). Since there is no reasonable action to take
305             // here, return an empty Rect and we will just not move the content.
306             return if (allowedBounds.contains(newBounds)) newBounds else Rect()
307         }
308 
309         /**
310          * Finds a new position for the given content, either above or below its current position
311          * depending on whether [findAbove] is true or false, respectively. This new position will
312          * not intersect with any of the [exclusionRects].
313          *
314          * This method is useful as a helper method for implementing your own conflict resolution
315          * logic. Otherwise, you'd want to use [findAreaForContentVertically], which takes screen
316          * bounds and conflicting bounds' location into account when deciding whether to move to new
317          * bounds above or below the current bounds.
318          *
319          * @param contentRect The content we're finding an area for.
320          * @param exclusionRects The areas we need to avoid when finding a new area for the content.
321          * These areas must be non-overlapping with each other.
322          * @param findAbove Whether we are finding an area above the content's current position,
323          * rather than an area below it.
324          */
325         fun findAreaForContentAboveOrBelow(
326             contentRect: Rect,
327             exclusionRects: Collection<Rect>,
328             findAbove: Boolean
329         ): Rect {
330             // Sort the rects, since we want to move the content as little as possible. We'll
331             // start with the rects closest to the content and move outward. If we're finding an
332             // area above the content, that means we sort in reverse order to search the rects
333             // from highest to lowest y-value.
334             val sortedExclusionRects =
335                     exclusionRects.sortedBy { if (findAbove) -it.top else it.top }
336 
337             val proposedNewBounds = Rect(contentRect)
338             for (exclusionRect in sortedExclusionRects) {
339                 // If the proposed new bounds don't intersect with this exclusion rect, that
340                 // means there's room for the content here. We know this because the rects are
341                 // sorted and non-overlapping, so any subsequent exclusion rects would be higher
342                 // (or lower) than this one and can't possibly intersect if this one doesn't.
343                 if (!Rect.intersects(proposedNewBounds, exclusionRect)) {
344                     break
345                 } else {
346                     // Otherwise, we need to keep searching for new bounds. If we're finding an
347                     // area above, propose new bounds that place the content just above the
348                     // exclusion rect. If we're finding an area below, propose new bounds that
349                     // place the content just below the exclusion rect.
350                     val verticalOffset =
351                             if (findAbove) -contentRect.height() else exclusionRect.height()
352                     proposedNewBounds.offsetTo(
353                             proposedNewBounds.left,
354                             exclusionRect.top + verticalOffset)
355                 }
356             }
357 
358             return proposedNewBounds
359         }
360 
361         /** Returns whether or not the two Rects share any of the same space on the X axis. */
362         private fun rectsIntersectVertically(r1: Rect, r2: Rect): Boolean {
363             return (r1.left >= r2.left && r1.left <= r2.right) ||
364                     (r1.right <= r2.right && r1.right >= r2.left)
365         }
366     }
367 }
368