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