1 package com.android.systemui.util 2 3 import android.graphics.Rect 4 import android.testing.AndroidTestingRunner 5 import android.testing.TestableLooper 6 import androidx.test.filters.SmallTest 7 import com.android.systemui.SysuiTestCase 8 import com.android.wm.shell.common.FloatingContentCoordinator 9 import org.junit.After 10 import org.junit.Assert.assertEquals 11 import org.junit.Assert.assertFalse 12 import org.junit.Before 13 import org.junit.Test 14 import org.junit.runner.RunWith 15 16 @TestableLooper.RunWithLooper 17 @RunWith(AndroidTestingRunner::class) 18 @SmallTest 19 class FloatingContentCoordinatorTest : SysuiTestCase() { 20 21 private val screenBounds = Rect(0, 0, 1000, 1000) 22 23 private val rect100px = Rect() 24 private val rect100pxFloating = FloatingRect(rect100px) 25 26 private val rect200px = Rect() 27 private val rect200pxFloating = FloatingRect(rect200px) 28 29 private val rect300px = Rect() 30 private val rect300pxFloating = FloatingRect(rect300px) 31 32 private val floatingCoordinator = FloatingContentCoordinator() 33 34 @Before 35 fun setup() { 36 rect100px.set(0, 0, 100, 100) 37 rect200px.set(0, 0, 200, 200) 38 rect300px.set(0, 0, 300, 300) 39 } 40 41 @After 42 fun tearDown() { 43 // We need to remove this stuff since it's a singleton object and it'll be there for the 44 // next test. 45 floatingCoordinator.onContentRemoved(rect100pxFloating) 46 floatingCoordinator.onContentRemoved(rect200pxFloating) 47 floatingCoordinator.onContentRemoved(rect300pxFloating) 48 } 49 50 @Test 51 fun testOnContentAdded() { 52 // Add rect1, and verify that the coordinator didn't move it. 53 floatingCoordinator.onContentAdded(rect100pxFloating) 54 assertEquals(rect100px.top, 0) 55 56 // Add rect2, which intersects rect1. Verify that rect2 was not moved, since newly added 57 // content is allowed to remain where it is. rect1 should have been moved below rect2 58 // since it was in the way. 59 floatingCoordinator.onContentAdded(rect200pxFloating) 60 assertEquals(rect200px.top, 0) 61 assertEquals(rect100px.top, 200) 62 63 verifyRectSizes() 64 } 65 66 @Test 67 fun testOnContentRemoved() { 68 // Add rect1, and remove it. Then add rect2. Since rect1 was removed before that, it should 69 // no longer be considered in the way, so it shouldn't move when rect2 is added. 70 floatingCoordinator.onContentAdded(rect100pxFloating) 71 floatingCoordinator.onContentRemoved(rect100pxFloating) 72 floatingCoordinator.onContentAdded(rect200pxFloating) 73 74 assertEquals(rect100px.top, 0) 75 assertEquals(rect200px.top, 0) 76 77 verifyRectSizes() 78 } 79 80 @Test 81 fun testOnContentMoved_twoRects() { 82 // Add rect1, which is at y = 0. 83 floatingCoordinator.onContentAdded(rect100pxFloating) 84 85 // Move rect2 down to 500px, where it won't conflict with rect1. 86 rect200px.offsetTo(0, 500) 87 floatingCoordinator.onContentAdded(rect200pxFloating) 88 89 // Then, move it to 0px where it will absolutely conflict with rect1. 90 rect200px.offsetTo(0, 0) 91 floatingCoordinator.onContentMoved(rect200pxFloating) 92 93 // The coordinator should have left rect2 alone, and moved rect1 below it. rect1 should now 94 // be at y = 200. 95 assertEquals(rect200px.top, 0) 96 assertEquals(rect100px.top, 200) 97 98 verifyRectSizes() 99 100 // Move rect2 to y = 275px. Since this puts it at the bottom half of rect1, it should push 101 // rect1 upward and leave rect2 alone. 102 rect200px.offsetTo(0, 275) 103 floatingCoordinator.onContentMoved(rect200pxFloating) 104 105 assertEquals(rect200px.top, 275) 106 assertEquals(rect100px.top, 175) 107 108 verifyRectSizes() 109 110 // Move rect2 to y = 110px. This makes it intersect rect1 again, but above its center of 111 // mass. That means rect1 should be pushed downward. 112 rect200px.offsetTo(0, 110) 113 floatingCoordinator.onContentMoved(rect200pxFloating) 114 115 assertEquals(rect200px.top, 110) 116 assertEquals(rect100px.top, 310) 117 118 verifyRectSizes() 119 } 120 121 @Test 122 fun testOnContentMoved_threeRects() { 123 floatingCoordinator.onContentAdded(rect100pxFloating) 124 125 // Add rect2, which should displace rect1 to y = 200 126 floatingCoordinator.onContentAdded(rect200pxFloating) 127 assertEquals(rect200px.top, 0) 128 assertEquals(rect100px.top, 200) 129 130 // Add rect3, which should completely cover both rect1 and rect2. That should cause them to 131 // move away. The order in which they do so is non-deterministic, so just make sure none of 132 // the three Rects intersect. 133 floatingCoordinator.onContentAdded(rect300pxFloating) 134 135 assertFalse(Rect.intersects(rect100px, rect200px)) 136 assertFalse(Rect.intersects(rect100px, rect300px)) 137 assertFalse(Rect.intersects(rect200px, rect300px)) 138 139 // Move rect2 to intersect both rect1 and rect3. 140 rect200px.offsetTo(0, 150) 141 floatingCoordinator.onContentMoved(rect200pxFloating) 142 143 assertFalse(Rect.intersects(rect100px, rect200px)) 144 assertFalse(Rect.intersects(rect100px, rect300px)) 145 assertFalse(Rect.intersects(rect200px, rect300px)) 146 } 147 148 @Test 149 fun testOnContentMoved_respectsUpperBounds() { 150 // Add rect1, which is at y = 0. 151 floatingCoordinator.onContentAdded(rect100pxFloating) 152 153 // Move rect2 down to 500px, where it won't conflict with rect1. 154 rect200px.offsetTo(0, 500) 155 floatingCoordinator.onContentAdded(rect200pxFloating) 156 157 // Then, move it to 90px where it will conflict with rect1, but with a center of mass below 158 // that of rect1's. This would normally mean that rect1 moves upward. However, since it's at 159 // the top of the screen, it should go downward instead. 160 rect200px.offsetTo(0, 90) 161 floatingCoordinator.onContentMoved(rect200pxFloating) 162 163 // rect2 should have been left alone, rect1 is now below rect2 at y = 290px even though it 164 // was intersected from below. 165 assertEquals(rect200px.top, 90) 166 assertEquals(rect100px.top, 290) 167 } 168 169 @Test 170 fun testOnContentMoved_respectsLowerBounds() { 171 // Put rect1 at the bottom of the screen and add it. 172 rect100px.offsetTo(0, screenBounds.bottom - 100) 173 floatingCoordinator.onContentAdded(rect100pxFloating) 174 175 // Put rect2 at the bottom as well. Since its center of mass is above rect1's, rect1 would 176 // normally move downward. Since it's at the bottom of the screen, it should go upward 177 // instead. 178 rect200px.offsetTo(0, 800) 179 floatingCoordinator.onContentAdded(rect200pxFloating) 180 181 assertEquals(rect200px.top, 800) 182 assertEquals(rect100px.top, 700) 183 } 184 185 /** 186 * Tests that the rect sizes didn't change when the coordinator manipulated them. This allows us 187 * to assert only the value of rect.top in tests, since if top, width, and height are correct, 188 * that means top/left/right/bottom are all correct. 189 */ 190 private fun verifyRectSizes() { 191 assertEquals(100, rect100px.width()) 192 assertEquals(200, rect200px.width()) 193 assertEquals(300, rect300px.width()) 194 195 assertEquals(100, rect100px.height()) 196 assertEquals(200, rect200px.height()) 197 assertEquals(300, rect300px.height()) 198 } 199 200 /** 201 * Helper class that uses [floatingCoordinator.findAreaForContentVertically] to move a 202 * Rect when needed. 203 */ 204 inner class FloatingRect( 205 private val underlyingRect: Rect 206 ) : FloatingContentCoordinator.FloatingContent { 207 override fun moveToBounds(bounds: Rect) { 208 underlyingRect.set(bounds) 209 } 210 211 override fun getAllowedFloatingBoundsRegion(): Rect { 212 return screenBounds 213 } 214 215 override fun getFloatingBoundsOnScreen(): Rect { 216 return underlyingRect 217 } 218 } 219 }