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