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 @file:Suppress("deprecation")
17 
18 package androidx.glance.wear.tiles
19 
20 import android.content.Context
21 import android.graphics.Bitmap
22 import android.util.Log
23 import android.view.View
24 import android.view.ViewGroup
25 import androidx.compose.ui.graphics.toArgb
26 import androidx.compose.ui.unit.dp
27 import androidx.glance.AndroidResourceImageProvider
28 import androidx.glance.BackgroundModifier
29 import androidx.glance.BitmapImageProvider
30 import androidx.glance.Emittable
31 import androidx.glance.EmittableButton
32 import androidx.glance.EmittableImage
33 import androidx.glance.GlanceModifier
34 import androidx.glance.TintColorFilterParams
35 import androidx.glance.VisibilityModifier
36 import androidx.glance.action.Action
37 import androidx.glance.action.ActionModifier
38 import androidx.glance.action.LambdaAction
39 import androidx.glance.action.StartActivityAction
40 import androidx.glance.action.StartActivityClassAction
41 import androidx.glance.action.StartActivityComponentAction
42 import androidx.glance.findModifier
43 import androidx.glance.layout.Alignment
44 import androidx.glance.layout.ContentScale
45 import androidx.glance.layout.EmittableBox
46 import androidx.glance.layout.EmittableColumn
47 import androidx.glance.layout.EmittableRow
48 import androidx.glance.layout.EmittableSpacer
49 import androidx.glance.layout.HeightModifier
50 import androidx.glance.layout.PaddingInDp
51 import androidx.glance.layout.PaddingModifier
52 import androidx.glance.layout.WidthModifier
53 import androidx.glance.layout.collectPaddingInDp
54 import androidx.glance.semantics.SemanticsModifier
55 import androidx.glance.semantics.SemanticsProperties
56 import androidx.glance.text.EmittableText
57 import androidx.glance.text.FontStyle
58 import androidx.glance.text.FontWeight
59 import androidx.glance.text.TextAlign
60 import androidx.glance.text.TextDecoration
61 import androidx.glance.text.TextStyle
62 import androidx.glance.toEmittableText
63 import androidx.glance.unit.ColorProvider
64 import androidx.glance.unit.Dimension
65 import androidx.glance.wear.tiles.action.RunCallbackAction
66 import androidx.glance.wear.tiles.curved.ActionCurvedModifier
67 import androidx.glance.wear.tiles.curved.AnchorType
68 import androidx.glance.wear.tiles.curved.CurvedTextStyle
69 import androidx.glance.wear.tiles.curved.EmittableCurvedChild
70 import androidx.glance.wear.tiles.curved.EmittableCurvedLine
71 import androidx.glance.wear.tiles.curved.EmittableCurvedRow
72 import androidx.glance.wear.tiles.curved.EmittableCurvedSpacer
73 import androidx.glance.wear.tiles.curved.EmittableCurvedText
74 import androidx.glance.wear.tiles.curved.GlanceCurvedModifier
75 import androidx.glance.wear.tiles.curved.RadialAlignment
76 import androidx.glance.wear.tiles.curved.SemanticsCurvedModifier
77 import androidx.glance.wear.tiles.curved.SweepAngleModifier
78 import androidx.glance.wear.tiles.curved.ThicknessModifier
79 import androidx.glance.wear.tiles.curved.findModifier
80 import androidx.wear.tiles.ColorBuilders
81 import java.io.ByteArrayOutputStream
82 import java.util.Arrays
83 
84 internal const val GlanceWearTileTag = "GlanceWearTile"
85 
86 @Suppress("deprecation") // for backward compatibility
87 @androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
88 private fun Alignment.Vertical.toProto(): Int =
89     when (this) {
90         Alignment.Vertical.Top -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
91         Alignment.Vertical.CenterVertically ->
92             androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
93         Alignment.Vertical.Bottom -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
94         else -> {
95             Log.w(GlanceWearTileTag, "Unknown vertical alignment type $this, align to Top instead")
96             androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
97         }
98     }
99 
100 @Suppress("deprecation") // for backward compatibility
101 @androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
Alignmentnull102 private fun Alignment.Horizontal.toProto(): Int =
103     when (this) {
104         Alignment.Horizontal.Start ->
105             androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
106         Alignment.Horizontal.CenterHorizontally ->
107             androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
108         Alignment.Horizontal.End -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
109         else -> {
110             Log.w(
111                 GlanceWearTileTag,
112                 "Unknown horizontal alignment type $this, align to Start instead"
113             )
114             androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
115         }
116     }
117 
118 @Suppress("deprecation") // for backward compatibility
toProtonull119 private fun PaddingInDp.toProto(): androidx.wear.tiles.ModifiersBuilders.Padding =
120     androidx.wear.tiles.ModifiersBuilders.Padding.Builder()
121         .setStart(androidx.wear.tiles.DimensionBuilders.dp(start.value))
122         .setTop(androidx.wear.tiles.DimensionBuilders.dp(top.value))
123         .setEnd(androidx.wear.tiles.DimensionBuilders.dp(end.value))
124         .setBottom((androidx.wear.tiles.DimensionBuilders.dp(bottom.value)))
125         .setRtlAware(true)
126         .build()
127 
128 @Suppress("deprecation") // for backward compatibility
129 private fun BackgroundModifier.toProto(
130     context: Context
131 ): androidx.wear.tiles.ModifiersBuilders.Background? =
132     when (this) {
133         is BackgroundModifier.Color ->
134             androidx.wear.tiles.ModifiersBuilders.Background.Builder()
135                 .setColor(ColorBuilders.argb(this.colorProvider.getColorAsArgb(context)))
136                 .build()
137         else -> error("Unexpected modifier $this")
138     }
139 
140 @Suppress("deprecation") // for backward compatibility
toProtonull141 private fun BorderModifier.toProto(context: Context): androidx.wear.tiles.ModifiersBuilders.Border =
142     androidx.wear.tiles.ModifiersBuilders.Border.Builder()
143         .setWidth(
144             androidx.wear.tiles.DimensionBuilders.dp(this.width.toDp(context.resources).value)
145         )
146         .setColor(androidx.wear.tiles.ColorBuilders.argb(this.color.getColorAsArgb(context)))
147         .build()
148 
149 @Suppress("deprecation") // for backward compatibility
150 private fun SemanticsModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
151     this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
152         androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
153             .setContentDescription(it.joinToString())
154             .build()
155     }
156 
157 @Suppress("deprecation") // for backward compatibility
toProtonull158 private fun SemanticsCurvedModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
159     this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
160         androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
161             .setContentDescription(it.joinToString())
162             .build()
163     }
164 
165 @Suppress("deprecation") // for backward compatibility
ColorProvidernull166 private fun ColorProvider.getColorAsArgb(context: Context) = getColor(context).toArgb()
167 
168 // TODO: handle parameters
169 @Suppress("deprecation") // for backward compatibility
170 private fun StartActivityAction.toProto(
171     context: Context
172 ): androidx.wear.tiles.ActionBuilders.LaunchAction =
173     androidx.wear.tiles.ActionBuilders.LaunchAction.Builder()
174         .setAndroidActivity(
175             androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder()
176                 .setPackageName(
177                     when (this) {
178                         is StartActivityComponentAction -> componentName.packageName
179                         is StartActivityClassAction -> context.packageName
180                         else -> error("Action type not defined in wear package: $this")
181                     }
182                 )
183                 .setClassName(
184                     when (this) {
185                         is StartActivityComponentAction -> componentName.className
186                         is StartActivityClassAction -> activityClass.name
187                         else -> error("Action type not defined in wear package: $this")
188                     }
189                 )
190                 .build()
191         )
192         .build()
193 
194 @Suppress("deprecation") // for backward compatibility
Actionnull195 private fun Action.toClickable(context: Context): androidx.wear.tiles.ModifiersBuilders.Clickable {
196     val builder = androidx.wear.tiles.ModifiersBuilders.Clickable.Builder()
197 
198     when (this) {
199         is StartActivityAction -> {
200             builder.setOnClick(toProto(context))
201         }
202         is RunCallbackAction -> {
203             builder
204                 .setOnClick(androidx.wear.tiles.ActionBuilders.LoadAction.Builder().build())
205                 .setId(callbackClass.canonicalName!!)
206         }
207         is LambdaAction -> {
208             Log.e(
209                 GlanceWearTileTag,
210                 "Lambda actions are not currently supported on Wear Tiles. Use " +
211                     "actionRunCallback actions instead."
212             )
213         }
214         else -> {
215             Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
216         }
217     }
218 
219     return builder.build()
220 }
221 
222 @Suppress("deprecation") // for backward compatibility
toProtonull223 private fun ActionModifier.toProto(
224     context: Context
225 ): androidx.wear.tiles.ModifiersBuilders.Clickable = this.action.toClickable(context)
226 
227 @Suppress("deprecation") // for backward compatibility
228 private fun ActionCurvedModifier.toProto(
229     context: Context
230 ): androidx.wear.tiles.ModifiersBuilders.Clickable = this.action.toClickable(context)
231 
232 @Suppress("deprecation") // for backward compatibility
233 private fun Dimension.toContainerDimension():
234     androidx.wear.tiles.DimensionBuilders.ContainerDimension =
235     when (this) {
236         is Dimension.Wrap -> androidx.wear.tiles.DimensionBuilders.wrap()
237         is Dimension.Expand -> androidx.wear.tiles.DimensionBuilders.expand()
238         is Dimension.Fill -> androidx.wear.tiles.DimensionBuilders.expand()
239         is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
240         else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
241     }
242 
243 @Suppress("deprecation") // for backward compatibility
244 @androidx.wear.tiles.LayoutElementBuilders.ArcAnchorType
toProtonull245 private fun AnchorType.toProto(): Int =
246     when (this) {
247         AnchorType.Start -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_START
248         AnchorType.Center -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_CENTER
249         AnchorType.End -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_END
250         else -> {
251             Log.w(GlanceWearTileTag, "Unknown arc anchor type $this, anchor to center instead")
252             androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_CENTER
253         }
254     }
255 
256 @Suppress("deprecation") // for backward compatibility
257 @androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
toProtonull258 private fun RadialAlignment.toProto(): Int =
259     when (this) {
260         RadialAlignment.Outer -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
261         RadialAlignment.Center -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
262         RadialAlignment.Inner -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
263         else -> {
264             Log.w(GlanceWearTileTag, "Unknown radial alignment $this, align to center instead")
265             androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
266         }
267     }
268 
269 @Suppress("deprecation") // for backward compatibility
270 @androidx.wear.tiles.LayoutElementBuilders.TextAlignment
TextAlignnull271 private fun TextAlign.toTextAlignment(isRtl: Boolean): Int =
272     when (this) {
273         TextAlign.Center -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_CENTER
274         TextAlign.End -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
275         TextAlign.Left ->
276             if (isRtl) androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
277             else androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
278         TextAlign.Right ->
279             if (isRtl) androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
280             else androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
281         TextAlign.Start -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
282         else -> {
283             Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
284             androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
285         }
286     }
287 
288 @Suppress("deprecation") // for backward compatibility
289 @androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
TextAlignnull290 private fun TextAlign.toHorizontalAlignment(): Int =
291     when (this) {
292         TextAlign.Center -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
293         TextAlign.End -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
294         TextAlign.Left -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_LEFT
295         TextAlign.Right -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_RIGHT
296         TextAlign.Start -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
297         else -> {
298             Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
299             androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
300         }
301     }
302 
resolvenull303 private fun Dimension.resolve(context: Context): Dimension {
304     if (this !is Dimension.Resource) return this
305     val sizePx = context.resources.getDimension(res)
306     return when (sizePx.toInt()) {
307         ViewGroup.LayoutParams.MATCH_PARENT -> Dimension.Fill
308         ViewGroup.LayoutParams.WRAP_CONTENT -> Dimension.Wrap
309         else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
310     }
311 }
312 
GlanceModifiernull313 private fun GlanceModifier.getWidth(
314     context: Context,
315     default: Dimension = Dimension.Wrap
316 ): Dimension = findModifier<WidthModifier>()?.width?.resolve(context) ?: default
317 
318 private fun GlanceModifier.getHeight(
319     context: Context,
320     default: Dimension = Dimension.Wrap
321 ): Dimension = findModifier<HeightModifier>()?.height?.resolve(context) ?: default
322 
323 @Suppress("deprecation") // for backward compatibility
324 private fun translateEmittableBox(
325     context: Context,
326     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
327     element: EmittableBox
328 ) =
329     androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
330         .setVerticalAlignment(element.contentAlignment.vertical.toProto())
331         .setHorizontalAlignment(element.contentAlignment.horizontal.toProto())
332         .setModifiers(translateModifiers(context, element.modifier))
333         .setWidth(element.modifier.getWidth(context).toContainerDimension())
334         .setHeight(element.modifier.getHeight(context).toContainerDimension())
335         .also { box ->
336             element.children.forEach {
337                 box.addContent(translateComposition(context, resourceBuilder, it))
338             }
339         }
340         .build()
341 
342 @Suppress("deprecation") // for backward compatibility
translateEmittableRownull343 private fun translateEmittableRow(
344     context: Context,
345     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
346     element: EmittableRow
347 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
348     val width = element.modifier.getWidth(context)
349     val height = element.modifier.getHeight(context)
350 
351     val baseRowBuilder =
352         androidx.wear.tiles.LayoutElementBuilders.Row.Builder()
353             .setHeight(height.toContainerDimension())
354             .setVerticalAlignment(element.verticalAlignment.toProto())
355             .also { row ->
356                 element.children.forEach {
357                     row.addContent(translateComposition(context, resourceBuilder, it))
358                 }
359             }
360 
361     // Do we need to wrap it in a column to set the horizontal alignment?
362     return if (
363         element.horizontalAlignment != Alignment.Horizontal.Start && width !is Dimension.Wrap
364     ) {
365         androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
366             .setHorizontalAlignment(element.horizontalAlignment.toProto())
367             .setModifiers(translateModifiers(context, element.modifier))
368             .setWidth(width.toContainerDimension())
369             .setHeight(height.toContainerDimension())
370             .addContent(
371                 baseRowBuilder.setWidth(androidx.wear.tiles.DimensionBuilders.wrap()).build()
372             )
373             .build()
374     } else {
375         baseRowBuilder
376             .setModifiers(translateModifiers(context, element.modifier))
377             .setWidth(width.toContainerDimension())
378             .build()
379     }
380 }
381 
382 @Suppress("deprecation") // for backward compatibility
translateEmittableColumnnull383 private fun translateEmittableColumn(
384     context: Context,
385     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
386     element: EmittableColumn
387 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
388     val width = element.modifier.getWidth(context)
389     val height = element.modifier.getHeight(context)
390 
391     val baseColumnBuilder =
392         androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
393             .setWidth(width.toContainerDimension())
394             .setHorizontalAlignment(element.horizontalAlignment.toProto())
395             .also { column ->
396                 element.children.forEach {
397                     column.addContent(translateComposition(context, resourceBuilder, it))
398                 }
399             }
400 
401     // Do we need to wrap it in a row to set the vertical alignment?
402     return if (element.verticalAlignment != Alignment.Vertical.Top && height !is Dimension.Wrap) {
403         androidx.wear.tiles.LayoutElementBuilders.Row.Builder()
404             .setVerticalAlignment(element.verticalAlignment.toProto())
405             .setModifiers(translateModifiers(context, element.modifier))
406             .setWidth(width.toContainerDimension())
407             .setHeight(height.toContainerDimension())
408             .addContent(
409                 baseColumnBuilder.setHeight(androidx.wear.tiles.DimensionBuilders.wrap()).build()
410             )
411             .build()
412     } else {
413         baseColumnBuilder
414             .setModifiers(translateModifiers(context, element.modifier))
415             .setHeight(height.toContainerDimension())
416             .build()
417     }
418 }
419 
420 @Suppress("deprecation") // for backward compatibility
translateTextStylenull421 private fun translateTextStyle(
422     context: Context,
423     style: TextStyle,
424 ): androidx.wear.tiles.LayoutElementBuilders.FontStyle {
425     val fontStyleBuilder = androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder()
426 
427     style.color.let {
428         fontStyleBuilder.setColor(
429             androidx.wear.tiles.ColorBuilders.argb(it.getColorAsArgb(context))
430         )
431     }
432     // TODO(b/203656358): Can we support Em here too?
433     style.fontSize?.let {
434         if (!it.isSp) {
435             throw IllegalArgumentException("Only Sp is supported for font size")
436         }
437         fontStyleBuilder.setSize(androidx.wear.tiles.DimensionBuilders.sp(it.value))
438     }
439     style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
440     style.fontWeight?.let {
441         fontStyleBuilder.setWeight(
442             when (it) {
443                 FontWeight.Normal -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
444                 FontWeight.Medium -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
445                 FontWeight.Bold -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
446                 else -> {
447                     Log.w(GlanceWearTileTag, "Unknown font weight $it, use Normal weight instead")
448                     androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
449                 }
450             }
451         )
452     }
453     style.textDecoration?.let { fontStyleBuilder.setUnderline(TextDecoration.Underline in it) }
454 
455     return fontStyleBuilder.build()
456 }
457 
458 @Suppress("deprecation") // for backward compatibility
translateTextStylenull459 private fun translateTextStyle(
460     context: Context,
461     style: CurvedTextStyle,
462 ): androidx.wear.tiles.LayoutElementBuilders.FontStyle {
463     val fontStyleBuilder = androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder()
464 
465     style.color?.let {
466         fontStyleBuilder.setColor(
467             androidx.wear.tiles.ColorBuilders.argb(it.getColorAsArgb(context))
468         )
469     }
470     style.fontSize?.let {
471         fontStyleBuilder.setSize(androidx.wear.tiles.DimensionBuilders.sp(it.value))
472     }
473     style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
474     style.fontWeight?.let {
475         fontStyleBuilder.setWeight(
476             when (it) {
477                 FontWeight.Normal -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
478                 FontWeight.Medium -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
479                 FontWeight.Bold -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
480                 else -> {
481                     Log.w(GlanceWearTileTag, "Unknown font weight $it, use Normal weight instead")
482                     androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
483                 }
484             }
485         )
486     }
487 
488     return fontStyleBuilder.build()
489 }
490 
491 @Suppress("deprecation") // for backward compatibility
translateEmittableTextnull492 private fun translateEmittableText(
493     context: Context,
494     element: EmittableText
495 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
496     // Does it have a width or height set? If so, we need to wrap it in a Box.
497     val width = element.modifier.getWidth(context)
498     val height = element.modifier.getHeight(context)
499 
500     val textBuilder =
501         androidx.wear.tiles.LayoutElementBuilders.Text.Builder()
502             .setText(element.text)
503             .setMaxLines(element.maxLines)
504 
505     element.style?.let { textBuilder.setFontStyle(translateTextStyle(context, it)) }
506 
507     val textAlign: TextAlign? = element.style?.textAlign
508     if (textAlign != null) {
509         val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
510         textBuilder.setMultilineAlignment(textAlign.toTextAlignment(isRtl))
511     }
512 
513     return if (width !is Dimension.Wrap || height !is Dimension.Wrap) {
514         val boxBuilder = androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
515         if (textAlign != null) {
516             boxBuilder.setHorizontalAlignment(textAlign.toHorizontalAlignment())
517         }
518         boxBuilder
519             .setWidth(width.toContainerDimension())
520             .setHeight(height.toContainerDimension())
521             .setModifiers(translateModifiers(context, element.modifier))
522             .addContent(textBuilder.build())
523             .build()
524     } else {
525         textBuilder.setModifiers(translateModifiers(context, element.modifier)).build()
526     }
527 }
528 
529 @Suppress("deprecation") // for backward compatibility
toImageDimensionnull530 private fun Dimension.toImageDimension(): androidx.wear.tiles.DimensionBuilders.ImageDimension =
531     when (this) {
532         is Dimension.Expand -> androidx.wear.tiles.DimensionBuilders.expand()
533         is Dimension.Fill -> androidx.wear.tiles.DimensionBuilders.expand()
534         is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
535         else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
536     }
537 
538 @Suppress("deprecation") // for backward compatibility
translateEmittableImagenull539 private fun translateEmittableImage(
540     context: Context,
541     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
542     element: EmittableImage
543 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
544     var mappedResId: String
545     when (element.provider) {
546         is AndroidResourceImageProvider -> {
547             val resId = (element.provider as AndroidResourceImageProvider).resId
548             mappedResId = "android_$resId"
549             resourceBuilder.addIdToImageMapping(
550                 mappedResId,
551                 androidx.wear.tiles.ResourceBuilders.ImageResource.Builder()
552                     .setAndroidResourceByResId(
553                         androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId.Builder()
554                             .setResourceId(resId)
555                             .build()
556                     )
557                     .build()
558             )
559         }
560         is BitmapImageProvider -> {
561             val bitmap = (element.provider as BitmapImageProvider).bitmap
562             val buffer =
563                 ByteArrayOutputStream()
564                     .apply { bitmap.compress(Bitmap.CompressFormat.PNG, 100, this) }
565                     .toByteArray()
566             mappedResId = "android_${Arrays.hashCode(buffer)}"
567             resourceBuilder.addIdToImageMapping(
568                 mappedResId,
569                 androidx.wear.tiles.ResourceBuilders.ImageResource.Builder()
570                     .setInlineResource(
571                         androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder()
572                             .setWidthPx(bitmap.width)
573                             .setHeightPx(bitmap.height)
574                             .setData(buffer)
575                             .build()
576                     )
577                     .build()
578             )
579         }
580         else -> throw IllegalArgumentException("An unsupported ImageProvider type was used")
581     }
582 
583     val imageBuilder =
584         androidx.wear.tiles.LayoutElementBuilders.Image.Builder()
585             .setWidth(element.modifier.getWidth(context).toImageDimension())
586             .setHeight(element.modifier.getHeight(context).toImageDimension())
587             .setModifiers(translateModifiers(context, element.modifier))
588             .setResourceId(mappedResId)
589             .setContentScaleMode(
590                 when (element.contentScale) {
591                     ContentScale.Crop ->
592                         androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_CROP
593                     ContentScale.Fit ->
594                         androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
595                     ContentScale.FillBounds ->
596                         androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FILL_BOUNDS
597                     // Defaults to CONTENT_SCALE_MODE_FIT
598                     else -> androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
599                 }
600             )
601 
602     element.colorFilterParams?.let { colorFilterParams ->
603         when (colorFilterParams) {
604             is TintColorFilterParams -> {
605                 imageBuilder.setColorFilter(
606                     androidx.wear.tiles.LayoutElementBuilders.ColorFilter.Builder()
607                         .setTint(
608                             androidx.wear.tiles.ColorBuilders.argb(
609                                 colorFilterParams.colorProvider.getColorAsArgb(context)
610                             )
611                         )
612                         .build()
613                 )
614             }
615             else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
616         }
617     }
618     return imageBuilder.build()
619 }
620 
621 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedRownull622 private fun translateEmittableCurvedRow(
623     context: Context,
624     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
625     element: EmittableCurvedRow
626 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
627     // Does it have a width or height set? If so, we need to wrap it in a Box.
628     val width = element.modifier.getWidth(context)
629     val height = element.modifier.getHeight(context)
630 
631     // Note: Wear Tiles uses 0 degrees = 12 o clock, but Glance / Wear Compose use 0 degrees = 3
632     // o clock. Tiles supports wraparound etc though, so just add on the 90 degrees here.
633     val arcBuilder =
634         androidx.wear.tiles.LayoutElementBuilders.Arc.Builder()
635             .setAnchorAngle(
636                 androidx.wear.tiles.DimensionBuilders.degrees(element.anchorDegrees + 90f)
637             )
638             .setAnchorType(element.anchorType.toProto())
639             .setVerticalAlign(element.radialAlignment.toProto())
640 
641     // Add all the children first...
642     element.children.forEach { curvedChild ->
643         if (curvedChild is EmittableCurvedChild) {
644             curvedChild.children.forEach {
645                 arcBuilder.addContent(
646                     translateEmittableElementInArc(
647                         context,
648                         resourceBuilder,
649                         it,
650                         curvedChild.rotateContent
651                     )
652                 )
653             }
654         } else {
655             arcBuilder.addContent(translateCurvedCompositionInArc(context, curvedChild))
656         }
657     }
658 
659     return if (width is Dimension.Dp || height is Dimension.Dp) {
660         androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
661             .setWidth(width.toContainerDimension())
662             .setHeight(height.toContainerDimension())
663             .setModifiers(translateModifiers(context, element.modifier))
664             .addContent(arcBuilder.build())
665             .build()
666     } else {
667         arcBuilder.setModifiers(translateModifiers(context, element.modifier)).build()
668     }
669 }
670 
671 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedTextnull672 private fun translateEmittableCurvedText(
673     context: Context,
674     element: EmittableCurvedText
675 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
676     // Modifiers are currently ignored for this element; we'll have to add CurvedScope modifiers in
677     // future which can be used with ArcModifiers, but we don't have any of those added right now.
678     val arcTextBuilder =
679         androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder().setText(element.text)
680 
681     element.style?.let { arcTextBuilder.setFontStyle(translateTextStyle(context, it)) }
682 
683     arcTextBuilder.setModifiers(translateCurvedModifiers(context, element.curvedModifier))
684 
685     return arcTextBuilder.build()
686 }
687 
688 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedLinenull689 private fun translateEmittableCurvedLine(
690     context: Context,
691     element: EmittableCurvedLine
692 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
693     var sweepAngleDegrees = element.curvedModifier.findModifier<SweepAngleModifier>()?.degrees ?: 0f
694     var thickness = element.curvedModifier.findModifier<ThicknessModifier>()?.thickness ?: 0.dp
695 
696     return androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder()
697         .setLength(androidx.wear.tiles.DimensionBuilders.degrees(sweepAngleDegrees))
698         .setThickness(androidx.wear.tiles.DimensionBuilders.dp(thickness.value))
699         .setColor(androidx.wear.tiles.ColorBuilders.argb(element.color.getColorAsArgb(context)))
700         .setModifiers(translateCurvedModifiers(context, element.curvedModifier))
701         .build()
702 }
703 
704 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedSpacernull705 private fun translateEmittableCurvedSpacer(
706     context: Context,
707     element: EmittableCurvedSpacer
708 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
709     var sweepAngleDegrees = element.curvedModifier.findModifier<SweepAngleModifier>()?.degrees ?: 0f
710     var thickness = element.curvedModifier.findModifier<ThicknessModifier>()?.thickness ?: 0.dp
711 
712     return androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder()
713         .setLength(androidx.wear.tiles.DimensionBuilders.degrees(sweepAngleDegrees))
714         .setThickness(androidx.wear.tiles.DimensionBuilders.dp(thickness.value))
715         .setModifiers(translateCurvedModifiers(context, element.curvedModifier))
716         .build()
717 }
718 
719 @Suppress("deprecation") // for backward compatibility
translateEmittableElementInArcnull720 private fun translateEmittableElementInArc(
721     context: Context,
722     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
723     element: Emittable,
724     rotateContent: Boolean
725 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement =
726     androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder()
727         .setContent(translateComposition(context, resourceBuilder, element))
728         .setRotateContents(rotateContent)
729         .build()
730 
731 @Suppress("deprecation") // for backward compatibility
732 private fun translateCurvedModifiers(
733     context: Context,
734     curvedModifier: GlanceCurvedModifier
735 ): androidx.wear.tiles.ModifiersBuilders.ArcModifiers =
736     curvedModifier
737         .foldIn(androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder()) { builder, element ->
738             when (element) {
739                 is ActionCurvedModifier -> builder.setClickable(element.toProto(context))
740                 is ThicknessModifier -> builder /* Skip for now, handled elsewhere. */
741                 is SweepAngleModifier -> builder /* Skip for now, handled elsewhere. */
742                 is SemanticsCurvedModifier -> {
743                     element.toProto()?.let { builder.setSemantics(it) } ?: builder
744                 }
745                 else -> throw IllegalArgumentException("Unknown curved modifier type")
746             }
747         }
748         .build()
749 
750 @Suppress("deprecation") // for backward compatibility
translateModifiersnull751 private fun translateModifiers(
752     context: Context,
753     modifier: GlanceModifier,
754 ): androidx.wear.tiles.ModifiersBuilders.Modifiers =
755     modifier
756         .foldIn(androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder()) { builder, element ->
757             when (element) {
758                 is BackgroundModifier -> {
759                     element.toProto(context)?.let { builder.setBackground(it) } ?: builder
760                 }
761                 is WidthModifier -> builder /* Skip for now, handled elsewhere. */
762                 is HeightModifier -> builder /* Skip for now, handled elsewhere. */
763                 is ActionModifier -> builder.setClickable(element.toProto(context))
764                 is PaddingModifier -> builder // Processing that after
765                 is VisibilityModifier -> builder // Already processed
766                 is BorderModifier -> builder.setBorder(element.toProto(context))
767                 is SemanticsModifier -> {
768                     element.toProto()?.let { builder.setSemantics(it) } ?: builder
769                 }
770                 else -> throw IllegalArgumentException("Unknown modifier type")
771             }
772         }
buildernull773         .also { builder ->
774             val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
775             modifier.collectPaddingInDp(context.resources)?.toRelative(isRtl)?.let {
776                 builder.setPadding(it.toProto())
777             }
778         }
779         .build()
780 
781 @Suppress("deprecation") // for backward compatibility
translateCurvedCompositionInArcnull782 private fun translateCurvedCompositionInArc(
783     context: Context,
784     element: Emittable
785 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
786     return when (element) {
787         is EmittableCurvedText -> translateEmittableCurvedText(context, element)
788         is EmittableCurvedLine -> translateEmittableCurvedLine(context, element)
789         is EmittableCurvedSpacer -> translateEmittableCurvedSpacer(context, element)
790         else -> throw IllegalArgumentException("Unknown curved Element: $element")
791     }
792 }
793 
794 @Suppress("deprecation") // for backward compatibility
toSpacerDimensionnull795 private fun Dimension.toSpacerDimension(): androidx.wear.tiles.DimensionBuilders.SpacerDimension =
796     when (this) {
797         is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
798         else ->
799             throw IllegalArgumentException(
800                 "The spacer dimension should be with dp value, not $this."
801             )
802     }
803 
804 @Suppress("deprecation") // for backward compatibility
translateEmittableSpacernull805 private fun translateEmittableSpacer(context: Context, element: EmittableSpacer) =
806     androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder()
807         .setWidth(element.modifier.getWidth(context, Dimension.Dp(0.dp)).toSpacerDimension())
808         .setHeight(element.modifier.getHeight(context, Dimension.Dp(0.dp)).toSpacerDimension())
809         .build()
810 
811 private fun translateEmittableAndroidLayoutElement(element: EmittableAndroidLayoutElement) =
812     element.layoutElement
813 
814 /**
815  * Translates a Glance Composition to a Wear Tile.
816  *
817  * @throws IllegalArgumentException If the provided Emittable is not recognised (e.g. it is an
818  *   element which this translator doesn't understand).
819  */
820 @Suppress("deprecation") // for backward compatibility
821 internal fun translateComposition(
822     context: Context,
823     resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
824     element: Emittable
825 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
826     return when (element) {
827         is EmittableBox -> translateEmittableBox(context, resourceBuilder, element)
828         is EmittableRow -> translateEmittableRow(context, resourceBuilder, element)
829         is EmittableColumn -> translateEmittableColumn(context, resourceBuilder, element)
830         is EmittableText -> translateEmittableText(context, element)
831         is EmittableCurvedRow -> translateEmittableCurvedRow(context, resourceBuilder, element)
832         is EmittableAndroidLayoutElement -> translateEmittableAndroidLayoutElement(element)
833         is EmittableButton -> translateEmittableText(context, element.toEmittableText())
834         is EmittableSpacer -> translateEmittableSpacer(context, element)
835         is EmittableImage -> translateEmittableImage(context, resourceBuilder, element)
836         else -> throw IllegalArgumentException("Unknown element $element")
837     }
838 }
839 
840 @Suppress("deprecation") // for backward compatibility
841 internal class CompositionResult(
842     val layout: androidx.wear.tiles.LayoutElementBuilders.LayoutElement,
843     val resources: androidx.wear.tiles.ResourceBuilders.Resources.Builder
844 )
845 
846 @Suppress("deprecation") // for backward compatibility
translateTopLevelCompositionnull847 internal fun translateTopLevelComposition(context: Context, element: Emittable): CompositionResult {
848     val resourceBuilder = androidx.wear.tiles.ResourceBuilders.Resources.Builder()
849     val layout = translateComposition(context, resourceBuilder, element)
850     return CompositionResult(layout, resourceBuilder)
851 }
852