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