1 /*
<lambda>null2 * Copyright 2021 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 androidx.glance.appwidget
18
19 import android.appwidget.AppWidgetManager
20 import android.content.Context
21 import android.os.Build
22 import android.os.Bundle
23 import android.util.Log
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.ListView
27 import android.widget.RelativeLayout
28 import android.widget.TextView
29 import androidx.compose.ui.unit.DpSize
30 import androidx.compose.ui.unit.max
31 import androidx.compose.ui.unit.min
32 import androidx.core.view.children
33 import androidx.glance.appwidget.test.R
34 import androidx.test.core.app.ApplicationProvider
35 import androidx.test.platform.app.InstrumentationRegistry
36 import com.google.common.truth.Truth.assertThat
37 import java.io.FileInputStream
38 import java.util.concurrent.atomic.AtomicReference
39 import kotlin.test.assertIs
40
41 // remote_views_adapter_default_loading_view.xml is used for adapter based views.
42 object DefaultLoadingViewConstants {
43 const val id = "default_loading_view"
44 const val resource_package = "android"
45 }
46
47 private const val TAG = "AndroidTestUtils"
48
findChildnull49 inline fun <reified T : View> View.findChild(noinline pred: (T) -> Boolean) =
50 findChild(pred, T::class.java)
51
52 inline fun <reified T : View> View.findChildByType() = findChild({ true }, T::class.java)
53
toArrayListnull54 internal inline fun <reified T> Collection<T>.toArrayList() = ArrayList<T>(this)
55
56 fun <T : View> View.findChild(predicate: (T) -> Boolean, klass: Class<T>): T? {
57 try {
58 val castView = klass.cast(this)!!
59 if (predicate(castView)) {
60 return castView
61 }
62 } catch (e: ClassCastException) {
63 // Nothing to do
64 }
65 if (this !is ViewGroup) {
66 return null
67 }
68 return children.mapNotNull { it.findChild(predicate, klass) }.firstOrNull()
69 }
70
optionsBundleOfnull71 fun optionsBundleOf(sizes: List<DpSize>): Bundle {
72 require(sizes.isNotEmpty()) { "There must be at least one size" }
73 val (minSize, maxSize) =
74 sizes.fold(sizes[0] to sizes[0]) { acc, s ->
75 DpSize(min(acc.first.width, s.width), min(acc.first.height, s.height)) to
76 DpSize(max(acc.second.width, s.width), max(acc.second.height, s.height))
77 }
78 return Bundle().apply {
79 putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minSize.width.value.toInt())
80 putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minSize.height.value.toInt())
81 putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxSize.width.value.toInt())
82 putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxSize.height.value.toInt())
83 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
84 val sizeList = sizes.map { it.toSizeF() }.toArrayList()
85 putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizeList)
86 }
87 }
88 }
89
90 /** Run a command and retrieve the output as a string. */
runShellCommandnull91 fun runShellCommand(command: String): String {
92 return InstrumentationRegistry.getInstrumentation()
93 .uiAutomation
94 .executeShellCommand(command)
95 .use { FileInputStream(it.fileDescriptor).reader().readText() }
96 }
97
98 val context: Context
99 get() = ApplicationProvider.getApplicationContext()
100
101 /** Count the number of children that are not gone. */
102 val ViewGroup.notGoneChildCount: Int
<lambda>null103 get() = children.count { it.visibility != View.GONE }
104
105 /** Iterate over children that are not gone. */
106 val ViewGroup.notGoneChildren: Sequence<View>
<lambda>null107 get() = children.filter { it.visibility != View.GONE }
108
109 // Extract the target view if it is a complex view in Android R-.
getTargetViewnull110 inline fun <reified T : View> View.getTargetView(): T {
111 if ((tag as? String) != "glanceComplexLayout") {
112 return assertIs(this)
113 }
114 val layout = assertIs<RelativeLayout>(this)
115 return assertIs(
116 when (layout.childCount) {
117 1 -> layout.getChildAt(0)
118 2 -> layout.getChildAt(1)
119 else -> throw IllegalStateException("Unknown complex layout with more than 2 elements.")
120 }
121 )
122 }
123
124 // Get the parent view, even if the current view is in a complex layout.
getParentViewnull125 inline fun <reified T : View> View.getParentView(): T {
126 val parent = assertIs<ViewGroup>(this.parent)
127 if ((parent.tag as? String) != "glanceComplexLayout") {
128 return assertIs(parent)
129 }
130 return assertIs(parent.parent)
131 }
132
133 // Perform a click on the root layout of a compound button. In our tests we often identify a
134 // compound button by its TextView, but we should perform the click on the root view of a compound
135 // button layout (parent of the TextView). On both S+ (standard compound button views) and R-
136 // (backported views) we tag the root view with "glanceCompoundButton", so we can use that to find
137 // the right view to click on.
Viewnull138 fun View.performCompoundButtonClick() {
139 if (tag == "glanceCompoundButton") {
140 this
141 } else {
142 assertIs<View>(this.parent).also {
143 assertThat(it.tag).isEqualTo("glanceCompoundButton")
144 }
145 }
146 .performClick()
147 }
148
149 /** Returns true if view / subviews are still showing loading views. */
isLoadingnull150 fun View.isLoading(): Boolean {
151 // If this method was called on a top-level view, then it can match initial loading view set in
152 // app provider info and even if the loading layout structure changes to coincidentally match
153 // with view we are expecting, we will still be able to identify that views have not loaded.
154 val emptyLoadingViewID = R.layout.empty_layout
155 val defaultLoadingViewId =
156 context.resources.getIdentifier(
157 DefaultLoadingViewConstants.id,
158 "id",
159 DefaultLoadingViewConstants.resource_package
160 )
161 return findViewById<View>(defaultLoadingViewId) != null ||
162 findViewById<View>(emptyLoadingViewID) != null
163 }
164
ListViewnull165 fun ListView.isItemLoaded(text: String): Boolean {
166 if (childCount > 0 && adapter != null) {
167 return children.any {
168 val matches = arrayListOf<View>()
169 it.findViewsWithText(matches, text, View.FIND_VIEWS_WITH_TEXT)
170 matches.isNotEmpty()
171 }
172 }
173 return false
174 }
175
176 /** Returns true if list items are fully loaded (i.e. not in loading... state). */
ListViewnull177 fun ListView.areItemsFullyLoaded(): Boolean {
178 val loadingViewId =
179 context.resources.getIdentifier(
180 DefaultLoadingViewConstants.id,
181 "id",
182 DefaultLoadingViewConstants.resource_package
183 )
184 if (childCount > 0 && adapter != null) {
185 // Searching directly on listView doesn't seem to return matching items, so we search each
186 // item.
187 return children.any { it.findViewById<TextView>(loadingViewId) != null }.not()
188 }
189 return false
190 }
191
192 // Update the value of the AtomicReference using the given updater function. Will throw an error
193 // if unable to successfully set the value.
updatenull194 fun <T> AtomicReference<T>.update(updater: (T) -> T) {
195 repeat(100) { get().let { if (compareAndSet(it, updater(it))) return } }
196 error("Could not update the AtomicReference")
197 }
198
199 /**
200 * Print the view hierarchy from the current view to the log.
201 *
202 * @param tag Log tag to use for logging
203 * @param parent view to start the hierarchy print from
204 * @param indent to use for the log messages
205 */
logViewHierarchynull206 fun logViewHierarchy(tag: String, parent: ViewGroup, indent: String) {
207 for (child in parent.children) {
208 var childString = child.toString()
209 if (child is TextView) {
210 childString = "$childString '${child.text}'"
211 }
212 Log.e(tag, "$indent|- $childString")
213 if (child is ViewGroup) {
214 logViewHierarchy(tag, child, "$indent ")
215 }
216 }
217 }
218
waitForBroadcastIdlenull219 fun waitForBroadcastIdle(timeoutSeconds: Int = 5) {
220 // Default timeout set per observation with FTL devices in b/283484546
221 val cmd: String =
222 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
223 // wait for pending broadcasts until this point to be completed for UDC+
224 "am wait-for-broadcast-barrier"
225 } else {
226 // wait for broadcast queues to be idle. This is less preferred approach as it can
227 // technically take forever.
228 "am wait-for-broadcast-idle"
229 }
230 Log.i(TAG, runShellCommand("timeout $timeoutSeconds $cmd"))
231 }
232