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 android.platform.systemui_tapl.ui 18 19 import android.graphics.Point 20 import android.os.SystemClock 21 import android.platform.helpers.CommonUtils 22 import android.platform.systemui_tapl.utils.DeviceUtils.launcherResSelector 23 import android.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector 24 import android.platform.uiautomatorhelpers.BetterSwipe 25 import android.platform.uiautomatorhelpers.DeviceHelpers.context 26 import android.platform.uiautomatorhelpers.DeviceHelpers.hasObject 27 import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice 28 import android.platform.uiautomatorhelpers.DeviceHelpers.waitForPossibleEmpty 29 import android.platform.uiautomatorhelpers.PRECISE_GESTURE_INTERPOLATOR 30 import android.view.WindowInsets 31 import android.view.WindowManager 32 import androidx.test.uiautomator.UiObject2 33 import com.android.wm.shell.Flags 34 import com.google.common.truth.Truth.assertWithMessage 35 import java.time.Duration 36 import java.time.temporal.ChronoUnit 37 38 /** 39 * System UI test automation object representing a notification bubble, specifically the view 40 * representing the bubble, shown when the stack is collapsed or expanded. 41 */ 42 class Bubble internal constructor(private val bubbleView: UiObject2) { 43 /** Expands the bubble into the stack. */ expandnull44 fun expand(): ExpandedBubbleStack { 45 bubbleView.click() 46 // check if bubble stack education is visible 47 // education visibility can be checked only after interacting with the bubble (click) 48 // it might be invoked by user interaction, if it wasn't presented yet 49 if (isEducationVisible) { 50 // click bubble again to expand 51 // if education is visible, the previous interaction didn't expand the bubble stack 52 bubbleView.click() 53 } 54 // let the stack expand animation to finish 55 SystemClock.sleep(STACK_EXPAND_TIMEOUT.toMillis()) 56 uiDevice.waitForIdle() 57 return ExpandedBubbleStack().apply { 58 // dismiss manage education if it's visible 59 dismissManageEducation() 60 } 61 } 62 63 /** Clicks on the bubble. */ clicknull64 fun click() { 65 bubbleView.click() 66 } 67 68 /** Returns the content description of the bubble. */ contentDescriptionnull69 fun contentDescription(): String = bubbleView.contentDescription 70 71 /** Dismisses the bubble by dragging it to the Dismiss target. */ 72 fun dismiss() { 73 dragBubbleToDismiss() 74 // check if bubble stack education is visible and blocked interaction 75 // education visibility can be checked only after interacting with the bubble (swipe) 76 // it might be invoked by user interaction, if it wasn't presented yet 77 if (isEducationVisible) { 78 // retry drag interaction 79 // if education is visible, the previous interaction was blocked and didn't drag bubble 80 dragBubbleToDismiss() 81 } 82 } 83 84 /** Returns the flyout if it's visible, or fails. */ 85 val flyout: BubbleFlyout 86 get() = BubbleFlyout() 87 88 /** Is Welcome education visible and stopped bubbles expand */ 89 private val isEducationVisible: Boolean 90 get() = hasObject(BUBBLE_STACK_EDUCATION) 91 92 /** Drag bubble to Dismiss target */ dragBubbleToDismissnull93 private fun dragBubbleToDismiss() { 94 val windowMetrics = 95 context.getSystemService(WindowManager::class.java)!!.currentWindowMetrics 96 val insets = 97 windowMetrics.windowInsets.getInsetsIgnoringVisibility( 98 WindowInsets.Type.mandatorySystemGestures() or 99 WindowInsets.Type.navigationBars() or 100 WindowInsets.Type.displayCutout() 101 ) 102 val destination = 103 Point(windowMetrics.bounds.width() / 2, (windowMetrics.bounds.height() - insets.bottom)) 104 BetterSwipe.swipe( 105 bubbleView.visibleCenter, 106 destination, 107 duration = Duration.of(700, ChronoUnit.MILLIS), 108 interpolator = PRECISE_GESTURE_INTERPOLATOR, 109 ) 110 } 111 equalsnull112 override fun equals(other: Any?): Boolean { 113 if (this === other) return true 114 if (other !is Bubble) return false 115 // Bubbles are equal if the backing bubbleView is the same. 116 // This ensures that Bubble objects can be used as targets for waitForValueToSettle. 117 return this.bubbleView == other.bubbleView 118 } 119 120 companion object { 121 val FIND_OBJECT_TIMEOUT = Duration.ofSeconds(20) 122 val BUBBLE_VIEW = sysuiResSelector("bubble_view") 123 val BUBBLE_BAR_VIEWS = launcherResSelector("bubble_view") 124 private val STACK_EXPAND_TIMEOUT = Duration.ofSeconds(1) 125 private val BUBBLE_STACK_EDUCATION = sysuiResSelector("stack_education_layout") 126 127 @JvmStatic 128 internal val bubbleViews: List<UiObject2> 129 get() { 130 val bubbleViews = 131 if (CommonUtils.isLargeScreen() && Flags.enableBubbleBar()) { 132 // Check bubble bar first if we're large screen 133 waitForPossibleEmpty(BUBBLE_BAR_VIEWS, timeout = FIND_OBJECT_TIMEOUT) <lambda>null134 .ifEmpty { 135 // Check floating in case bubble bar wasn't active 136 waitForPossibleEmpty(BUBBLE_VIEW, timeout = FIND_OBJECT_TIMEOUT) 137 } 138 } else { 139 waitForPossibleEmpty(BUBBLE_VIEW, timeout = FIND_OBJECT_TIMEOUT) 140 } 141 assertWithMessage("No bubbles visible").that(bubbleViews.size).isAtLeast(1) 142 return bubbleViews 143 } 144 } 145 } 146