1 /* <lambda>null2 * 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.wm.shell.bubbles.bar 18 19 import android.animation.AnimatorTestRule 20 import android.app.Activity 21 import android.app.ActivityManager 22 import android.content.Context 23 import android.graphics.Insets 24 import android.graphics.Outline 25 import android.graphics.Rect 26 import android.os.Bundle 27 import android.view.View 28 import android.view.ViewGroup 29 import android.view.WindowManager 30 import android.widget.FrameLayout 31 import android.widget.FrameLayout.LayoutParams 32 import androidx.test.core.app.ActivityScenario 33 import androidx.test.core.app.ApplicationProvider 34 import androidx.test.ext.junit.runners.AndroidJUnit4 35 import androidx.test.filters.SmallTest 36 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 37 import com.android.internal.logging.testing.UiEventLoggerFake 38 import com.android.internal.protolog.ProtoLog 39 import com.android.wm.shell.bubbles.Bubble 40 import com.android.wm.shell.bubbles.BubbleExpandedViewManager 41 import com.android.wm.shell.bubbles.BubbleLogger 42 import com.android.wm.shell.bubbles.BubbleOverflow 43 import com.android.wm.shell.bubbles.BubblePositioner 44 import com.android.wm.shell.bubbles.BubbleTaskView 45 import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager 46 import com.android.wm.shell.bubbles.FakeBubbleFactory 47 import com.android.wm.shell.common.TestShellExecutor 48 import com.android.wm.shell.shared.bubbles.DeviceConfig 49 import com.android.wm.shell.taskview.TaskView 50 import com.android.wm.shell.taskview.TaskViewController 51 import com.android.wm.shell.taskview.TaskViewTaskController 52 import com.google.common.truth.Truth.assertThat 53 import java.util.concurrent.Semaphore 54 import java.util.concurrent.TimeUnit 55 import org.junit.After 56 import org.junit.Before 57 import org.junit.Rule 58 import org.junit.Test 59 import org.junit.runner.RunWith 60 import org.mockito.kotlin.any 61 import org.mockito.kotlin.clearInvocations 62 import org.mockito.kotlin.eq 63 import org.mockito.kotlin.mock 64 import org.mockito.kotlin.verify 65 import org.mockito.kotlin.whenever 66 67 /** Tests for [BubbleBarAnimationHelper] */ 68 @SmallTest 69 @RunWith(AndroidJUnit4::class) 70 class BubbleBarAnimationHelperTest { 71 72 @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) 73 private lateinit var activityScenario: ActivityScenario<TestActivity> 74 75 companion object { 76 const val SCREEN_WIDTH = 2000 77 const val SCREEN_HEIGHT = 1000 78 } 79 80 private val context = ApplicationProvider.getApplicationContext<Context>() 81 82 private lateinit var animationHelper: BubbleBarAnimationHelper 83 private lateinit var bubblePositioner: BubblePositioner 84 private lateinit var expandedViewManager: BubbleExpandedViewManager 85 private lateinit var bubbleLogger: BubbleLogger 86 private lateinit var mainExecutor: TestShellExecutor 87 private lateinit var bgExecutor: TestShellExecutor 88 private lateinit var container: FrameLayout 89 90 @Before 91 fun setUp() { 92 ProtoLog.REQUIRE_PROTOLOGTOOL = false 93 ProtoLog.init() 94 activityScenario = ActivityScenario.launch(TestActivity::class.java) 95 activityScenario.onActivity { activity -> container = activity.container } 96 val windowManager = context.getSystemService(WindowManager::class.java) 97 bubblePositioner = BubblePositioner(context, windowManager) 98 bubblePositioner.setShowingInBubbleBar(true) 99 val deviceConfig = 100 DeviceConfig( 101 windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), 102 isLargeScreen = true, 103 isSmallTablet = false, 104 isLandscape = true, 105 isRtl = false, 106 insets = Insets.of(10, 20, 30, 40), 107 ) 108 bubblePositioner.update(deviceConfig) 109 expandedViewManager = FakeBubbleExpandedViewManager() 110 bubbleLogger = BubbleLogger(UiEventLoggerFake()) 111 112 mainExecutor = TestShellExecutor() 113 bgExecutor = TestShellExecutor() 114 115 animationHelper = BubbleBarAnimationHelper(context, bubblePositioner) 116 } 117 118 @After 119 fun tearDown() { 120 bgExecutor.flushAll() 121 mainExecutor.flushAll() 122 } 123 124 @Test 125 fun animateSwitch_bubbleToBubble_oldHiddenNewShown() { 126 val fromBubble = createBubble(key = "from").initialize(container) 127 val toBubble = createBubble(key = "to").initialize(container) 128 129 val semaphore = Semaphore(0) 130 val after = Runnable { semaphore.release() } 131 132 activityScenario.onActivity { 133 animationHelper.animateSwitch(fromBubble, toBubble, after) 134 animatorTestRule.advanceTimeBy(1000) 135 } 136 getInstrumentation().waitForIdleSync() 137 138 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 139 assertThat(fromBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE) 140 assertThat(fromBubble.bubbleBarExpandedView?.alpha).isEqualTo(0f) 141 assertThat(fromBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() 142 143 assertThat(toBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE) 144 assertThat(toBubble.bubbleBarExpandedView?.alpha).isEqualTo(1f) 145 assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() 146 } 147 148 @Test 149 fun animateSwitch_bubbleToBubble_handleColorTransferred() { 150 val fromBubble = createBubble(key = "from").initialize(container) 151 fromBubble.bubbleBarExpandedView!! 152 .handleView 153 .updateHandleColor(/* isRegionDark= */ true, /* animated= */ false) 154 val toBubble = createBubble(key = "to").initialize(container) 155 156 activityScenario.onActivity { 157 animationHelper.animateSwitch(fromBubble, toBubble, /* afterAnimation= */ null) 158 animatorTestRule.advanceTimeBy(1000) 159 } 160 getInstrumentation().waitForIdleSync() 161 162 assertThat(toBubble.bubbleBarExpandedView!!.handleView.handleColor) 163 .isEqualTo(fromBubble.bubbleBarExpandedView!!.handleView.handleColor) 164 } 165 166 @Test 167 fun animateSwitch_bubbleToBubble_updateTaskBounds() { 168 val fromBubble = createBubble("from").initialize(container) 169 val toBubbleTaskController = mock<TaskViewTaskController>() 170 val taskController = mock<TaskViewController>() 171 val toBubble = createBubble("to", taskController, toBubbleTaskController).initialize( 172 container) 173 174 activityScenario.onActivity { 175 animationHelper.animateSwitch(fromBubble, toBubble) {} 176 // Start the animation, but don't finish 177 animatorTestRule.advanceTimeBy(100) 178 } 179 getInstrumentation().waitForIdleSync() 180 // Clear invocations to ensure that bounds update happens after animation ends 181 clearInvocations(taskController) 182 getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } 183 getInstrumentation().waitForIdleSync() 184 185 verify(taskController).setTaskBounds(eq(toBubbleTaskController), any()) 186 } 187 188 @Test 189 fun animateSwitch_bubbleToOverflow_oldHiddenNewShown() { 190 val fromBubble = createBubble(key = "from").initialize(container) 191 val overflow = createOverflow().initialize(container) 192 193 val semaphore = Semaphore(0) 194 val after = Runnable { semaphore.release() } 195 196 activityScenario.onActivity { 197 animationHelper.animateSwitch(fromBubble, overflow, after) 198 animatorTestRule.advanceTimeBy(1000) 199 } 200 getInstrumentation().waitForIdleSync() 201 202 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 203 assertThat(fromBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE) 204 assertThat(fromBubble.bubbleBarExpandedView?.alpha).isEqualTo(0f) 205 assertThat(fromBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() 206 207 assertThat(overflow.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE) 208 assertThat(overflow.bubbleBarExpandedView?.alpha).isEqualTo(1f) 209 } 210 211 @Test 212 fun animateSwitch_overflowToBubble_oldHiddenNewShown() { 213 val overflow = createOverflow().initialize(container) 214 val toBubble = createBubble(key = "to").initialize(container) 215 216 val semaphore = Semaphore(0) 217 val after = Runnable { semaphore.release() } 218 219 activityScenario.onActivity { 220 animationHelper.animateSwitch(overflow, toBubble, after) 221 animatorTestRule.advanceTimeBy(1000) 222 } 223 getInstrumentation().waitForIdleSync() 224 225 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 226 assertThat(overflow.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE) 227 assertThat(overflow.bubbleBarExpandedView?.alpha).isEqualTo(0f) 228 229 assertThat(toBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE) 230 assertThat(toBubble.bubbleBarExpandedView?.alpha).isEqualTo(1f) 231 assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() 232 } 233 234 @Test 235 fun animateToRestPosition_updateTaskBounds() { 236 val taskView = mock<TaskViewTaskController>() 237 val controller = mock<TaskViewController>() 238 val bubble = createBubble("key", controller, taskView).initialize(container) 239 240 val semaphore = Semaphore(0) 241 val after = Runnable { semaphore.release() } 242 243 activityScenario.onActivity { 244 animationHelper.animateExpansion(bubble, after) 245 animatorTestRule.advanceTimeBy(1000) 246 } 247 getInstrumentation().waitForIdleSync() 248 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 249 250 getInstrumentation().runOnMainSync { 251 animationHelper.animateToRestPosition() 252 animatorTestRule.advanceTimeBy(100) 253 } 254 // Clear invocations to ensure that bounds update happens after animation ends 255 clearInvocations(controller) 256 getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } 257 getInstrumentation().waitForIdleSync() 258 259 verify(controller).setTaskBounds(eq(taskView), any()) 260 } 261 262 @Test 263 fun animateExpansion() { 264 val bubble = createBubble(key = "b1").initialize(container) 265 val bbev = bubble.bubbleBarExpandedView!! 266 267 val semaphore = Semaphore(0) 268 val after = Runnable { semaphore.release() } 269 270 activityScenario.onActivity { 271 bbev.onTaskCreated() 272 animationHelper.animateExpansion(bubble, after) 273 animatorTestRule.advanceTimeBy(1000) 274 } 275 getInstrumentation().waitForIdleSync() 276 277 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 278 assertThat(bbev.alpha).isEqualTo(1) 279 } 280 281 @Test 282 fun onImeTopChanged_noOverlap() { 283 val bubble = createBubble(key = "b1").initialize(container) 284 val bbev = bubble.bubbleBarExpandedView!! 285 286 val semaphore = Semaphore(0) 287 val after = Runnable { semaphore.release() } 288 289 activityScenario.onActivity { 290 bbev.onTaskCreated() 291 animationHelper.animateExpansion(bubble, after) 292 animatorTestRule.advanceTimeBy(1000) 293 } 294 getInstrumentation().waitForIdleSync() 295 296 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 297 298 activityScenario.onActivity { 299 // notify that the IME top coordinate is greater than the bottom of the expanded view. 300 // there's no overlap so it should not be clipped. 301 animationHelper.onImeTopChanged(bbev.contentBottomOnScreen * 2) 302 } 303 val outline = Outline() 304 bbev.outlineProvider.getOutline(bbev, outline) 305 assertThat(outline.mRect.bottom).isEqualTo(bbev.height) 306 } 307 308 @Test 309 fun onImeTopChanged_overlapsWithExpandedView() { 310 val bubble = createBubble(key = "b1").initialize(container) 311 val bbev = bubble.bubbleBarExpandedView!! 312 313 val semaphore = Semaphore(0) 314 val after = Runnable { semaphore.release() } 315 316 activityScenario.onActivity { 317 bbev.onTaskCreated() 318 animationHelper.animateExpansion(bubble, after) 319 animatorTestRule.advanceTimeBy(1000) 320 } 321 getInstrumentation().waitForIdleSync() 322 323 assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() 324 325 activityScenario.onActivity { 326 // notify that the IME top coordinate is less than the bottom of the expanded view, 327 // meaning it overlaps with it so we should be clipping the expanded view. 328 animationHelper.onImeTopChanged(bbev.contentBottomOnScreen - 10) 329 } 330 val outline = Outline() 331 bbev.outlineProvider.getOutline(bbev, outline) 332 assertThat(outline.mRect.bottom).isEqualTo(bbev.height - 10) 333 } 334 335 private fun createBubble( 336 key: String, 337 taskViewController: TaskViewController = mock<TaskViewController>(), 338 taskViewTaskController: TaskViewTaskController = mock<TaskViewTaskController>(), 339 ): Bubble { 340 val taskView = TaskView(context, taskViewController, taskViewTaskController) 341 val taskInfo = mock<ActivityManager.RunningTaskInfo>() 342 whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) 343 val bubbleTaskView = BubbleTaskView(taskView, mainExecutor) 344 345 val bubbleBarExpandedView = 346 FakeBubbleFactory.createExpandedView( 347 context, 348 bubblePositioner, 349 expandedViewManager, 350 bubbleTaskView, 351 mainExecutor, 352 bgExecutor, 353 bubbleLogger, 354 ) 355 val viewInfo = FakeBubbleFactory.createViewInfo(bubbleBarExpandedView) 356 return FakeBubbleFactory.createChatBubble(context, key, viewInfo) 357 } 358 359 private fun createOverflow(): BubbleOverflow { 360 val overflow = BubbleOverflow(context, bubblePositioner) 361 overflow.initializeForBubbleBar(expandedViewManager, bubblePositioner) 362 return overflow 363 } 364 365 private fun Bubble.initialize(container: ViewGroup): Bubble { 366 activityScenario.onActivity { container.addView(bubbleBarExpandedView) } 367 // Mark taskView's visible 368 bubbleBarExpandedView!!.onContentVisibilityChanged(true) 369 return this 370 } 371 372 private fun BubbleOverflow.initialize(container: ViewGroup): BubbleOverflow { 373 activityScenario.onActivity { container.addView(bubbleBarExpandedView) } 374 return this 375 } 376 377 class TestActivity : Activity() { 378 lateinit var container: FrameLayout 379 override fun onCreate(savedInstanceState: Bundle?) { 380 super.onCreate(savedInstanceState) 381 container = FrameLayout(applicationContext) 382 container.layoutParams = LayoutParams(50, 50) 383 setContentView(container) 384 } 385 } 386 } 387