1 /* 2 * Copyright (C) 2024 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.launcher3 18 19 import android.content.Context 20 import android.content.ContextWrapper 21 import android.graphics.Rect 22 import android.graphics.RectF 23 import android.view.View 24 import android.view.ViewGroup 25 import androidx.test.core.app.ApplicationProvider.getApplicationContext 26 import androidx.test.ext.junit.runners.AndroidJUnit4 27 import com.android.launcher3.util.ActivityContextWrapper 28 import kotlin.random.Random 29 import org.junit.Assert.assertArrayEquals 30 import org.junit.Assert.assertEquals 31 import org.junit.Assert.assertFalse 32 import org.junit.Assert.assertTrue 33 import org.junit.Before 34 import org.junit.Test 35 import org.junit.runner.RunWith 36 37 @RunWith(AndroidJUnit4::class) 38 class UtilitiesTest { 39 40 companion object { 41 const val SEED = 827 42 } 43 44 private lateinit var mContext: Context 45 46 @Before setUpnull47 fun setUp() { 48 mContext = ActivityContextWrapper(getApplicationContext()) 49 } 50 51 @Test testIsPropertyEnablednull52 fun testIsPropertyEnabled() { 53 // This assumes the property "propertyName" is not enabled by default 54 assertFalse(Utilities.isPropertyEnabled("propertyName")) 55 } 56 57 @Test testGetDescendantCoordRelativeToAncestornull58 fun testGetDescendantCoordRelativeToAncestor() { 59 val ancestor = 60 object : ViewGroup(mContext) { 61 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {} 62 } 63 val descendant = View(mContext) 64 65 descendant.x = 50f 66 descendant.y = 30f 67 descendant.scaleX = 2f 68 descendant.scaleY = 2f 69 70 ancestor.addView(descendant) 71 72 val coord = floatArrayOf(10f, 15f) 73 val scale = 74 Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, coord, false) 75 76 assertEquals(2f, scale) // Expecting scale to be 2f 77 assertEquals(70f, coord[0]) 78 assertEquals(60f, coord[1]) 79 } 80 81 @Test testRoundArraynull82 fun testRoundArray() { 83 val floatArray = floatArrayOf(1.2f, 3.7f, 5.5f) 84 val intArray = IntArray(3) 85 Utilities.roundArray(floatArray, intArray) 86 assertArrayEquals(intArrayOf(1, 4, 6), intArray) 87 } 88 89 @Test testOffsetPointsnull90 fun testOffsetPoints() { 91 val points = floatArrayOf(1f, 2f, 3f, 4f) 92 Utilities.offsetPoints(points, 5f, 6f) 93 94 val expected = listOf(6f, 8f, 8f, 10f) 95 assertEquals(expected, points.toList()) 96 } 97 98 @Test testPointInViewnull99 fun testPointInView() { 100 val view = View(mContext) 101 view.layout(0, 0, 100, 100) 102 103 assertTrue(Utilities.pointInView(view, 50f, 50f, 0f)) // Inside view 104 assertFalse(Utilities.pointInView(view, -10f, -10f, 0f)) // Outside view 105 assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop 106 assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop 107 } 108 109 @Test testNumberBoundingnull110 fun testNumberBounding() { 111 assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f)) 112 assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f)) 113 assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f)) 114 115 assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L)) 116 assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L)) 117 assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L)) 118 119 assertEquals(345, Utilities.boundToRange(345, 2, 500)) 120 assertEquals(400, Utilities.boundToRange(345, 400, 500)) 121 assertEquals(300, Utilities.boundToRange(345, 2, 300)) 122 123 val random = Random(SEED) 124 for (i in 1..300) { 125 val value = random.nextFloat() 126 val lowerBound = random.nextFloat() 127 val higherBound = lowerBound + random.nextFloat() 128 129 assertEquals( 130 "Utilities.boundToRange doesn't match Kotlin coerceIn", 131 value.coerceIn(lowerBound, higherBound), 132 Utilities.boundToRange(value, lowerBound, higherBound) 133 ) 134 assertEquals( 135 "Utilities.boundToRange doesn't match Kotlin coerceIn", 136 value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()), 137 Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt()) 138 ) 139 assertEquals( 140 "Utilities.boundToRange doesn't match Kotlin coerceIn", 141 value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()), 142 Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong()) 143 ) 144 assertEquals( 145 "If the lower bound is higher than lower bound, it should return the lower bound", 146 higherBound, 147 Utilities.boundToRange(value, higherBound, lowerBound) 148 ) 149 } 150 } 151 152 @Test testTranslateOverlappingViewnull153 fun testTranslateOverlappingView() { 154 testConcentricOverlap() 155 leftDownCornerOverlap() 156 noOverlap() 157 } 158 159 /* 160 Test Case: Rectangle Contained Within Another Rectangle 161 162 +-------------+ <-- exclusionBounds 163 | | 164 | +-----+ | 165 | | | | <-- targetViewBounds 166 | | | | 167 | +-----+ | 168 | | 169 +-------------+ 170 */ testConcentricOverlapnull171 private fun testConcentricOverlap() { 172 val targetView = View(ContextWrapper(getApplicationContext())) 173 val targetViewBounds = Rect(40, 40, 60, 60) 174 val inclusionBounds = Rect(0, 0, 100, 100) 175 val exclusionBounds = Rect(30, 30, 70, 70) 176 177 Utilities.translateOverlappingView( 178 targetView, 179 targetViewBounds, 180 inclusionBounds, 181 exclusionBounds, 182 Utilities.TRANSLATE_RIGHT 183 ) 184 assertEquals(30f, targetView.translationX) 185 Utilities.translateOverlappingView( 186 targetView, 187 targetViewBounds, 188 inclusionBounds, 189 exclusionBounds, 190 Utilities.TRANSLATE_LEFT 191 ) 192 assertEquals(-30f, targetView.translationX) 193 Utilities.translateOverlappingView( 194 targetView, 195 targetViewBounds, 196 inclusionBounds, 197 exclusionBounds, 198 Utilities.TRANSLATE_DOWN 199 ) 200 assertEquals(30f, targetView.translationY) 201 Utilities.translateOverlappingView( 202 targetView, 203 targetViewBounds, 204 inclusionBounds, 205 exclusionBounds, 206 Utilities.TRANSLATE_UP 207 ) 208 assertEquals(-30f, targetView.translationY) 209 } 210 211 /* 212 Test Case: Non-Overlapping Rectangles 213 214 +-----------------+ <-- targetViewBounds 215 | | 216 | | 217 +-----------------+ 218 219 +-----------+ <-- exclusionBounds 220 | | 221 | | 222 +-----------+ 223 */ noOverlapnull224 private fun noOverlap() { 225 val targetView = View(ContextWrapper(getApplicationContext())) 226 val targetViewBounds = Rect(10, 10, 20, 20) 227 228 val inclusionBounds = Rect(0, 0, 100, 100) 229 val exclusionBounds = Rect(30, 30, 40, 40) 230 231 Utilities.translateOverlappingView( 232 targetView, 233 targetViewBounds, 234 inclusionBounds, 235 exclusionBounds, 236 Utilities.TRANSLATE_RIGHT 237 ) 238 assertEquals(0f, targetView.translationX) 239 Utilities.translateOverlappingView( 240 targetView, 241 targetViewBounds, 242 inclusionBounds, 243 exclusionBounds, 244 Utilities.TRANSLATE_LEFT 245 ) 246 assertEquals(0f, targetView.translationX) 247 Utilities.translateOverlappingView( 248 targetView, 249 targetViewBounds, 250 inclusionBounds, 251 exclusionBounds, 252 Utilities.TRANSLATE_DOWN 253 ) 254 assertEquals(0f, targetView.translationY) 255 Utilities.translateOverlappingView( 256 targetView, 257 targetViewBounds, 258 inclusionBounds, 259 exclusionBounds, 260 Utilities.TRANSLATE_UP 261 ) 262 assertEquals(0f, targetView.translationY) 263 } 264 265 /* 266 Test Case: Rectangles Overlapping at Corners 267 268 +------------+ <-- exclusionBounds 269 | | 270 +-------+ | 271 | | | | <-- targetViewBounds 272 | +------------+ 273 | | 274 +-------+ 275 */ leftDownCornerOverlapnull276 private fun leftDownCornerOverlap() { 277 val targetView = View(ContextWrapper(getApplicationContext())) 278 val targetViewBounds = Rect(20, 20, 30, 30) 279 280 val inclusionBounds = Rect(0, 0, 100, 100) 281 val exclusionBounds = Rect(25, 25, 35, 35) 282 283 Utilities.translateOverlappingView( 284 targetView, 285 targetViewBounds, 286 inclusionBounds, 287 exclusionBounds, 288 Utilities.TRANSLATE_RIGHT 289 ) 290 assertEquals(15f, targetView.translationX) 291 Utilities.translateOverlappingView( 292 targetView, 293 targetViewBounds, 294 inclusionBounds, 295 exclusionBounds, 296 Utilities.TRANSLATE_LEFT 297 ) 298 assertEquals(-5f, targetView.translationX) 299 Utilities.translateOverlappingView( 300 targetView, 301 targetViewBounds, 302 inclusionBounds, 303 exclusionBounds, 304 Utilities.TRANSLATE_DOWN 305 ) 306 assertEquals(15f, targetView.translationY) 307 Utilities.translateOverlappingView( 308 targetView, 309 targetViewBounds, 310 inclusionBounds, 311 exclusionBounds, 312 Utilities.TRANSLATE_UP 313 ) 314 assertEquals(-5f, targetView.translationY) 315 } 316 317 @Test trimnull318 fun trim() { 319 val expectedString = "Hello World" 320 assertEquals(expectedString, Utilities.trim("Hello World ")) 321 // Basic trimming 322 assertEquals(expectedString, Utilities.trim(" Hello World ")) 323 assertEquals(expectedString, Utilities.trim(" Hello World")) 324 325 // Non-breaking whitespace 326 assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0")) 327 328 // Whitespace combinations 329 assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r")) 330 assertEquals(expectedString, Utilities.trim("\nHello World ")) 331 332 // Null input 333 assertEquals("", Utilities.trim(null)) 334 335 // Empty String 336 assertEquals("", Utilities.trim("")) 337 } 338 339 @Test getProgressnull340 fun getProgress() { 341 // Basic test 342 assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f) 343 344 // Negative values 345 assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f) 346 347 // Outside of range 348 assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f) 349 } 350 351 @Test scaleRectFAboutPivotnull352 fun scaleRectFAboutPivot() { 353 // Enlarge 354 var rectF = RectF(10f, 20f, 50f, 80f) 355 Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f) 356 assertEquals(RectF(0f, 5f, 60f, 95f), rectF) 357 358 // Shrink 359 rectF = RectF(10f, 20f, 50f, 80f) 360 Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f) 361 assertEquals(RectF(20f, 35f, 40f, 65f), rectF) 362 363 // No scale 364 rectF = RectF(10f, 20f, 50f, 80f) 365 Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f) 366 assertEquals(RectF(10f, 20f, 50f, 80f), rectF) 367 } 368 369 @Test rotateBoundsnull370 fun rotateBounds() { 371 var rect = Rect(20, 70, 60, 80) 372 Utilities.rotateBounds(rect, 100, 100, 0) 373 assertEquals(Rect(20, 70, 60, 80), rect) 374 375 rect = Rect(20, 70, 60, 80) 376 Utilities.rotateBounds(rect, 100, 100, 1) 377 assertEquals(Rect(70, 40, 80, 80), rect) 378 379 // case removed for b/28435189 380 // rect = Rect(20, 70, 60, 80) 381 // Utilities.rotateBounds(rect, 100, 100, 2) 382 // assertEquals(Rect(40, 20, 80, 30), rect) 383 384 rect = Rect(20, 70, 60, 80) 385 Utilities.rotateBounds(rect, 100, 100, 3) 386 assertEquals(Rect(20, 20, 30, 60), rect) 387 } 388 } 389