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.Intent
21 import android.graphics.Bitmap
22 import android.graphics.Color
23 import android.test.AndroidTestCase
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 kotlin.reflect.KClass
36 
37 open class NotificationTemplateTestBase : AndroidTestCase() {
38 
39     // Used to give time to visually inspect or attach a debugger before the checkViews block
40     protected var waitBeforeCheckingViews: Long = 0
41 
42     protected fun checkIconView(views: RemoteViews, iconCheck: (ImageView) -> Unit) {
43         checkViews(views) {
44             iconCheck(requireViewByIdName("right_icon"))
45         }
46     }
47 
48     protected fun checkViews(
49         views: RemoteViews,
50         @DimenRes heightDimen: Int? = null,
51         checker: NotificationHostActivity.() -> Unit
52     ) {
53         val activityIntent = Intent(context, NotificationHostActivity::class.java)
54         activityIntent.putExtra(NotificationHostActivity.EXTRA_REMOTE_VIEWS, views)
55         heightDimen?.also {
56             activityIntent.putExtra(NotificationHostActivity.EXTRA_HEIGHT,
57                     context.resources.getDimensionPixelSize(it))
58         }
59         ActivityScenario.launch<NotificationHostActivity>(activityIntent).use { scenario ->
60             scenario.moveToState(Lifecycle.State.RESUMED)
61             if (waitBeforeCheckingViews > 0) {
62                 Thread.sleep(waitBeforeCheckingViews)
63             }
64             scenario.onActivity { activity ->
65                 activity.checker()
66             }
67         }
68     }
69 
70     protected fun createBitmap(width: Int, height: Int): Bitmap =
71             Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
72                 it.eraseColor(Color.GRAY)
73             }
74 
75     protected fun makeCustomContent(): RemoteViews {
76         val customContent = RemoteViews(mContext.packageName, R.layout.simple_list_item_1)
77         val textId = getAndroidRId("text1")
78         customContent.setTextViewText(textId, "Example Text")
79         return customContent
80     }
81 
82     protected fun <T : View> NotificationHostActivity.requireViewByIdName(idName: String): T {
83         val viewId = getAndroidRId(idName)
84         return notificationRoot.findViewById<T>(viewId)
85                 ?: throw NullPointerException("No view with id: android.R.id.$idName ($viewId)")
86     }
87 
88     protected fun <T : View> NotificationHostActivity.findViewByIdName(idName: String): T? =
89             notificationRoot.findViewById<T>(getAndroidRId(idName))
90 
91     /** [Sequence] that yields all of the direct children of this [ViewGroup] */
92     private val ViewGroup.children
93         get() = sequence { for (i in 0 until childCount) yield(getChildAt(i)) }
94 
95     private fun <T : View> collectViews(
96         view: View,
97         type: KClass<T>,
98         mutableList: MutableList<T>,
99         requireVisible: Boolean = true,
100         predicate: (T) -> Boolean
101     ) {
102         if (requireVisible && view.visibility != View.VISIBLE) {
103             return
104         }
105         if (type.java.isInstance(view)) {
106             if (predicate(view as T)) {
107                 mutableList.add(view)
108             }
109         }
110         if (view is ViewGroup) {
111             for (child in view.children) {
112                 collectViews(child, type, mutableList, requireVisible, predicate)
113             }
114         }
115     }
116 
117     protected fun NotificationHostActivity.requireViewWithText(text: String): TextView =
118             findViewWithText(text) ?: throw RuntimeException("Unable to find view with text: $text")
119 
120     protected fun NotificationHostActivity.findViewWithText(text: String): TextView? {
121         val views: MutableList<TextView> = ArrayList()
122         collectViews(notificationRoot, TextView::class, views) { it.text?.toString() == text }
123         when (views.size) {
124             0 -> return null
125             1 -> return views[0]
126             else -> throw RuntimeException("Found multiple views with text: $text")
127         }
128     }
129 
130     private fun getAndroidRes(resType: String, resName: String): Int =
131             mContext.resources.getIdentifier(resName, resType, "android")
132 
133     @IdRes
134     protected fun getAndroidRId(idName: String): Int = getAndroidRes("id", idName)
135 
136     @StringRes
137     protected fun getAndroidRString(stringName: String): Int = getAndroidRes("string", stringName)
138 
139     @BoolRes
140     protected fun getAndroidRBool(boolName: String): Int = getAndroidRes("bool", boolName)
141 }