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