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