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