1 /*
<lambda>null2  * Copyright 2022 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  *      https://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 @file:Suppress("DEPRECATION")
18 
19 package com.google.accompanist.themeadapter.core
20 
21 import android.annotation.SuppressLint
22 import android.content.Context
23 import android.content.res.Resources
24 import android.content.res.TypedArray
25 import android.graphics.Typeface
26 import android.os.Build
27 import android.util.TypedValue
28 import androidx.annotation.RequiresApi
29 import androidx.annotation.StyleRes
30 import androidx.compose.foundation.shape.CornerBasedShape
31 import androidx.compose.foundation.shape.CornerSize
32 import androidx.compose.foundation.shape.CutCornerShape
33 import androidx.compose.foundation.shape.RoundedCornerShape
34 import androidx.compose.ui.geometry.Offset
35 import androidx.compose.ui.graphics.Color
36 import androidx.compose.ui.graphics.Shadow
37 import androidx.compose.ui.text.TextStyle
38 import androidx.compose.ui.text.font.Font
39 import androidx.compose.ui.text.font.FontFamily
40 import androidx.compose.ui.text.font.FontStyle
41 import androidx.compose.ui.text.font.FontWeight
42 import androidx.compose.ui.text.font.toFontFamily
43 import androidx.compose.ui.unit.Density
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.TextUnit
46 import androidx.compose.ui.unit.dp
47 import androidx.compose.ui.unit.em
48 import androidx.compose.ui.unit.sp
49 import androidx.core.content.res.FontResourcesParserCompat
50 import androidx.core.content.res.getColorOrThrow
51 import androidx.core.content.res.use
52 import kotlin.concurrent.getOrSet
53 
54 /**
55  * Returns the given index as a [Color], or [fallbackColor] if the value can't be coerced to a
56  * [Color].
57  *
58  * @param index Index of attribute to retrieve.
59  * @param fallbackColor Value to return if the attribute is not defined or can't be coerced to a
60  * [Color].
61  */
62 @Deprecated(
63     """
64    ThemeAdapter is deprecated.
65 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
66 """
67 )
68 public fun TypedArray.parseColor(
69     index: Int,
70     fallbackColor: Color = Color.Unspecified
71 ): Color = if (hasValue(index)) Color(getColorOrThrow(index)) else fallbackColor
72 
73 /**
74  * Returns the given style resource ID as a [TextStyle].
75  *
76  * @param context The current context.
77  * @param id ID of style resource to retrieve.
78  * @param density The current display density.
79  * @param setTextColors Whether to read and set text colors from the style. Defaults to `false`.
80  * @param defaultFontFamily Optional default font family to use in [TextStyle]s.
81  */
82 @Deprecated(
83     """
84    ThemeAdapter is deprecated.
85 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
86 """
87 )
88 public fun parseTextAppearance(
89     context: Context,
90     @StyleRes id: Int,
91     density: Density,
92     setTextColors: Boolean,
93     defaultFontFamily: FontFamily?
94 ): TextStyle {
95     return context.obtainStyledAttributes(id, R.styleable.ThemeAdapterTextAppearance).use { a ->
96         val textStyle = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_textStyle, -1)
97         val textFontWeight = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_textFontWeight, -1)
98         val typeface = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_typeface, -1)
99 
100         // TODO read and expand android:fontVariationSettings.
101         // Variable fonts are not supported in Compose yet
102 
103         // FYI, this only works with static font files in assets
104         val fontFamily: FontFamilyWithWeight? = a.parseFontFamily(
105             R.styleable.ThemeAdapterTextAppearance_fontFamily
106         ) ?: a.parseFontFamily(R.styleable.ThemeAdapterTextAppearance_android_fontFamily)
107 
108         TextStyle(
109             color = when {
110                 setTextColors -> {
111                     a.parseColor(R.styleable.ThemeAdapterTextAppearance_android_textColor)
112                 }
113                 else -> Color.Unspecified
114             },
115             fontSize = a.parseTextUnit(R.styleable.ThemeAdapterTextAppearance_android_textSize, density),
116             lineHeight = run {
117                 a.parseTextUnit(
118                     R.styleable.ThemeAdapterTextAppearance_lineHeight, density,
119                     fallbackTextUnit = a.parseTextUnit(
120                         R.styleable.ThemeAdapterTextAppearance_android_lineHeight, density
121                     )
122                 )
123             },
124             fontFamily = when {
125                 defaultFontFamily != null -> defaultFontFamily
126                 fontFamily != null -> fontFamily.fontFamily
127                 // Values below are from frameworks/base attrs.xml
128                 typeface == 1 -> FontFamily.SansSerif
129                 typeface == 2 -> FontFamily.Serif
130                 typeface == 3 -> FontFamily.Monospace
131                 else -> null
132             },
133             fontStyle = when {
134                 (textStyle and Typeface.ITALIC) != 0 -> FontStyle.Italic
135                 else -> FontStyle.Normal
136             },
137             fontWeight = when {
138                 textFontWeight in 0..149 -> FontWeight.W100
139                 textFontWeight in 150..249 -> FontWeight.W200
140                 textFontWeight in 250..349 -> FontWeight.W300
141                 textFontWeight in 350..449 -> FontWeight.W400
142                 textFontWeight in 450..549 -> FontWeight.W500
143                 textFontWeight in 550..649 -> FontWeight.W600
144                 textFontWeight in 650..749 -> FontWeight.W700
145                 textFontWeight in 750..849 -> FontWeight.W800
146                 textFontWeight in 850..999 -> FontWeight.W900
147                 // Else, check the text style for bold
148                 (textStyle and Typeface.BOLD) != 0 -> FontWeight.Bold
149                 // Else, the font family might have an implicit weight (san-serif-light, etc)
150                 fontFamily != null -> fontFamily.weight
151                 else -> null
152             },
153             fontFeatureSettings = a.getString(R.styleable.ThemeAdapterTextAppearance_android_fontFeatureSettings),
154             shadow = run {
155                 val shadowColor = a.parseColor(R.styleable.ThemeAdapterTextAppearance_android_shadowColor)
156                 if (shadowColor != Color.Unspecified) {
157                     val dx = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowDx, 0f)
158                     val dy = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowDy, 0f)
159                     val rad = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowRadius, 0f)
160                     Shadow(color = shadowColor, offset = Offset(dx, dy), blurRadius = rad)
161                 } else null
162             },
163             letterSpacing = when {
164                 a.hasValue(R.styleable.ThemeAdapterTextAppearance_android_letterSpacing) -> {
165                     a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_letterSpacing, 0f).em
166                 }
167                 // FIXME: Normally we'd use TextUnit.Unspecified,
168                 // but this can cause a crash due to mismatched Sp and Em TextUnits
169                 // https://issuetracker.google.com/issues/182881244
170                 else -> 0.em
171             }
172         )
173     }
174 }
175 
176 /**
177  * Returns the given index as a [FontFamilyWithWeight], or `null` if the value can't be coerced to
178  * a [FontFamilyWithWeight].
179  *
180  * @param index Index of attribute to retrieve.
181  */
182 @Deprecated(
183     """
184    ThemeAdapter is deprecated.
185 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
186 """
187 )
parseFontFamilynull188 public fun TypedArray.parseFontFamily(index: Int): FontFamilyWithWeight? {
189     val tv = tempTypedValue.getOrSet(::TypedValue)
190     if (getValue(index, tv) && tv.type == TypedValue.TYPE_STRING) {
191         return when (tv.string) {
192             "sans-serif" -> FontFamilyWithWeight(FontFamily.SansSerif)
193             "sans-serif-thin" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Thin)
194             "sans-serif-light" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Light)
195             "sans-serif-medium" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Medium)
196             "sans-serif-black" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Black)
197             "serif" -> FontFamilyWithWeight(FontFamily.Serif)
198             "cursive" -> FontFamilyWithWeight(FontFamily.Cursive)
199             "monospace" -> FontFamilyWithWeight(FontFamily.Monospace)
200             // TODO: Compose does not expose a FontFamily for all strings yet
201             else -> {
202                 // If there's a resource ID and the string starts with res/,
203                 // it's probably a @font resource
204                 if (tv.resourceId != 0 && tv.string.startsWith("res/")) {
205                     // If we're running on API 23+ and the resource is an XML, we can parse
206                     // the fonts into a full FontFamily.
207                     if (Build.VERSION.SDK_INT >= 23 && tv.string.endsWith(".xml")) {
208                         resources.parseXmlFontFamily(tv.resourceId)?.let(::FontFamilyWithWeight)
209                     } else {
210                         // Otherwise we just load it as a single font
211                         FontFamilyWithWeight(Font(tv.resourceId).toFontFamily())
212                     }
213                 } else null
214             }
215         }
216     }
217     return null
218 }
219 
220 /**
221  * A lightweight class for storing a [FontFamily] and [FontWeight].
222  */
223 @Deprecated(
224     """
225    ThemeAdapter is deprecated.
226 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
227 """
228 )
229 public data class FontFamilyWithWeight(
230     val fontFamily: FontFamily,
231     val weight: FontWeight = FontWeight.Normal
232 )
233 
234 /**
235  * Returns the given XML resource ID as a [FontFamily], or `null` if the value can't be coerced to
236  * a [FontFamily].
237  *
238  * @param id ID of XML resource to retrieve.
239  */
240 @Deprecated(
241     """
242    ThemeAdapter is deprecated.
243 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
244 """
245 )
246 @SuppressLint("RestrictedApi") // FontResourcesParserCompat.*
247 @RequiresApi(23)
248 // XML font families with > 1 fonts are only supported on API 23+
parseXmlFontFamilynull249 public fun Resources.parseXmlFontFamily(id: Int): FontFamily? {
250     val parser = getXml(id)
251 
252     // Can't use {} since XmlResourceParser is AutoCloseable, not Closeable
253     @Suppress("ConvertTryFinallyToUseCall")
254     try {
255         val result = FontResourcesParserCompat.parse(parser, this)
256         if (result is FontResourcesParserCompat.FontFamilyFilesResourceEntry) {
257             val fonts = result.entries.map { font ->
258                 Font(
259                     resId = font.resourceId,
260                     weight = fontWeightOf(font.weight),
261                     style = if (font.isItalic) FontStyle.Italic else FontStyle.Normal
262                 )
263             }
264             return FontFamily(fonts)
265         }
266     } finally {
267         parser.close()
268     }
269     return null
270 }
271 
fontWeightOfnull272 private fun fontWeightOf(weight: Int): FontWeight = when (weight) {
273     in 0..149 -> FontWeight.W100
274     in 150..249 -> FontWeight.W200
275     in 250..349 -> FontWeight.W300
276     in 350..449 -> FontWeight.W400
277     in 450..549 -> FontWeight.W500
278     in 550..649 -> FontWeight.W600
279     in 650..749 -> FontWeight.W700
280     in 750..849 -> FontWeight.W800
281     in 850..999 -> FontWeight.W900
282     // Else, we use the 'normal' weight
283     else -> FontWeight.W400
284 }
285 
286 /**
287  * Returns the given index as a [TextUnit], or [fallbackTextUnit] if the value can't be coerced to
288  * a [TextUnit].
289  *
290  * @param index Index of attribute to retrieve.
291  * @param density The current display density.
292  * @param fallbackTextUnit Value to return if the attribute is not defined or can't be coerced to a
293  * [TextUnit].
294  */
295 @Deprecated(
296     """
297    ThemeAdapter is deprecated.
298 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
299 """
300 )
parseTextUnitnull301 public fun TypedArray.parseTextUnit(
302     index: Int,
303     density: Density,
304     fallbackTextUnit: TextUnit = TextUnit.Unspecified
305 ): TextUnit {
306     val tv = tempTypedValue.getOrSet { TypedValue() }
307     if (getValue(index, tv) && tv.type == TypedValue.TYPE_DIMENSION) {
308         return when (tv.complexUnitCompat) {
309             // For SP values, we convert the value directly to an TextUnit.Sp
310             TypedValue.COMPLEX_UNIT_SP -> TypedValue.complexToFloat(tv.data).sp
311             // For DIP values, we convert the value to an TextUnit.Em (roughly equivalent)
312             TypedValue.COMPLEX_UNIT_DIP -> TypedValue.complexToFloat(tv.data).em
313             // For another other types, we let the TypedArray flatten to a px value, and
314             // we convert it to an Sp based on the current density
315             else -> with(density) { getDimension(index, 0f).toSp() }
316         }
317     }
318     return fallbackTextUnit
319 }
320 
321 /**
322  * Returns the given style resource ID as a [CornerBasedShape], or [fallbackShape] if the value
323  * can't be coerced to a [CornerBasedShape].
324  *
325  * @param context The current context.
326  * @param id ID of style resource to retrieve.
327  * @param layoutDirection The current display layout direction.
328  * @param fallbackShape Value to return if the style resource ID is not defined or can't be coerced
329  * to a [CornerBasedShape].
330  */
331 @Deprecated(
332     """
333    ThemeAdapter is deprecated.
334 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
335 """
336 )
parseShapeAppearancenull337 public fun parseShapeAppearance(
338     context: Context,
339     @StyleRes id: Int,
340     layoutDirection: LayoutDirection,
341     fallbackShape: CornerBasedShape
342 ): CornerBasedShape {
343     return context.obtainStyledAttributes(id, R.styleable.ThemeAdapterShapeAppearance).use { a ->
344         val defaultCornerSize = a.parseCornerSize(
345             R.styleable.ThemeAdapterShapeAppearance_cornerSize
346         )
347         val cornerSizeTL = a.parseCornerSize(
348             R.styleable.ThemeAdapterShapeAppearance_cornerSizeTopLeft
349         )
350         val cornerSizeTR = a.parseCornerSize(
351             R.styleable.ThemeAdapterShapeAppearance_cornerSizeTopRight
352         )
353         val cornerSizeBL = a.parseCornerSize(
354             R.styleable.ThemeAdapterShapeAppearance_cornerSizeBottomLeft
355         )
356         val cornerSizeBR = a.parseCornerSize(
357             R.styleable.ThemeAdapterShapeAppearance_cornerSizeBottomRight
358         )
359         val isRtl = layoutDirection == LayoutDirection.Rtl
360         val cornerSizeTS = if (isRtl) cornerSizeTR else cornerSizeTL
361         val cornerSizeTE = if (isRtl) cornerSizeTL else cornerSizeTR
362         val cornerSizeBS = if (isRtl) cornerSizeBR else cornerSizeBL
363         val cornerSizeBE = if (isRtl) cornerSizeBL else cornerSizeBR
364 
365         /**
366          * We do not support the individual `cornerFamilyTopLeft`, etc, since Compose only supports
367          * one corner type per shape. Therefore we only read the `cornerFamily` attribute.
368          */
369         when (a.getInt(R.styleable.ThemeAdapterShapeAppearance_cornerFamily, 0)) {
370             0 -> {
371                 RoundedCornerShape(
372                     topStart = cornerSizeTS ?: defaultCornerSize ?: fallbackShape.topStart,
373                     topEnd = cornerSizeTE ?: defaultCornerSize ?: fallbackShape.topEnd,
374                     bottomEnd = cornerSizeBE ?: defaultCornerSize ?: fallbackShape.bottomEnd,
375                     bottomStart = cornerSizeBS ?: defaultCornerSize ?: fallbackShape.bottomStart
376                 )
377             }
378             1 -> {
379                 CutCornerShape(
380                     topStart = cornerSizeTS ?: defaultCornerSize ?: fallbackShape.topStart,
381                     topEnd = cornerSizeTE ?: defaultCornerSize ?: fallbackShape.topEnd,
382                     bottomEnd = cornerSizeBE ?: defaultCornerSize ?: fallbackShape.bottomEnd,
383                     bottomStart = cornerSizeBS ?: defaultCornerSize ?: fallbackShape.bottomStart
384                 )
385             }
386             else -> throw IllegalArgumentException("Unknown cornerFamily set in ShapeAppearance")
387         }
388     }
389 }
390 
391 /**
392  * Returns the given index as a [CornerSize], or `null` if the value can't be coerced to a
393  * [CornerSize].
394  *
395  * @param index Index of attribute to retrieve.
396  */
397 @Deprecated(
398     """
399    ThemeAdapter is deprecated.
400 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
401 """
402 )
parseCornerSizenull403 public fun TypedArray.parseCornerSize(index: Int): CornerSize? {
404     val tv = tempTypedValue.getOrSet { TypedValue() }
405     if (getValue(index, tv)) {
406         return when (tv.type) {
407             TypedValue.TYPE_DIMENSION -> {
408                 when (tv.complexUnitCompat) {
409                     // For DIP and PX values, we convert the value to the equivalent
410                     TypedValue.COMPLEX_UNIT_DIP -> CornerSize(TypedValue.complexToFloat(tv.data).dp)
411                     TypedValue.COMPLEX_UNIT_PX -> CornerSize(TypedValue.complexToFloat(tv.data))
412                     // For another other dim types, we let the TypedArray flatten to a px value
413                     else -> CornerSize(getDimensionPixelSize(index, 0))
414                 }
415             }
416             TypedValue.TYPE_FRACTION -> CornerSize(tv.getFraction(1f, 1f))
417             else -> null
418         }
419     }
420     return null
421 }
422 
423 /**
424  * A workaround since [TypedValue.getComplexUnit] is API 22+
425  */
426 private inline val TypedValue.complexUnitCompat
427     get() = when {
428         Build.VERSION.SDK_INT > 22 -> complexUnit
429         else -> TypedValue.COMPLEX_UNIT_MASK and (data shr TypedValue.COMPLEX_UNIT_SHIFT)
430     }
431 
432 private val tempTypedValue = ThreadLocal<TypedValue>()
433