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