1 @file:JvmName("AppWidgetManagerCompat")
2 
3 /*
4  * Copyright 2021 The Android Open Source Project
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *      http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 
19 package androidx.core.widget
20 
21 import android.appwidget.AppWidgetManager
22 import android.content.res.Resources
23 import android.os.Build.VERSION.SDK_INT
24 import android.util.Log
25 import android.util.SizeF
26 import android.widget.RemoteViews
27 import androidx.annotation.RequiresApi
28 import androidx.core.util.SizeFCompat
29 import kotlin.math.ceil
30 
31 /** Returns whether this size is approximately at least as big as [other] in all dimensions. */
approxDominatesnull32 internal infix fun SizeFCompat.approxDominates(other: SizeFCompat): Boolean {
33     return ceil(width) + 1 >= other.width && ceil(height) + 1 >= other.height
34 }
35 
36 internal val SizeFCompat.area: Float
37     get() = width * height
38 
39 /**
40  * Updates the app widget with [appWidgetId], creating a [RemoteViews] for each size assigned to the
41  * app widget by [AppWidgetManager], invoking [factory] to create each alternative view.
42  *
43  * This provides
44  * ["exact" sizing](https://developer.android.com/guide/topics/appwidgets/layouts#provide-exact-layouts)
45  * , which allows you to tailor your app widget appearance to the exact size at which it is
46  * displayed. If you are only concerned with a small number of size thresholds, it is preferable to
47  * use "responsive" sizing by providing a fixed set of sizes that your app widget supports.
48  *
49  * As your [factory] may be invoked multiple times, if there is expensive computation of state that
50  * is shared among each size, it is recommended to perform that computation before calling this and
51  * cache the results as necessary.
52  *
53  * To handle resizing of your app widget, it is necessary to call this function during both
54  * [android.appwidget.AppWidgetProvider.onUpdate] and
55  * [android.appwidget.AppWidgetProvider.onAppWidgetOptionsChanged].
56  *
57  * @param appWidgetId the id of the app widget
58  * @param factory a function to create a [RemoteViews] for a given width and height (in dp)
59  */
updateAppWidgetnull60 public fun AppWidgetManager.updateAppWidget(
61     appWidgetId: Int,
62     factory: (SizeFCompat) -> RemoteViews
63 ) {
64     updateAppWidget(appWidgetId, createExactSizeAppWidget(this, appWidgetId, factory))
65 }
66 
67 /**
68  * Creates a [RemoteViews] associated with each size assigned to the app widget by
69  * [AppWidgetManager], invoking [factory] to create each alternative view.
70  *
71  * This provides
72  * ["exact" sizing](https://developer.android.com/guide/topics/appwidgets/layouts#provide-exact-layouts)
73  * , which allows you to tailor your app widget appearance to the exact size at which it is
74  * displayed. If you are only concerned with a small number of size thresholds, it is preferable to
75  * use "responsive" sizing by providing a fixed set of sizes that your app widget supports.
76  *
77  * As your [factory] may be invoked multiple times, if there is expensive computation of state that
78  * is shared among each size, it is recommended to perform that computation before calling this and
79  * cache the results as necessary.
80  *
81  * To handle resizing of your app widget, it is necessary to call [AppWidgetManager.updateAppWidget]
82  * during both [android.appwidget.AppWidgetProvider.onUpdate] and
83  * [android.appwidget.AppWidgetProvider.onAppWidgetOptionsChanged].
84  *
85  * @param appWidgetManager the [AppWidgetManager] to provide information about [appWidgetId]
86  * @param appWidgetId the id of the app widget
87  * @param factory a function to create a [RemoteViews] for a given width and height (in dp)
88  */
createExactSizeAppWidgetnull89 public fun createExactSizeAppWidget(
90     appWidgetManager: AppWidgetManager,
91     appWidgetId: Int,
92     factory: (SizeFCompat) -> RemoteViews
93 ): RemoteViews {
94     appWidgetManager.requireValidAppWidgetId(appWidgetId)
95     return when {
96         SDK_INT >= 31 -> {
97             AppWidgetManagerApi31Impl.createExactSizeAppWidget(
98                 appWidgetManager,
99                 appWidgetId,
100                 factory
101             )
102         }
103         else -> createExactSizeAppWidgetInner(appWidgetManager, appWidgetId, factory)
104     }
105 }
106 
107 /**
108  * Updates the app widget with [appWidgetId], creating a [RemoteViews] for each size provided in
109  * [dpSizes].
110  *
111  * This provides
112  * ["responsive" sizing](https://developer.android.com/guide/topics/appwidgets/layouts#provide-responsive-layouts)
113  * , which allows for smoother resizing and a more consistent experience across different host
114  * configurations.
115  *
116  * As your [factory] may be invoked multiple times, if there is expensive computation of state that
117  * is shared among each size, it is recommended to perform that computation before calling this and
118  * cache the results as necessary.
119  *
120  * To handle resizing of your app widget, it is necessary to call this function during both
121  * [android.appwidget.AppWidgetProvider.onUpdate] and
122  * [android.appwidget.AppWidgetProvider.onAppWidgetOptionsChanged]. If your app's minSdk is 31 or
123  * higher, it is only necessary to call this function during `onUpdate`.
124  *
125  * @param appWidgetId the id of the app widget
126  * @param dpSizes a collection of sizes (in dp) that your app widget supports. Must not be empty or
127  *   contain more than 16 elements.
128  * @param factory a function to create a [RemoteViews] for a given width and height (in dp). It is
129  *   guaranteed that [factory] will only ever be called with the values provided in [dpSizes].
130  */
updateAppWidgetnull131 public fun AppWidgetManager.updateAppWidget(
132     appWidgetId: Int,
133     dpSizes: Collection<SizeFCompat>,
134     factory: (SizeFCompat) -> RemoteViews
135 ) {
136     updateAppWidget(appWidgetId, createResponsiveSizeAppWidget(this, appWidgetId, dpSizes, factory))
137 }
138 
139 /**
140  * Creating a [RemoteViews] associated with each size provided in [dpSizes].
141  *
142  * This provides
143  * ["responsive" sizing](https://developer.android.com/guide/topics/appwidgets/layouts#provide-responsive-layouts)
144  * , which allows for smoother resizing and a more consistent experience across different host
145  * configurations.
146  *
147  * As your [factory] may be invoked multiple times, if there is expensive computation of state that
148  * is shared among each size, it is recommended to perform that computation before calling this and
149  * cache the results as necessary.
150  *
151  * To handle resizing of your app widget, it is necessary to call [AppWidgetManager.updateAppWidget]
152  * during both [android.appwidget.AppWidgetProvider.onUpdate] and
153  * [android.appwidget.AppWidgetProvider.onAppWidgetOptionsChanged]. If your app's minSdk is 31 or
154  * higher, it is only necessary to call `updateAppWidget` during `onUpdate`.
155  *
156  * @param appWidgetManager the [AppWidgetManager] to provide information about [appWidgetId]
157  * @param appWidgetId the id of the app widget
158  * @param dpSizes a collection of sizes (in dp) that your app widget supports. Must not be empty or
159  *   contain more than 16 elements.
160  * @param factory a function to create a [RemoteViews] for a given width and height (in dp). It is
161  *   guaranteed that [factory] will only ever be called with the values provided in [dpSizes].
162  */
createResponsiveSizeAppWidgetnull163 public fun createResponsiveSizeAppWidget(
164     appWidgetManager: AppWidgetManager,
165     appWidgetId: Int,
166     dpSizes: Collection<SizeFCompat>,
167     factory: (SizeFCompat) -> RemoteViews
168 ): RemoteViews {
169     appWidgetManager.requireValidAppWidgetId(appWidgetId)
170     require(dpSizes.isNotEmpty()) { "Sizes cannot be empty" }
171     require(dpSizes.size <= 16) { "At most 16 sizes may be provided" }
172     return when {
173         SDK_INT >= 31 -> AppWidgetManagerApi31Impl.createResponsiveSizeAppWidget(dpSizes, factory)
174         else -> createResponsiveSizeAppWidgetInner(appWidgetManager, appWidgetId, dpSizes, factory)
175     }
176 }
177 
AppWidgetManagernull178 private fun AppWidgetManager.requireValidAppWidgetId(appWidgetId: Int) {
179     requireNotNull(getAppWidgetInfo(appWidgetId)) { "Invalid app widget id: $appWidgetId" }
180 }
181 
182 @RequiresApi(31)
183 @Suppress("DEPRECATION")
184 private object AppWidgetManagerApi31Impl {
createExactSizeAppWidgetnull185     fun createExactSizeAppWidget(
186         appWidgetManager: AppWidgetManager,
187         appWidgetId: Int,
188         factory: (SizeFCompat) -> RemoteViews
189     ): RemoteViews {
190         val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
191         val sizes = options.getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
192         if (sizes.isNullOrEmpty()) {
193             Log.w(
194                 LogTag,
195                 "App widget SizeF sizes not found in the options bundle, falling back to the " +
196                     "min/max sizes"
197             )
198             return createExactSizeAppWidgetInner(appWidgetManager, appWidgetId, factory)
199         }
200         return RemoteViews(sizes.associateWith { factory(it.toSizeFCompat()) })
201     }
202 
createResponsiveSizeAppWidgetnull203     fun createResponsiveSizeAppWidget(
204         dpSizes: Collection<SizeFCompat>,
205         factory: (SizeFCompat) -> RemoteViews
206     ): RemoteViews {
207         return RemoteViews(dpSizes.associate { it.toSizeF() to factory(it) })
208     }
209 
SizeFnull210     private fun SizeF.toSizeFCompat() = SizeFCompat.toSizeFCompat(this)
211 }
212 
213 internal fun createExactSizeAppWidgetInner(
214     appWidgetManager: AppWidgetManager,
215     appWidgetId: Int,
216     factory: (SizeFCompat) -> RemoteViews
217 ): RemoteViews {
218     val (landscapeSize, portraitSize) =
219         getSizesFromOptionsBundle(appWidgetManager, appWidgetId)
220             ?: run {
221                 Log.w(
222                     LogTag,
223                     "App widget sizes not found in the options bundle, falling back to the " +
224                         "provider size"
225                 )
226                 return createAppWidgetFromProviderInfo(appWidgetManager, appWidgetId, factory)
227             }
228     return createAppWidget(landscapeSize = landscapeSize, portraitSize = portraitSize, factory)
229 }
230 
createResponsiveSizeAppWidgetInnernull231 internal fun createResponsiveSizeAppWidgetInner(
232     appWidgetManager: AppWidgetManager,
233     appWidgetId: Int,
234     sizes: Collection<SizeFCompat>,
235     factory: (SizeFCompat) -> RemoteViews
236 ): RemoteViews {
237     val minSize = sizes.minByOrNull { it.area } ?: error("Sizes cannot be empty")
238     val (landscapeSize, portraitSize) =
239         getSizesFromOptionsBundle(appWidgetManager, appWidgetId)
240             ?: run {
241                 Log.w(
242                     LogTag,
243                     "App widget sizes not found in the options bundle, falling back to the " +
244                         "smallest supported size ($minSize)"
245                 )
246                 LandscapePortraitSizes(minSize, minSize)
247             }
248     val effectiveLandscapeSize =
249         sizes.filter { landscapeSize approxDominates it }.maxByOrNull { it.area } ?: minSize
250     val effectivePortraitSize =
251         sizes.filter { portraitSize approxDominates it }.maxByOrNull { it.area } ?: minSize
252     return createAppWidget(
253         landscapeSize = effectiveLandscapeSize,
254         portraitSize = effectivePortraitSize,
255         factory
256     )
257 }
258 
createAppWidgetnull259 private fun createAppWidget(
260     landscapeSize: SizeFCompat,
261     portraitSize: SizeFCompat,
262     factory: (SizeFCompat) -> RemoteViews
263 ): RemoteViews {
264     return if (landscapeSize == portraitSize) {
265         factory(landscapeSize)
266     } else {
267         RemoteViews(/* landscape= */ factory(landscapeSize), /* portrait= */ factory(portraitSize))
268     }
269 }
270 
getSizesFromOptionsBundlenull271 private fun getSizesFromOptionsBundle(
272     appWidgetManager: AppWidgetManager,
273     appWidgetId: Int
274 ): LandscapePortraitSizes? {
275     val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
276 
277     val portWidthDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, -1)
278     val portHeightDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, -1)
279     if (portWidthDp < 0 || portHeightDp < 0) return null
280 
281     val landWidthDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, -1)
282     val landHeightDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, -1)
283     if (landWidthDp < 0 || landHeightDp < 0) return null
284 
285     return LandscapePortraitSizes(
286         landscape = SizeFCompat(landWidthDp.toFloat(), landHeightDp.toFloat()),
287         portrait = SizeFCompat(portWidthDp.toFloat(), portHeightDp.toFloat())
288     )
289 }
290 
createAppWidgetFromProviderInfonull291 internal fun createAppWidgetFromProviderInfo(
292     appWidgetManager: AppWidgetManager,
293     appWidgetId: Int,
294     factory: (SizeFCompat) -> RemoteViews
295 ): RemoteViews {
296     return factory(appWidgetManager.getSizeFromProviderInfo(appWidgetId))
297 }
298 
getSizeFromProviderInfonull299 internal fun AppWidgetManager.getSizeFromProviderInfo(appWidgetId: Int): SizeFCompat {
300     val providerInfo = getAppWidgetInfo(appWidgetId)
301     fun pxToDp(value: Int) = (value / Resources.getSystem().displayMetrics.density)
302     val width = pxToDp(providerInfo.minWidth)
303     val height = pxToDp(providerInfo.minHeight)
304     return SizeFCompat(width, height)
305 }
306 
307 internal data class LandscapePortraitSizes(val landscape: SizeFCompat, val portrait: SizeFCompat)
308 
309 private const val LogTag = "AppWidgetManagerCompat"
310