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