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).apply { 72 // IMPORTANT: Pass current DisplayMetrics when creating a Bitmap, so that it 73 // receives the correct density. Otherwise, the Bitmap may get the default density 74 // (DisplayMetrics.DENSITY_DEVICE), which in some cases (eg. for apps running in 75 // compat mode) may be different from the actual density the app is rendered with. 76 // This would lead to the Bitmap eventually being rendered with different sizes, 77 // than the ones passed here. 78 density = context.resources.displayMetrics.densityDpi 79 80 eraseColor(Color.GRAY) 81 } 82 83 protected fun makeCustomContent(): RemoteViews { 84 val customContent = RemoteViews(mContext.packageName, R.layout.simple_list_item_1) 85 val textId = getAndroidRId("text1") 86 customContent.setTextViewText(textId, "Example Text") 87 return customContent 88 } 89 90 protected fun <T : View> NotificationHostActivity.requireViewByIdName(idName: String): T { 91 val viewId = getAndroidRId(idName) 92 return notificationRoot.findViewById<T>(viewId) 93 ?: throw NullPointerException("No view with id: android.R.id.$idName ($viewId)") 94 } 95 96 protected fun <T : View> NotificationHostActivity.findViewByIdName(idName: String): T? = 97 notificationRoot.findViewById<T>(getAndroidRId(idName)) 98 99 /** [Sequence] that yields all of the direct children of this [ViewGroup] */ 100 private val ViewGroup.children 101 get() = sequence { for (i in 0 until childCount) yield(getChildAt(i)) } 102 103 private fun <T : View> collectViews( 104 view: View, 105 type: KClass<T>, 106 mutableList: MutableList<T>, 107 requireVisible: Boolean = true, 108 predicate: (T) -> Boolean 109 ) { 110 if (requireVisible && view.visibility != View.VISIBLE) { 111 return 112 } 113 if (type.java.isInstance(view)) { 114 if (predicate(view as T)) { 115 mutableList.add(view) 116 } 117 } 118 if (view is ViewGroup) { 119 for (child in view.children) { 120 collectViews(child, type, mutableList, requireVisible, predicate) 121 } 122 } 123 } 124 125 protected fun NotificationHostActivity.requireViewWithText(text: String): TextView = 126 findViewWithText(text) ?: throw RuntimeException("Unable to find view with text: $text") 127 128 protected fun NotificationHostActivity.findViewWithText(text: String): TextView? { 129 val views: MutableList<TextView> = ArrayList() 130 collectViews(notificationRoot, TextView::class, views) { it.text?.toString() == text } 131 when (views.size) { 132 0 -> return null 133 1 -> return views[0] 134 else -> throw RuntimeException("Found multiple views with text: $text") 135 } 136 } 137 138 private fun getAndroidRes(resType: String, resName: String): Int = 139 mContext.resources.getIdentifier(resName, resType, "android") 140 141 @IdRes 142 protected fun getAndroidRId(idName: String): Int = getAndroidRes("id", idName) 143 144 @StringRes 145 protected fun getAndroidRString(stringName: String): Int = getAndroidRes("string", stringName) 146 147 @BoolRes 148 protected fun getAndroidRBool(boolName: String): Int = getAndroidRes("bool", boolName) 149 150 @DimenRes 151 protected fun getAndroidRDimen(dimenName: String) : Int = getAndroidRes("dimen", dimenName) 152 }