1 /* <lambda>null2 * Copyright 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 17 package androidx.core.widget 18 19 import android.Manifest 20 import android.app.UiAutomation 21 import android.appwidget.AppWidgetHostView 22 import android.appwidget.AppWidgetManager 23 import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT 24 import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH 25 import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT 26 import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH 27 import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_SIZES 28 import android.content.Context 29 import android.os.Build 30 import android.text.Editable 31 import android.text.TextWatcher 32 import android.util.SizeF 33 import android.view.ViewTreeObserver 34 import android.widget.RemoteViews 35 import android.widget.TextView 36 import androidx.core.remoteviews.test.R 37 import androidx.core.util.SizeFCompat 38 import androidx.core.util.component1 39 import androidx.core.util.component2 40 import androidx.test.core.app.ApplicationProvider 41 import androidx.test.ext.junit.rules.ActivityScenarioRule 42 import androidx.test.filters.MediumTest 43 import androidx.test.filters.SdkSuppress 44 import androidx.test.platform.app.InstrumentationRegistry 45 import com.google.common.truth.Truth.assertThat 46 import java.util.concurrent.CountDownLatch 47 import java.util.concurrent.TimeUnit 48 import kotlin.test.fail 49 import org.junit.After 50 import org.junit.Before 51 import org.junit.Rule 52 import org.junit.Test 53 54 @SdkSuppress(minSdkVersion = 29) 55 @MediumTest 56 class AppWidgetManagerDeviceTest { 57 private val mContext = ApplicationProvider.getApplicationContext<Context>() 58 private val mPackageName = mContext.packageName 59 private val mAppWidgetManager = AppWidgetManager.getInstance(mContext) 60 61 @Rule 62 @JvmField 63 public val mActivityTestRule: ActivityScenarioRule<AppWidgetHostTestActivity> = 64 ActivityScenarioRule(AppWidgetHostTestActivity::class.java) 65 66 private lateinit var mRemoteViews: RemoteViews 67 private lateinit var mHostView: AppWidgetHostView 68 private var mAppWidgetId = 0 69 70 private val mUiAutomation: UiAutomation 71 get() = InstrumentationRegistry.getInstrumentation().uiAutomation 72 73 private val mTextView: TextView 74 get() = mHostView.getChildAt(0) as TextView 75 76 @Before 77 public fun setUp() { 78 mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.BIND_APPWIDGET) 79 80 mActivityTestRule.scenario.onActivity { activity -> mHostView = activity.bindAppWidget() } 81 82 mAppWidgetId = mHostView.appWidgetId 83 mRemoteViews = RemoteViews(mPackageName, R.layout.remote_views_text) 84 mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews) 85 86 // Wait until the remote views has been added to the host view. 87 observeDrawUntil { mHostView.childCount > 0 } 88 } 89 90 @After 91 public fun tearDown() { 92 mUiAutomation.dropShellPermissionIdentity() 93 } 94 95 @Test 96 public fun exact_shouldUseActualWidgetSize() { 97 mAppWidgetManager.updateAppWidget(mAppWidgetId) { (widthDp, heightDp) -> 98 RemoteViews(mPackageName, R.layout.remote_views_text).apply { 99 setTextViewText(R.id.text, "$widthDp x $heightDp") 100 } 101 } 102 103 val (width, height) = getSingleWidgetSize() 104 observeTextUntilEquals("$width x $height") 105 } 106 107 @Test 108 public fun responsive_shouldUseBestFittingProvidedWidgetSizes() { 109 val (width, height) = getSingleWidgetSize() 110 mAppWidgetManager.updateAppWidget( 111 mAppWidgetId, 112 listOf(width - 2 x height - 2, width + 2 x height + 2) 113 ) { (widthDp, heightDp) -> 114 RemoteViews(mPackageName, R.layout.remote_views_text).apply { 115 setTextViewText(R.id.text, "$widthDp x $heightDp") 116 } 117 } 118 119 observeTextUntilEquals("${width - 2} x ${height - 2}") 120 } 121 122 private fun observeTextUntilEquals(expectedText: String) { 123 if (mTextView.text.toString() == expectedText) return 124 125 val latch = CountDownLatch(1) 126 val textWatcher = 127 object : TextWatcher { 128 override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} 129 130 override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) { 131 if (text.toString() == expectedText) latch.countDown() 132 } 133 134 override fun afterTextChanged(text: Editable?) {} 135 } 136 137 mActivityTestRule.scenario.onActivity { mTextView.addTextChangedListener(textWatcher) } 138 139 val countedDown = latch.await(5, TimeUnit.SECONDS) 140 141 mActivityTestRule.scenario.onActivity { mTextView.removeTextChangedListener(textWatcher) } 142 143 if (!countedDown && mTextView.text.toString() != expectedText) { 144 fail("Expected text to be \"$expectedText\" within 5 seconds") 145 } 146 } 147 148 private fun observeDrawUntil(test: () -> Boolean) { 149 val latch = CountDownLatch(1) 150 val onDrawListener = ViewTreeObserver.OnDrawListener { if (test()) latch.countDown() } 151 152 mActivityTestRule.scenario.onActivity { 153 mHostView.viewTreeObserver.addOnDrawListener(onDrawListener) 154 } 155 156 val countedDown = latch.await(5, TimeUnit.SECONDS) 157 158 mActivityTestRule.scenario.onActivity { 159 mHostView.viewTreeObserver.removeOnDrawListener(onDrawListener) 160 } 161 162 if (!countedDown && !test()) { 163 fail("Expected condition to be met within 5 seconds") 164 } 165 } 166 167 @Suppress("DEPRECATION") 168 private fun getSingleWidgetSize(): SizeFCompat { 169 val options = mAppWidgetManager.getAppWidgetOptions(mAppWidgetId) 170 return if (Build.VERSION.SDK_INT >= 31) { 171 options.getParcelableArrayList<SizeF>(OPTION_APPWIDGET_SIZES)!!.single().let { 172 it.width x it.height 173 } 174 } else { 175 val minWidth = options.getInt(OPTION_APPWIDGET_MIN_WIDTH) 176 val maxWidth = options.getInt(OPTION_APPWIDGET_MAX_WIDTH) 177 val minHeight = options.getInt(OPTION_APPWIDGET_MIN_HEIGHT) 178 val maxHeight = options.getInt(OPTION_APPWIDGET_MAX_HEIGHT) 179 assertThat(minWidth).isEqualTo(maxWidth) 180 assertThat(minHeight).isEqualTo(maxHeight) 181 minWidth x minHeight 182 } 183 } 184 185 private infix fun Number.x(other: Number) = SizeFCompat(this.toFloat(), other.toFloat()) 186 } 187