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