1 /*
2 * 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 package androidx.glance.appwidget
17
18 import android.content.Context
19 import android.os.Build
20 import android.util.Log
21 import android.view.View
22 import android.view.ViewGroup
23 import android.widget.RemoteViews
24 import androidx.annotation.IdRes
25 import androidx.annotation.LayoutRes
26 import androidx.annotation.RequiresApi
27 import androidx.compose.ui.unit.dp
28 import androidx.glance.GlanceModifier
29 import androidx.glance.findModifier
30 import androidx.glance.layout.Alignment
31 import androidx.glance.layout.HeightModifier
32 import androidx.glance.layout.WidthModifier
33 import androidx.glance.unit.Dimension
34
35 /**
36 * Information about a generated layout, including the layout id, ids of elements within, and other
37 * details about the layout contents.
38 */
39 internal data class LayoutInfo(@LayoutRes val layoutId: Int)
40
41 /**
42 * Information about a [RemoteViews] created from generated layouts, including the layout id, ids of
43 * elements within, and other details about the layout contents.
44 */
45 internal data class RemoteViewsInfo(
46 val remoteViews: RemoteViews,
47 val view: InsertedViewInfo,
48 )
49
50 internal data class InsertedViewInfo(
51 val mainViewId: Int = View.NO_ID,
52 val complexViewId: Int = View.NO_ID,
53 val children: Map<Int, Map<SizeSelector, Int>> = emptyMap(),
54 )
55
56 internal val InsertedViewInfo.isSimple: Boolean
57 get() = complexViewId == View.NO_ID
58
59 /**
60 * Container selector.
61 *
62 * This class is used to select a particular container layout.
63 */
64 internal data class ContainerSelector(
65 val type: LayoutType,
66 val numChildren: Int,
67 val horizontalAlignment: Alignment.Horizontal? = null,
68 val verticalAlignment: Alignment.Vertical? = null,
69 )
70
71 internal data class ContainerInfo(@LayoutRes val layoutId: Int)
72
73 /**
74 * Box child selector.
75 *
76 * This class is used to select a layout with a particular alignment to be used as a child of Box.
77 */
78 internal data class BoxChildSelector(
79 val type: LayoutType,
80 val horizontalAlignment: Alignment.Horizontal,
81 val verticalAlignment: Alignment.Vertical,
82 )
83
84 /**
85 * Selector for children of [Row] and [Column].
86 *
87 * This class is used to select a layout with layout_weight set / unset.
88 */
89 internal data class RowColumnChildSelector(
90 val type: LayoutType,
91 val expandWidth: Boolean,
92 val expandHeight: Boolean,
93 )
94
95 /** Type of size needed for a layout. */
96 internal enum class LayoutSize {
97 Wrap,
98 Fixed,
99 Expand,
100 MatchParent,
101 }
102
103 /** Type of a layout. */
104 internal enum class LayoutType {
105 Row,
106 Column,
107 Box,
108 Text,
109 List,
110 CheckBox,
111 CheckBoxBackport,
112 Button,
113 Frame,
114 LinearProgressIndicator,
115 CircularProgressIndicator,
116 VerticalGridOneColumn,
117 VerticalGridTwoColumns,
118 VerticalGridThreeColumns,
119 VerticalGridFourColumns,
120 VerticalGridFiveColumns,
121 VerticalGridAutoFit,
122
123 // Note: Java keywords, such as 'switch', can't be used for layout ids.
124 Swtch,
125 SwtchBackport,
126 ImageCrop,
127 ImageFit,
128 ImageFillBounds,
129 ImageCropDecorative,
130 ImageFitDecorative,
131 ImageFillBoundsDecorative,
132 RadioButton,
133 RadioButtonBackport,
134 RadioRow,
135 RadioColumn,
136 }
137
138 /** Mapping from layout type to fixed layout (if any). */
139 private val LayoutMap =
140 mapOf(
141 LayoutType.Text to R.layout.glance_text,
142 LayoutType.List to R.layout.glance_list,
143 LayoutType.CheckBox to R.layout.glance_check_box,
144 LayoutType.CheckBoxBackport to R.layout.glance_check_box_backport,
145 LayoutType.Button to R.layout.glance_button,
146 LayoutType.Swtch to R.layout.glance_swtch,
147 LayoutType.SwtchBackport to R.layout.glance_swtch_backport,
148 LayoutType.Frame to R.layout.glance_frame,
149 LayoutType.ImageCrop to R.layout.glance_image_crop,
150 LayoutType.ImageCropDecorative to R.layout.glance_image_crop_decorative,
151 LayoutType.ImageFit to R.layout.glance_image_fit,
152 LayoutType.ImageFitDecorative to R.layout.glance_image_fit_decorative,
153 LayoutType.ImageFillBounds to R.layout.glance_image_fill_bounds,
154 LayoutType.ImageFillBoundsDecorative to R.layout.glance_image_fill_bounds_decorative,
155 LayoutType.LinearProgressIndicator to R.layout.glance_linear_progress_indicator,
156 LayoutType.CircularProgressIndicator to R.layout.glance_circular_progress_indicator,
157 LayoutType.VerticalGridOneColumn to R.layout.glance_vertical_grid_one_column,
158 LayoutType.VerticalGridTwoColumns to R.layout.glance_vertical_grid_two_columns,
159 LayoutType.VerticalGridThreeColumns to R.layout.glance_vertical_grid_three_columns,
160 LayoutType.VerticalGridFourColumns to R.layout.glance_vertical_grid_four_columns,
161 LayoutType.VerticalGridFiveColumns to R.layout.glance_vertical_grid_five_columns,
162 LayoutType.VerticalGridAutoFit to R.layout.glance_vertical_grid_auto_fit,
163 LayoutType.RadioButton to R.layout.glance_radio_button,
164 LayoutType.RadioButtonBackport to R.layout.glance_radio_button_backport,
165 )
166
167 internal data class SizeSelector(
168 val width: LayoutSize,
169 val height: LayoutSize,
170 )
171
172 /** Make the selector for a view sub, that is transforming "Fixed" into "Wrap". */
toViewStubSizenull173 private fun LayoutSize.toViewStubSize() = if (this == LayoutSize.Fixed) LayoutSize.Wrap else this
174
175 private fun makeViewStubSelector(width: LayoutSize, height: LayoutSize) =
176 SizeSelector(width = width.toViewStubSize(), height = height.toViewStubSize())
177
178 private val RootAliasTypeCount = generatedRootLayoutShifts.size
179
180 internal val TopLevelLayoutsCount: Int =
181 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
182 RootAliasCount
183 } else {
184 RootAliasCount / RootAliasTypeCount
185 }
186
187 /**
188 * Create the [RemoteViews] that can be used to create the child.
189 *
190 * @param translationContext Context for the translation for that node
191 * @param modifier Modifier attached to the view that will be added to the root
192 * @param aliasIndex Alias to use to create this root view
193 * @return The [RemoteViews] created and the descriptor needed to be able to add the first view.
194 */
createRootViewnull195 internal fun createRootView(
196 translationContext: TranslationContext,
197 modifier: GlanceModifier,
198 aliasIndex: Int
199 ): RemoteViewsInfo {
200 val context = translationContext.context
201 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
202 require(aliasIndex < RootAliasCount) {
203 "Index of the root view cannot be more than $RootAliasCount, " + "currently $aliasIndex"
204 }
205 val sizeSelector = SizeSelector(LayoutSize.Wrap, LayoutSize.Wrap)
206 val layoutId = FirstRootAlias + aliasIndex
207 return RemoteViewsInfo(
208 remoteViews =
209 remoteViews(translationContext, layoutId).apply {
210 modifier.findModifier<WidthModifier>()?.let {
211 applySimpleWidthModifier(context, this, it, R.id.rootView)
212 }
213 modifier.findModifier<HeightModifier>()?.let {
214 applySimpleHeightModifier(context, this, it, R.id.rootView)
215 }
216 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
217 removeAllViews(R.id.rootView)
218 }
219 },
220 view =
221 InsertedViewInfo(
222 mainViewId = R.id.rootView,
223 children =
224 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
225 emptyMap()
226 } else {
227 mapOf(0 to mapOf(sizeSelector to R.id.rootStubId))
228 }
229 ),
230 )
231 }
232 require(RootAliasTypeCount * aliasIndex < RootAliasCount) {
233 "Index of the root view cannot be more than ${RootAliasCount / 4}, " +
234 "currently $aliasIndex"
235 }
236 val widthMod =
237 modifier.findModifier<WidthModifier>()?.width?.resolveDimension(context) ?: Dimension.Wrap
238 val heightMod =
239 modifier.findModifier<HeightModifier>()?.height?.resolveDimension(context) ?: Dimension.Wrap
240 val width = if (widthMod == Dimension.Fill) LayoutSize.MatchParent else LayoutSize.Wrap
241 val height = if (heightMod == Dimension.Fill) LayoutSize.MatchParent else LayoutSize.Wrap
242 val sizeSelector = makeViewStubSelector(width, height)
243 val layoutIdShift =
244 generatedRootLayoutShifts[sizeSelector]
245 ?: throw IllegalStateException("Cannot find root element for size [$width, $height]")
246 val layoutId = FirstRootAlias + RootAliasTypeCount * aliasIndex + layoutIdShift
247 return RemoteViewsInfo(
248 remoteViews = remoteViews(translationContext, layoutId),
249 view = InsertedViewInfo(children = mapOf(0 to mapOf(sizeSelector to R.id.rootStubId))),
250 )
251 }
252
253 @IdRes
selectLayout33null254 private fun selectLayout33(
255 type: LayoutType,
256 modifier: GlanceModifier,
257 ): Int? {
258 if (Build.VERSION.SDK_INT < 33) return null
259 val align = modifier.findModifier<AlignmentModifier>()
260 val expandWidth =
261 modifier.findModifier<WidthModifier>()?.let { it.width == Dimension.Expand } ?: false
262 val expandHeight =
263 modifier.findModifier<HeightModifier>()?.let { it.height == Dimension.Expand } ?: false
264 if (align != null) {
265 return generatedBoxChildren[
266 BoxChildSelector(
267 type,
268 align.alignment.horizontal,
269 align.alignment.vertical,
270 )]
271 ?.layoutId
272 ?: throw IllegalArgumentException("Cannot find $type with alignment ${align.alignment}")
273 } else if (expandWidth || expandHeight) {
274 return generatedRowColumnChildren[
275 RowColumnChildSelector(
276 type,
277 expandWidth,
278 expandHeight,
279 )]
280 ?.layoutId ?: throw IllegalArgumentException("Cannot find $type with defaultWeight set")
281 } else {
282 return null
283 }
284 }
285
insertViewnull286 internal fun RemoteViews.insertView(
287 translationContext: TranslationContext,
288 type: LayoutType,
289 modifier: GlanceModifier
290 ): InsertedViewInfo {
291 val childLayout =
292 selectLayout33(type, modifier)
293 ?: LayoutMap[type]
294 ?: throw IllegalArgumentException("Cannot use `insertView` with a container like $type")
295 return insertViewInternal(translationContext, childLayout, modifier)
296 }
297
insertViewInternalnull298 private fun RemoteViews.insertViewInternal(
299 translationContext: TranslationContext,
300 @LayoutRes childLayout: Int,
301 modifier: GlanceModifier
302 ): InsertedViewInfo {
303 val pos = translationContext.itemPosition
304 val widthMod = modifier.findModifier<WidthModifier>()?.width ?: Dimension.Wrap
305 val heightMod = modifier.findModifier<HeightModifier>()?.height ?: Dimension.Wrap
306 // Null unless the view Id is specified by some attributes.
307 val specifiedViewId =
308 if (modifier.all { it !is AppWidgetBackgroundModifier }) {
309 null
310 } else {
311 check(!translationContext.isBackgroundSpecified.getAndSet(true)) {
312 "At most one view can be set as AppWidgetBackground."
313 }
314 android.R.id.background
315 }
316 if (Build.VERSION.SDK_INT >= 33) {
317 val viewId = specifiedViewId ?: translationContext.nextViewId()
318 val child =
319 LayoutSelectionApi31Impl.remoteViews(
320 translationContext.context.packageName,
321 childLayout,
322 viewId,
323 )
324 addChildView(translationContext.parentContext.mainViewId, child, pos)
325 return InsertedViewInfo(mainViewId = viewId)
326 }
327 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
328 val width = if (widthMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
329 val height = if (heightMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
330 val stubId = selectChild(translationContext, pos, width, height)
331 val resId = inflateViewStub(translationContext, stubId, childLayout, specifiedViewId)
332 return InsertedViewInfo(mainViewId = resId)
333 }
334 val context = translationContext.context
335 val width = widthMod.resolveDimension(context).toSpecSize()
336 val height = heightMod.resolveDimension(context).toSpecSize()
337 val stubId = selectChild(translationContext, pos, width, height)
338 val needsResize = width == LayoutSize.Fixed || height == LayoutSize.Fixed
339 return if (needsResize) {
340 val complexLayout =
341 generatedComplexLayouts[SizeSelector(width, height)]
342 ?: throw IllegalArgumentException(
343 "Could not find complex layout for width=$width, height=$height"
344 )
345 val complexId = inflateViewStub(translationContext, stubId, complexLayout.layoutId)
346 val childId =
347 inflateViewStub(translationContext, R.id.glanceViewStub, childLayout, specifiedViewId)
348 InsertedViewInfo(mainViewId = childId, complexViewId = complexId)
349 } else {
350 val resId = inflateViewStub(translationContext, stubId, childLayout, specifiedViewId)
351 InsertedViewInfo(mainViewId = resId)
352 }
353 }
354
355 @IdRes
RemoteViewsnull356 private fun RemoteViews.selectChild(
357 translationContext: TranslationContext,
358 pos: Int,
359 width: LayoutSize,
360 height: LayoutSize
361 ): Int {
362 val child = makeViewStubSelector(width, height)
363 @Suppress("PrimitiveInCollection")
364 val children =
365 translationContext.parentContext.children[pos]
366 ?: throw IllegalStateException("Parent doesn't have child position $pos")
367 val stubId =
368 children[child]
369 ?: throw IllegalStateException("No child for position $pos and size $width x $height")
370 children.values
371 .filter { it != stubId }
372 .forEach {
373 inflateViewStub(
374 translationContext,
375 it,
376 R.layout.glance_deleted_view,
377 R.id.deletedViewId
378 )
379 }
380 return stubId
381 }
382
insertContainerViewnull383 internal fun RemoteViews.insertContainerView(
384 translationContext: TranslationContext,
385 type: LayoutType,
386 numChildren: Int,
387 modifier: GlanceModifier,
388 horizontalAlignment: Alignment.Horizontal?,
389 verticalAlignment: Alignment.Vertical?,
390 ): InsertedViewInfo {
391 if (numChildren > 10) {
392 Log.e(
393 GlanceAppWidgetTag,
394 "Truncated $type container from $numChildren to 10 elements",
395 IllegalArgumentException("$type container cannot have more than 10 elements")
396 )
397 }
398 val children = numChildren.coerceAtMost(10)
399 val childLayout =
400 selectLayout33(type, modifier)
401 ?: generatedContainers[
402 ContainerSelector(type, children, horizontalAlignment, verticalAlignment)]
403 ?.layoutId
404 ?: throw IllegalArgumentException(
405 "Cannot find container $type with $numChildren children"
406 )
407 @Suppress("PrimitiveInCollection")
408 val childrenMapping =
409 generatedChildren[type]
410 ?: throw IllegalArgumentException("Cannot find generated children for $type")
411 return insertViewInternal(translationContext, childLayout, modifier)
412 .copy(children = childrenMapping)
413 .also { if (Build.VERSION.SDK_INT >= 33) removeAllViews(it.mainViewId) }
414 }
415
toSpecSizenull416 private fun Dimension.toSpecSize(): LayoutSize =
417 when (this) {
418 is Dimension.Wrap -> LayoutSize.Wrap
419 is Dimension.Expand -> LayoutSize.Expand
420 is Dimension.Fill -> LayoutSize.MatchParent
421 is Dimension.Dp,
422 is Dimension.Resource -> LayoutSize.Fixed
423 }
424
resolveDimensionnull425 internal fun Dimension.resolveDimension(context: Context): Dimension {
426 if (this !is Dimension.Resource) return this
427 val sizePx = context.resources.getDimension(res)
428 return when (sizePx.toInt()) {
429 ViewGroup.LayoutParams.MATCH_PARENT -> Dimension.Fill
430 ViewGroup.LayoutParams.WRAP_CONTENT -> Dimension.Wrap
431 else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
432 }
433 }
434
435 @RequiresApi(Build.VERSION_CODES.S)
436 private object LayoutSelectionApi31Impl {
remoteViewsnull437 fun remoteViews(packageName: String, @LayoutRes layoutId: Int, viewId: Int) =
438 RemoteViews(packageName, layoutId, viewId)
439 }
440