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 }