1 /*
<lambda>null2  * Copyright 2020 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.compose.ui.text.platform
18 
19 import android.graphics.Typeface
20 import android.os.Build
21 import android.text.SpannableString
22 import android.text.Spanned
23 import android.text.style.ScaleXSpan
24 import android.text.style.StrikethroughSpan
25 import android.text.style.StyleSpan
26 import android.text.style.TypefaceSpan
27 import android.text.style.UnderlineSpan
28 import androidx.annotation.RequiresApi
29 import androidx.annotation.RestrictTo
30 import androidx.compose.ui.text.AnnotatedString
31 import androidx.compose.ui.text.ExperimentalTextApi
32 import androidx.compose.ui.text.InternalTextApi
33 import androidx.compose.ui.text.LinkAnnotation
34 import androidx.compose.ui.text.SpanStyle
35 import androidx.compose.ui.text.font.FontFamily
36 import androidx.compose.ui.text.font.FontStyle
37 import androidx.compose.ui.text.font.FontSynthesis
38 import androidx.compose.ui.text.font.FontWeight
39 import androidx.compose.ui.text.font.GenericFontFamily
40 import androidx.compose.ui.text.font.getAndroidTypefaceStyle
41 import androidx.compose.ui.text.platform.extensions.setBackground
42 import androidx.compose.ui.text.platform.extensions.setColor
43 import androidx.compose.ui.text.platform.extensions.setFontSize
44 import androidx.compose.ui.text.platform.extensions.setLocaleList
45 import androidx.compose.ui.text.platform.extensions.toSpan
46 import androidx.compose.ui.text.style.TextDecoration
47 import androidx.compose.ui.unit.Density
48 import androidx.compose.ui.util.fastForEach
49 
50 @OptIn(ExperimentalTextApi::class)
51 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
52 @InternalTextApi // used in ui:ui
53 fun AnnotatedString.toAccessibilitySpannableString(
54     density: Density,
55     fontFamilyResolver: FontFamily.Resolver,
56     urlSpanCache: URLSpanCache,
57 ): SpannableString {
58     val spannableString = SpannableString(text)
59     spanStylesOrNull?.fastForEach { (style, start, end) ->
60         // b/232238615 looking up fonts inside of accessibility does not honor overwritten
61         // FontFamilyResolver. This is not safe until Font.ResourceLoader is fully removed.
62         val noFontStyle = style.copy(fontFamily = null)
63         spannableString.setSpanStyle(noFontStyle, start, end, density, fontFamilyResolver)
64     }
65 
66     getTtsAnnotations(0, length).fastForEach { (ttsAnnotation, start, end) ->
67         spannableString.setSpan(
68             ttsAnnotation.toSpan(),
69             start,
70             end,
71             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
72         )
73     }
74 
75     @Suppress("Deprecation")
76     getUrlAnnotations(0, length).fastForEach { (urlAnnotation, start, end) ->
77         spannableString.setSpan(
78             urlSpanCache.toURLSpan(urlAnnotation),
79             start,
80             end,
81             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
82         )
83     }
84 
85     getLinkAnnotations(0, length).fastForEach { linkRange ->
86         if (linkRange.start != linkRange.end) {
87             val link = linkRange.item
88             if (link is LinkAnnotation.Url && link.linkInteractionListener == null) {
89                 spannableString.setSpan(
90                     urlSpanCache.toURLSpan(linkRange.toUrlLink()),
91                     linkRange.start,
92                     linkRange.end,
93                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
94                 )
95             } else {
96                 spannableString.setSpan(
97                     urlSpanCache.toClickableSpan(linkRange),
98                     linkRange.start,
99                     linkRange.end,
100                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
101                 )
102             }
103         }
104     }
105     return spannableString
106 }
107 
108 /** Apply the serializable styles to SpannableString. */
SpannableStringnull109 private fun SpannableString.setSpanStyle(
110     spanStyle: SpanStyle,
111     start: Int,
112     end: Int,
113     density: Density,
114     fontFamilyResolver: FontFamily.Resolver
115 ) {
116     setColor(spanStyle.color, start, end)
117 
118     setFontSize(spanStyle.fontSize, density, start, end)
119 
120     if (spanStyle.fontWeight != null || spanStyle.fontStyle != null) {
121         // If current typeface is bold, StyleSpan won't change it to normal. The same applies to
122         // font style, so use normal as default works here.
123         // This is also a bug in framework span. But we can't find a good solution so far.
124         val fontWeight = spanStyle.fontWeight ?: FontWeight.Normal
125         val fontStyle = spanStyle.fontStyle ?: FontStyle.Normal
126         setSpan(
127             StyleSpan(getAndroidTypefaceStyle(fontWeight, fontStyle)),
128             start,
129             end,
130             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
131         )
132     }
133 
134     // TypefaceSpan accepts Typeface as parameter only after P. And only font family string can be
135     // pass to other thread.
136     // Here we try to create TypefaceSpan with font family string if possible.
137     if (spanStyle.fontFamily != null) {
138         if (spanStyle.fontFamily is GenericFontFamily) {
139             setSpan(
140                 TypefaceSpan(spanStyle.fontFamily.name),
141                 start,
142                 end,
143                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
144             )
145         } else {
146             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
147                 // TODO(b/214587005): Check for async here and uncache
148                 val typeface =
149                     fontFamilyResolver
150                         .resolve(
151                             fontFamily = spanStyle.fontFamily,
152                             fontSynthesis = spanStyle.fontSynthesis ?: FontSynthesis.All
153                         )
154                         .value as Typeface
155                 setSpan(
156                     Api28Impl.createTypefaceSpan(typeface),
157                     start,
158                     end,
159                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
160                 )
161             }
162         }
163     }
164 
165     if (spanStyle.textDecoration != null) {
166         // This doesn't match how we rendering the styles. When TextDecoration.None is set, it
167         // should remove the underline and lineThrough effect on the given range. Here we didn't
168         // remove any previously applied spans yet.
169         if (TextDecoration.Underline in spanStyle.textDecoration) {
170             setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
171         }
172         if (TextDecoration.LineThrough in spanStyle.textDecoration) {
173             setSpan(StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
174         }
175     }
176 
177     if (spanStyle.textGeometricTransform != null) {
178         setSpan(
179             ScaleXSpan(spanStyle.textGeometricTransform.scaleX),
180             start,
181             end,
182             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
183         )
184     }
185 
186     setLocaleList(spanStyle.localeList, start, end)
187 
188     setBackground(spanStyle.background, start, end)
189 }
190 
191 @RequiresApi(28)
192 private object Api28Impl {
createTypefaceSpannull193     fun createTypefaceSpan(typeface: Typeface): TypefaceSpan = TypefaceSpan(typeface)
194 }
195 
196 private fun AnnotatedString.Range<LinkAnnotation>.toUrlLink() =
197     AnnotatedString.Range(this.item as LinkAnnotation.Url, this.start, this.end)
198