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.compose.ui.text.font
18 
19 import androidx.collection.LruCache
20 import androidx.compose.runtime.State
21 import androidx.compose.ui.text.platform.makeSynchronizedObject
22 import androidx.compose.ui.text.platform.synchronized
23 import androidx.compose.ui.util.fastMap
24 
25 internal class FontFamilyResolverImpl(
26     internal val platformFontLoader: PlatformFontLoader /* exposed for desktop ParagraphBuilder */,
27     private val platformResolveInterceptor: PlatformResolveInterceptor =
28         PlatformResolveInterceptor.Default,
29     private val typefaceRequestCache: TypefaceRequestCache = GlobalTypefaceRequestCache,
30     private val fontListFontFamilyTypefaceAdapter: FontListFontFamilyTypefaceAdapter =
31         FontListFontFamilyTypefaceAdapter(GlobalAsyncTypefaceCache),
32     private val platformFamilyTypefaceAdapter: PlatformFontFamilyTypefaceAdapter =
33         PlatformFontFamilyTypefaceAdapter()
34 ) : FontFamily.Resolver {
35     private val createDefaultTypeface: (TypefaceRequest) -> Any = {
36         resolve(it.copy(fontFamily = null)).value
37     }
38 
39     override suspend fun preload(fontFamily: FontFamily) {
40         // all other types of FontFamily are already preloaded.
41         if (fontFamily !is FontListFontFamily) return
42 
43         fontListFontFamilyTypefaceAdapter.preload(fontFamily, platformFontLoader)
44 
45         val typeRequests =
46             fontFamily.fonts.fastMap {
47                 TypefaceRequest(
48                     platformResolveInterceptor.interceptFontFamily(fontFamily),
49                     platformResolveInterceptor.interceptFontWeight(it.weight),
50                     platformResolveInterceptor.interceptFontStyle(it.style),
51                     FontSynthesis.All,
52                     platformFontLoader.cacheKey
53                 )
54             }
55 
56         typefaceRequestCache.preWarmCache(typeRequests) { typeRequest ->
57             @Suppress("MoveLambdaOutsideParentheses")
58             fontListFontFamilyTypefaceAdapter.resolve(
59                 typefaceRequest = typeRequest,
60                 platformFontLoader = platformFontLoader,
61                 onAsyncCompletion = { /* nothing */ },
62                 createDefaultTypeface = createDefaultTypeface
63             )
64                 ?: platformFamilyTypefaceAdapter.resolve(
65                     typefaceRequest = typeRequest,
66                     platformFontLoader = platformFontLoader,
67                     onAsyncCompletion = { /* nothing */ },
68                     createDefaultTypeface = createDefaultTypeface
69                 )
70                 ?: throw IllegalStateException("Could not load font")
71         }
72     }
73 
74     override fun resolve(
75         fontFamily: FontFamily?,
76         fontWeight: FontWeight,
77         fontStyle: FontStyle,
78         fontSynthesis: FontSynthesis,
79     ): State<Any> {
80         return resolve(
81             TypefaceRequest(
82                 platformResolveInterceptor.interceptFontFamily(fontFamily),
83                 platformResolveInterceptor.interceptFontWeight(fontWeight),
84                 platformResolveInterceptor.interceptFontStyle(fontStyle),
85                 platformResolveInterceptor.interceptFontSynthesis(fontSynthesis),
86                 platformFontLoader.cacheKey
87             )
88         )
89     }
90 
91     /** Resolves the final [typefaceRequest] without interceptors. */
92     private fun resolve(typefaceRequest: TypefaceRequest): State<Any> {
93         val result =
94             typefaceRequestCache.runCached(typefaceRequest) { onAsyncCompletion ->
95                 fontListFontFamilyTypefaceAdapter.resolve(
96                     typefaceRequest,
97                     platformFontLoader,
98                     onAsyncCompletion,
99                     createDefaultTypeface
100                 )
101                     ?: platformFamilyTypefaceAdapter.resolve(
102                         typefaceRequest,
103                         platformFontLoader,
104                         onAsyncCompletion,
105                         createDefaultTypeface
106                     )
107                     ?: throw IllegalStateException("Could not load font")
108             }
109         return result
110     }
111 }
112 
113 /**
114  * Platform level [FontFamily.Resolver] argument interceptor. This interface is intended to bridge
115  * accessibility constraints on any platform with Compose through the use of
116  * [FontFamilyResolverImpl.resolve].
117  */
118 internal interface PlatformResolveInterceptor {
119 
interceptFontFamilynull120     fun interceptFontFamily(fontFamily: FontFamily?): FontFamily? = fontFamily
121 
122     fun interceptFontWeight(fontWeight: FontWeight): FontWeight = fontWeight
123 
124     fun interceptFontStyle(fontStyle: FontStyle): FontStyle = fontStyle
125 
126     fun interceptFontSynthesis(fontSynthesis: FontSynthesis): FontSynthesis = fontSynthesis
127 
128     companion object {
129         // NO-OP default interceptor
130         internal val Default: PlatformResolveInterceptor = object : PlatformResolveInterceptor {}
131     }
132 }
133 
134 internal val GlobalTypefaceRequestCache = TypefaceRequestCache()
135 internal val GlobalAsyncTypefaceCache = AsyncTypefaceCache()
136 
137 internal expect class PlatformFontFamilyTypefaceAdapter() : FontFamilyTypefaceAdapter {
resolvenull138     override fun resolve(
139         typefaceRequest: TypefaceRequest,
140         platformFontLoader: PlatformFontLoader,
141         onAsyncCompletion: (TypefaceResult.Immutable) -> Unit,
142         createDefaultTypeface: (TypefaceRequest) -> Any
143     ): TypefaceResult?
144 }
145 
146 internal data class TypefaceRequest(
147     val fontFamily: FontFamily?,
148     val fontWeight: FontWeight,
149     val fontStyle: FontStyle,
150     val fontSynthesis: FontSynthesis,
151     val resourceLoaderCacheKey: Any?
152 )
153 
154 internal sealed interface TypefaceResult : State<Any> {
155     val cacheable: Boolean
156 
157     // Immutable results present as State, but don't trigger a read observer
158     class Immutable(override val value: Any, override val cacheable: Boolean = true) :
159         TypefaceResult
160 
161     class Async(internal val current: AsyncFontListLoader) : TypefaceResult, State<Any> by current {
162         override val cacheable: Boolean
163             get() = current.cacheable
164     }
165 }
166 
167 internal class TypefaceRequestCache {
168     internal val lock = makeSynchronizedObject()
169     // @GuardedBy("lock")
170     private val resultCache = LruCache<TypefaceRequest, TypefaceResult>(16)
171 
runCachednull172     fun runCached(
173         typefaceRequest: TypefaceRequest,
174         resolveTypeface: ((TypefaceResult) -> Unit) -> TypefaceResult
175     ): State<Any> {
176         synchronized(lock) {
177             resultCache[typefaceRequest]?.let {
178                 if (it.cacheable) {
179                     return it
180                 } else {
181                     resultCache.remove(typefaceRequest)
182                 }
183             }
184         }
185         // this is not run synchronized2 as it incurs expected file system reads.
186         //
187         // As a result, it is possible the same FontFamily resolution is started twice if this
188         // function is entered concurrently. This is explicitly allowed, to avoid creating a global
189         // lock here.
190         //
191         // This function must ensure that the final result is a valid cache in the presence of
192         // multiple entries.
193         //
194         // Necessary font load de-duping is the responsibility of actual font resolution mechanisms.
195         val currentTypefaceResult =
196             try {
197                 resolveTypeface { finalResult ->
198                     // may this after runCached returns, or immediately if the typeface is
199                     // immediately
200                     // available without dispatch
201 
202                     // this converts an async (state) result to an immutable (val) result to
203                     // optimize
204                     // future lookups
205                     synchronized(lock) {
206                         if (finalResult.cacheable) {
207                             resultCache.put(typefaceRequest, finalResult)
208                         } else {
209                             resultCache.remove(typefaceRequest)
210                         }
211                     }
212                 }
213             } catch (cause: Exception) {
214                 throw IllegalStateException("Could not load font", cause)
215             }
216         synchronized(lock) {
217             // async result may have completed prior to this block entering, do not overwrite
218             // final results
219             if (resultCache[typefaceRequest] == null && currentTypefaceResult.cacheable) {
220                 resultCache.put(typefaceRequest, currentTypefaceResult)
221             }
222         }
223         return currentTypefaceResult
224     }
225 
preWarmCachenull226     fun preWarmCache(
227         typefaceRequests: List<TypefaceRequest>,
228         resolveTypeface: (TypefaceRequest) -> TypefaceResult
229     ) {
230         for (i in typefaceRequests.indices) {
231             val typeRequest = typefaceRequests[i]
232 
233             val prior = synchronized(lock) { resultCache[typeRequest] }
234             if (prior != null) continue
235 
236             val next =
237                 try {
238                     resolveTypeface(typeRequest)
239                 } catch (cause: Exception) {
240                     throw IllegalStateException("Could not load font", cause)
241                 }
242 
243             // only cache immutable, should not reach as FontListFontFamilyTypefaceAdapter already
244             // has async fonts in permanent cache
245             if (next is TypefaceResult.Async) continue
246 
247             synchronized(lock) { resultCache.put(typeRequest, next) }
248         }
249     }
250 
251     // @VisibleForTesting
getnull252     internal fun get(typefaceRequest: TypefaceRequest) =
253         synchronized(lock) { resultCache.get(typefaceRequest) }
254 
255     // @VisibleForTesting
256     internal val size: Int
<lambda>null257         get() = synchronized(lock) { resultCache.size() }
258 }
259