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 
17 package androidx.glance.appwidget
18 
19 import android.content.Context
20 import android.os.Build
21 import android.util.Log
22 import android.util.TypedValue.COMPLEX_UNIT_DIP
23 import android.util.TypedValue.COMPLEX_UNIT_PX
24 import android.view.View
25 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
26 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
27 import android.widget.RemoteViews
28 import androidx.annotation.RequiresApi
29 import androidx.compose.ui.graphics.toArgb
30 import androidx.core.widget.RemoteViewsCompat.setTextViewHeight
31 import androidx.core.widget.RemoteViewsCompat.setTextViewWidth
32 import androidx.core.widget.RemoteViewsCompat.setViewBackgroundColor
33 import androidx.core.widget.RemoteViewsCompat.setViewBackgroundColorResource
34 import androidx.core.widget.RemoteViewsCompat.setViewBackgroundResource
35 import androidx.core.widget.RemoteViewsCompat.setViewClipToOutline
36 import androidx.glance.AndroidResourceImageProvider
37 import androidx.glance.BackgroundModifier
38 import androidx.glance.GlanceModifier
39 import androidx.glance.Visibility
40 import androidx.glance.VisibilityModifier
41 import androidx.glance.action.ActionModifier
42 import androidx.glance.appwidget.action.applyAction
43 import androidx.glance.color.DayNightColorProvider
44 import androidx.glance.layout.HeightModifier
45 import androidx.glance.layout.PaddingModifier
46 import androidx.glance.layout.WidthModifier
47 import androidx.glance.semantics.SemanticsModifier
48 import androidx.glance.semantics.SemanticsProperties
49 import androidx.glance.unit.Dimension
50 import androidx.glance.unit.FixedColorProvider
51 import androidx.glance.unit.ResourceColorProvider
52 
53 internal fun applyModifiers(
54     translationContext: TranslationContext,
55     rv: RemoteViews,
56     modifiers: GlanceModifier,
57     viewDef: InsertedViewInfo,
58 ) {
59     val context = translationContext.context
60     var widthModifier: WidthModifier? = null
61     var heightModifier: HeightModifier? = null
62     var paddingModifiers: PaddingModifier? = null
63     var cornerRadius: Dimension? = null
64     var visibility = Visibility.Visible
65     var actionModifier: ActionModifier? = null
66     var enabled: EnabledModifier? = null
67     var clipToOutline: ClipToOutlineModifier? = null
68     var semanticsModifier: SemanticsModifier? = null
69     modifiers.foldIn(Unit) { _, modifier ->
70         when (modifier) {
71             is ActionModifier -> {
72                 if (actionModifier != null) {
73                     Log.w(
74                         GlanceAppWidgetTag,
75                         "More than one clickable defined on the same GlanceModifier, " +
76                             "only the last one will be used."
77                     )
78                 }
79                 actionModifier = modifier
80             }
81             is WidthModifier -> widthModifier = modifier
82             is HeightModifier -> heightModifier = modifier
83             is BackgroundModifier -> applyBackgroundModifier(context, rv, modifier, viewDef)
84             is PaddingModifier -> {
85                 paddingModifiers = paddingModifiers?.let { it + modifier } ?: modifier
86             }
87             is VisibilityModifier -> visibility = modifier.visibility
88             is CornerRadiusModifier -> cornerRadius = modifier.radius
89             is AppWidgetBackgroundModifier -> {
90                 // This modifier is handled somewhere else.
91             }
92             is SelectableGroupModifier -> {
93                 if (!translationContext.canUseSelectableGroup) {
94                     error(
95                         "GlanceModifier.selectableGroup() can only be used on Row or Column " +
96                             "composables."
97                     )
98                 }
99             }
100             is AlignmentModifier -> {
101                 // This modifier is handled somewhere else.
102             }
103             is ClipToOutlineModifier -> clipToOutline = modifier
104             is EnabledModifier -> enabled = modifier
105             is SemanticsModifier -> semanticsModifier = modifier
106             else -> {
107                 Log.w(GlanceAppWidgetTag, "Unknown modifier '$modifier', nothing done.")
108             }
109         }
110     }
111     applySizeModifiers(translationContext, rv, widthModifier, heightModifier, viewDef)
112     actionModifier?.let { applyAction(translationContext, rv, it.action, viewDef.mainViewId) }
113     cornerRadius?.let { applyRoundedCorners(rv, viewDef.mainViewId, it) }
114     paddingModifiers?.let { padding ->
115         val absolutePadding = padding.toDp(context.resources).toAbsolute(translationContext.isRtl)
116         val displayMetrics = context.resources.displayMetrics
117         rv.setViewPadding(
118             viewDef.mainViewId,
119             absolutePadding.left.toPixels(displayMetrics),
120             absolutePadding.top.toPixels(displayMetrics),
121             absolutePadding.right.toPixels(displayMetrics),
122             absolutePadding.bottom.toPixels(displayMetrics)
123         )
124     }
125     clipToOutline?.let { clipModifier ->
126         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
127             rv.setBoolean(viewDef.mainViewId, "setClipToOutline", clipModifier.clip)
128         }
129     }
130     enabled?.let { rv.setBoolean(viewDef.mainViewId, "setEnabled", it.enabled) }
131     semanticsModifier?.let { semantics ->
132         val contentDescription: List<String>? =
133             semantics.configuration.getOrNull(SemanticsProperties.ContentDescription)
134         if (contentDescription != null) {
135             rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())
136         }
137     }
138     rv.setViewVisibility(viewDef.mainViewId, visibility.toViewVisibility())
139 }
140 
Visibilitynull141 private fun Visibility.toViewVisibility() =
142     when (this) {
143         Visibility.Visible -> View.VISIBLE
144         Visibility.Invisible -> View.INVISIBLE
145         Visibility.Gone -> View.GONE
146     }
147 
applySizeModifiersnull148 private fun applySizeModifiers(
149     translationContext: TranslationContext,
150     rv: RemoteViews,
151     widthModifier: WidthModifier?,
152     heightModifier: HeightModifier?,
153     viewDef: InsertedViewInfo
154 ) {
155     val context = translationContext.context
156     if (viewDef.isSimple) {
157         widthModifier?.let { applySimpleWidthModifier(context, rv, it, viewDef.mainViewId) }
158         heightModifier?.let { applySimpleHeightModifier(context, rv, it, viewDef.mainViewId) }
159         return
160     }
161 
162     check(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
163         "There is currently no valid use case where a complex view is used on Android S"
164     }
165 
166     val width = widthModifier?.width
167     val height = heightModifier?.height
168 
169     if (!(width.isFixed || height.isFixed)) {
170         // The sizing view is only present and needed for setting fixed dimensions.
171         return
172     }
173 
174     val useMatchSizeWidth = width is Dimension.Fill || width is Dimension.Expand
175     val useMatchSizeHeight = height is Dimension.Fill || height is Dimension.Expand
176     val sizeViewLayout =
177         when {
178             useMatchSizeWidth && useMatchSizeHeight -> R.layout.size_match_match
179             useMatchSizeWidth -> R.layout.size_match_wrap
180             useMatchSizeHeight -> R.layout.size_wrap_match
181             else -> R.layout.size_wrap_wrap
182         }
183 
184     val sizeTargetViewId = rv.inflateViewStub(translationContext, R.id.sizeViewStub, sizeViewLayout)
185 
186     fun Dimension.Dp.toPixels() = dp.toPixels(context)
187     fun Dimension.Resource.toPixels() = context.resources.getDimensionPixelSize(res)
188     when (width) {
189         is Dimension.Dp -> rv.setTextViewWidth(sizeTargetViewId, width.toPixels())
190         is Dimension.Resource -> rv.setTextViewWidth(sizeTargetViewId, width.toPixels())
191         Dimension.Expand,
192         Dimension.Fill,
193         Dimension.Wrap,
194         null -> {}
195     }.let {}
196     when (height) {
197         is Dimension.Dp -> rv.setTextViewHeight(sizeTargetViewId, height.toPixels())
198         is Dimension.Resource -> rv.setTextViewHeight(sizeTargetViewId, height.toPixels())
199         Dimension.Expand,
200         Dimension.Fill,
201         Dimension.Wrap,
202         null -> {}
203     }.let {}
204 }
205 
applySimpleWidthModifiernull206 internal fun applySimpleWidthModifier(
207     context: Context,
208     rv: RemoteViews,
209     modifier: WidthModifier,
210     viewId: Int,
211 ) {
212     val width = modifier.width
213     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
214         // Prior to Android S, these layouts already have the appropriate attribute in the xml, so
215         // no action is needed.
216         if (
217             width.resolveDimension(context) in
218                 listOf(Dimension.Wrap, Dimension.Fill, Dimension.Expand)
219         ) {
220             return
221         }
222         throw IllegalArgumentException(
223             "Using a width of $width requires a complex layout before API 31"
224         )
225     }
226     // Wrap and Expand are done in XML on Android S & Sv2
227     if (Build.VERSION.SDK_INT < 33 && width in listOf(Dimension.Wrap, Dimension.Expand)) return
228     ApplyModifiersApi31Impl.setViewWidth(rv, viewId, width)
229 }
230 
applySimpleHeightModifiernull231 internal fun applySimpleHeightModifier(
232     context: Context,
233     rv: RemoteViews,
234     modifier: HeightModifier,
235     viewId: Int,
236 ) {
237     // These layouts already have the appropriate attribute in the xml, so no action is needed.
238     val height = modifier.height
239     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
240         // Prior to Android S, these layouts already have the appropriate attribute in the xml, so
241         // no action is needed.
242         if (
243             height.resolveDimension(context) in
244                 listOf(Dimension.Wrap, Dimension.Fill, Dimension.Expand)
245         ) {
246             return
247         }
248         throw IllegalArgumentException(
249             "Using a height of $height requires a complex layout before API 31"
250         )
251     }
252     // Wrap and Expand are done in XML on Android S & Sv2
253     if (Build.VERSION.SDK_INT < 33 && height in listOf(Dimension.Wrap, Dimension.Expand)) return
254     ApplyModifiersApi31Impl.setViewHeight(rv, viewId, height)
255 }
256 
applyBackgroundModifiernull257 private fun applyBackgroundModifier(
258     context: Context,
259     rv: RemoteViews,
260     modifier: BackgroundModifier,
261     viewDef: InsertedViewInfo
262 ) {
263     val viewId = viewDef.mainViewId
264 
265     fun applyBackgroundImageModifier(modifier: BackgroundModifier.Image) {
266         val imageProvider = modifier.imageProvider
267         if (imageProvider is AndroidResourceImageProvider) {
268             rv.setViewBackgroundResource(viewId, imageProvider.resId)
269         }
270         // Otherwise, the background has been transformed and should be ignored
271         // (removing modifiers is not really possible).
272         return
273     }
274 
275     fun applyBackgroundColorModifier(modifier: BackgroundModifier.Color) {
276         when (val colorProvider = modifier.colorProvider) {
277             is FixedColorProvider -> rv.setViewBackgroundColor(viewId, colorProvider.color.toArgb())
278             is ResourceColorProvider ->
279                 rv.setViewBackgroundColorResource(viewId, colorProvider.resId)
280             is DayNightColorProvider -> {
281                 if (Build.VERSION.SDK_INT >= 31) {
282                     rv.setViewBackgroundColor(
283                         viewId,
284                         colorProvider.day.toArgb(),
285                         colorProvider.night.toArgb()
286                     )
287                 } else {
288                     rv.setViewBackgroundColor(viewId, colorProvider.getColor(context).toArgb())
289                 }
290             }
291             else ->
292                 Log.w(GlanceAppWidgetTag, "Unexpected background color modifier: $colorProvider")
293         }
294     }
295 
296     when (modifier) {
297         is BackgroundModifier.Image -> applyBackgroundImageModifier(modifier)
298         is BackgroundModifier.Color -> applyBackgroundColorModifier(modifier)
299     }
300 }
301 
302 private val Dimension?.isFixed: Boolean
303     get() =
304         when (this) {
305             is Dimension.Dp,
306             is Dimension.Resource -> true
307             Dimension.Expand,
308             Dimension.Fill,
309             Dimension.Wrap,
310             null -> false
311         }
312 
applyRoundedCornersnull313 private fun applyRoundedCorners(rv: RemoteViews, viewId: Int, radius: Dimension) {
314     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
315         ApplyModifiersApi31Impl.applyRoundedCorners(rv, viewId, radius)
316         return
317     }
318     Log.w(GlanceAppWidgetTag, "Cannot set the rounded corner of views before Api 31.")
319 }
320 
321 @RequiresApi(Build.VERSION_CODES.S)
322 private object ApplyModifiersApi31Impl {
setViewWidthnull323     fun setViewWidth(rv: RemoteViews, viewId: Int, width: Dimension) {
324         when (width) {
325             is Dimension.Wrap -> {
326                 rv.setViewLayoutWidth(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
327             }
328             is Dimension.Expand -> rv.setViewLayoutWidth(viewId, 0f, COMPLEX_UNIT_PX)
329             is Dimension.Dp -> rv.setViewLayoutWidth(viewId, width.dp.value, COMPLEX_UNIT_DIP)
330             is Dimension.Resource -> rv.setViewLayoutWidthDimen(viewId, width.res)
331             Dimension.Fill -> {
332                 rv.setViewLayoutWidth(viewId, MATCH_PARENT.toFloat(), COMPLEX_UNIT_PX)
333             }
334         }.let {}
335     }
336 
setViewHeightnull337     fun setViewHeight(rv: RemoteViews, viewId: Int, height: Dimension) {
338         when (height) {
339             is Dimension.Wrap -> {
340                 rv.setViewLayoutHeight(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
341             }
342             is Dimension.Expand -> rv.setViewLayoutHeight(viewId, 0f, COMPLEX_UNIT_PX)
343             is Dimension.Dp -> rv.setViewLayoutHeight(viewId, height.dp.value, COMPLEX_UNIT_DIP)
344             is Dimension.Resource -> rv.setViewLayoutHeightDimen(viewId, height.res)
345             Dimension.Fill -> {
346                 rv.setViewLayoutHeight(viewId, MATCH_PARENT.toFloat(), COMPLEX_UNIT_PX)
347             }
348         }.let {}
349     }
350 
applyRoundedCornersnull351     fun applyRoundedCorners(rv: RemoteViews, viewId: Int, radius: Dimension) {
352         rv.setViewClipToOutline(viewId, true)
353         when (radius) {
354             is Dimension.Dp -> {
355                 rv.setViewOutlinePreferredRadius(viewId, radius.dp.value, COMPLEX_UNIT_DIP)
356             }
357             is Dimension.Resource -> {
358                 rv.setViewOutlinePreferredRadiusDimen(viewId, radius.res)
359             }
360             else -> error("Rounded corners should not be ${radius.javaClass.canonicalName}")
361         }
362     }
363 }
364