1 /*
<lambda>null2  * Copyright (C) 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 package android.app.cts
17 
18 import android.R
19 import android.app.stubs.shared.NotificationHostActivity
20 import android.content.Context
21 import android.content.Intent
22 import android.graphics.Bitmap
23 import android.graphics.Color
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.ImageView
27 import android.widget.RemoteViews
28 import android.widget.TextView
29 import androidx.annotation.BoolRes
30 import androidx.annotation.DimenRes
31 import androidx.annotation.IdRes
32 import androidx.annotation.StringRes
33 import androidx.lifecycle.Lifecycle
34 import androidx.test.core.app.ActivityScenario
35 import androidx.test.platform.app.InstrumentationRegistry
36 import kotlin.reflect.KClass
37 import org.junit.Before
38 
39 open class NotificationTemplateTestBase {
40 
41     // Used to give time to visually inspect or attach a debugger before the checkViews block
42     protected var waitBeforeCheckingViews: Long = 0
43     protected var context: Context =
44             InstrumentationRegistry.getInstrumentation().getTargetContext()
45 
46     @Before
47     public fun baseSetUp() {
48         CtsAppTestUtils.turnScreenOn(InstrumentationRegistry.getInstrumentation(), context)
49     }
50 
51     protected fun checkIconView(views: RemoteViews, iconCheck: (ImageView) -> Unit) {
52         checkViews(views) {
53             iconCheck(requireViewByIdName("right_icon"))
54         }
55     }
56 
57     protected fun checkViews(
58         views: RemoteViews,
59         @DimenRes heightDimen: Int? = null,
60         checker: NotificationHostActivity.() -> Unit
61     ) {
62         val activityIntent = Intent(context, NotificationHostActivity::class.java)
63         activityIntent.putExtra(NotificationHostActivity.EXTRA_REMOTE_VIEWS, views)
64         heightDimen?.also {
65             activityIntent.putExtra(
66                 NotificationHostActivity.EXTRA_HEIGHT,
67                     context.resources.getDimensionPixelSize(it)
68             )
69         }
70         ActivityScenario.launch<NotificationHostActivity>(activityIntent).use { scenario ->
71             scenario.moveToState(Lifecycle.State.RESUMED)
72             if (waitBeforeCheckingViews > 0) {
73                 Thread.sleep(waitBeforeCheckingViews)
74             }
75             scenario.onActivity { activity ->
76                 activity.checker()
77             }
78         }
79     }
80 
81     protected fun createBitmap(width: Int, height: Int): Bitmap =
82             Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
83                 // IMPORTANT: Pass current DisplayMetrics when creating a Bitmap, so that it
84                 // receives the correct density. Otherwise, the Bitmap may get the default density
85                 // (DisplayMetrics.DENSITY_DEVICE), which in some cases (eg. for apps running in
86                 // compat mode) may be different from the actual density the app is rendered with.
87                 // This would lead to the Bitmap eventually being rendered with different sizes,
88                 // than the ones passed here.
89                 density = context.resources.displayMetrics.densityDpi
90 
91                 eraseColor(Color.GRAY)
92             }
93 
94     protected fun makeCustomContent(): RemoteViews {
95         val customContent = RemoteViews(context.packageName, R.layout.simple_list_item_1)
96         val textId = getAndroidRId("text1")
97         customContent.setTextViewText(textId, "Example Text")
98         return customContent
99     }
100 
101     protected fun <T : View> NotificationHostActivity.requireViewByIdName(idName: String): T {
102         val viewId = getAndroidRId(idName)
103         return notificationRoot.findViewById<T>(viewId)
104                 ?: throw NullPointerException("No view with id: android.R.id.$idName ($viewId)")
105     }
106 
107     protected fun <T : View> NotificationHostActivity.findViewByIdName(idName: String): T? =
108             notificationRoot.findViewById<T>(getAndroidRId(idName))
109 
110     /** [Sequence] that yields all of the direct children of this [ViewGroup] */
111     private val ViewGroup.children
112         get() = sequence { for (i in 0 until childCount) yield(getChildAt(i)) }
113 
114     private fun <T : View> collectViews(
115         view: View,
116         type: KClass<T>,
117         mutableList: MutableList<T>,
118         requireVisible: Boolean = true,
119         predicate: (T) -> Boolean
120     ) {
121         if (requireVisible && view.visibility != View.VISIBLE) {
122             return
123         }
124         if (type.java.isInstance(view)) {
125             if (predicate(view as T)) {
126                 mutableList.add(view)
127             }
128         }
129         if (view is ViewGroup) {
130             for (child in view.children) {
131                 collectViews(child, type, mutableList, requireVisible, predicate)
132             }
133         }
134     }
135 
136     protected fun NotificationHostActivity.requireViewWithText(text: String): TextView =
137             findViewWithText(text) ?: throw RuntimeException("Unable to find view with text: $text")
138 
139     protected fun NotificationHostActivity.requireViewWithTextContaining(
140         substring: String
141     ): TextView =
142         findViewWithTextContaining(substring)
143             ?: throw RuntimeException("Unable to find view with text containing: $substring")
144 
145     protected fun NotificationHostActivity.findViewWithText(text: String): TextView? {
146         val views: MutableList<TextView> = ArrayList()
147         collectViews(notificationRoot, TextView::class, views) { it.text?.toString() == text }
148         when (views.size) {
149             0 -> return null
150             1 -> return views[0]
151             else -> throw RuntimeException("Found multiple views with text: $text")
152         }
153     }
154 
155     protected fun NotificationHostActivity.findViewWithTextContaining(
156         substring: String
157     ): TextView? {
158         val views: MutableList<TextView> = ArrayList()
159         collectViews(notificationRoot, TextView::class, views) {
160             (it.text?.toString() ?: "").contains(substring)
161         }
162         when (views.size) {
163             0 -> return null
164             1 -> return views[0]
165             else -> throw RuntimeException("Found multiple views with text containing: $substring")
166         }
167     }
168 
169     private fun getAndroidRes(resType: String, resName: String): Int =
170             context.resources.getIdentifier(resName, resType, "android")
171 
172     @IdRes
173     protected fun getAndroidRId(idName: String): Int = getAndroidRes("id", idName)
174 
175     @StringRes
176     protected fun getAndroidRString(stringName: String): Int = getAndroidRes("string", stringName)
177 
178     @BoolRes
179     protected fun getAndroidRBool(boolName: String): Int = getAndroidRes("bool", boolName)
180 
181     @DimenRes
182     protected fun getAndroidRDimen(dimenName: String): Int = getAndroidRes("dimen", dimenName)
183 }
184