| /* |
| * Copyright 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // this file provides integration with fonts.google.com, which is called Google Fonts |
| @file:Suppress("MentionsGoogle") |
| |
| package androidx.compose.ui.text.googlefonts |
| |
| import android.content.Context |
| import android.graphics.Typeface |
| import android.os.Build |
| import android.os.Handler |
| import android.os.Looper |
| import androidx.annotation.ArrayRes |
| import androidx.annotation.WorkerThread |
| import androidx.compose.ui.text.font.AndroidFont |
| import androidx.compose.ui.text.font.Font |
| import androidx.compose.ui.text.font.FontLoadingStrategy |
| import androidx.compose.ui.text.font.FontStyle |
| import androidx.compose.ui.text.font.FontVariation |
| import androidx.compose.ui.text.font.FontWeight |
| import androidx.core.provider.FontRequest |
| import androidx.core.provider.FontsContractCompat |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_UNAVAILABLE |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_MALFORMED_QUERY |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES |
| import androidx.core.provider.FontsContractCompat.FontRequestCallback.FontRequestFailReason |
| import java.net.URLEncoder |
| import kotlin.coroutines.resume |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| |
| /** |
| * Load a font from Google Fonts via Downloadable Fonts. |
| * |
| * To learn more about the features supported by Google Fonts, see |
| * [Get Started with the Google Fonts for Android](https://developers.google.com/fonts/docs/android) |
| * |
| * @param googleFont A font to load from fonts.google.com |
| * @param fontProvider configuration for downloadable font provider |
| * @param weight font weight to load |
| * @param style italic or normal font |
| */ |
| // contains Google in name because this function provides integration with fonts.google.com |
| @Suppress("MentionsGoogle") |
| fun Font( |
| googleFont: GoogleFont, |
| fontProvider: GoogleFont.Provider, |
| weight: FontWeight = FontWeight.W400, |
| style: FontStyle = FontStyle.Normal |
| ): Font { |
| return GoogleFontImpl( |
| name = googleFont.name, |
| fontProvider = fontProvider, |
| weight = weight, |
| style = style, |
| bestEffort = googleFont.bestEffort |
| ) |
| } |
| |
| /** |
| * A downloadable font from fonts.google.com |
| * |
| * To learn more about the features supported by Google Fonts, see |
| * [Get Started with the Google Fonts for Android](https://developers.google.com/fonts/docs/android) |
| * |
| * For a full list of fonts available on Android, see the |
| * [Google Fonts Directory For Android XML](https://fonts.gstatic.com/s/a/directory.xml). |
| * |
| * @param name Name of a font on Google fonts, such as "Roboto" or "Open Sans" |
| * @param bestEffort If besteffort is true and your query specifies a valid family name but the |
| * requested width/weight/italic value is not supported Google Fonts will return the best match it |
| * can find within the family. If false, exact matches will be returned only. |
| * |
| * @throws IllegalArgumentException if name is empty |
| */ |
| // contains Google in name because this function provides integration with fonts.google.com |
| @Suppress("MentionsGoogle") |
| class GoogleFont(val name: String, val bestEffort: Boolean = true) { |
| init { |
| require(name.isNotEmpty()) { "name cannot be empty" } |
| } |
| |
| /** |
| * Attributes used to create a [FontRequest] for a [GoogleFont] based [Font]. |
| * |
| * @see FontRequest |
| */ |
| // contains Google in name because this function provides integration with fonts.google.com |
| @Suppress("MentionsGoogle") |
| class Provider private constructor( |
| internal val providerAuthority: String, |
| internal val providerPackage: String, |
| internal val certificates: List<List<ByteArray>>?, |
| @ArrayRes internal val certificatesRes: Int |
| ) { |
| |
| /** |
| * Describe a downloadable fonts provider using a list of certificates. |
| * |
| * The font provider is matched by `providerAuthority` and `packageName`, then the resulting |
| * provider has it's certificates validated against `certificates`. |
| * |
| * If the certificates check success, the provider is used for downloadable fonts. |
| * |
| * If the certificates check fails, the provider will not be used and any downloadable fonts |
| * requests configured with it will fail. |
| * |
| * @param providerAuthority The authority of the Font Provider to be used for the request. |
| * @param providerPackage The package for the Font Provider to be used for the request. This |
| * is used to verify the identity of the provider. |
| * @param certificates The list of sets of hashes for the certificates the provider should |
| * be signed with. This is used to verify the identity of the provider. Each set in the |
| * list represents one collection of signature hashes. Refer to your font provider's |
| * documentation for these values. |
| */ |
| constructor( |
| providerAuthority: String, |
| providerPackage: String, |
| certificates: List<List<ByteArray>> |
| ) : this(providerAuthority, providerPackage, certificates, 0) |
| |
| /** |
| * Describe a downloadable fonts provider using a resource array for certificates. |
| * |
| * The font provider is matched by `providerAuthority` and `packageName`, then the resulting |
| * provider has it's certificates validated against `certificates`. |
| * |
| * If the certificates check success, the provider is used for downloadable fonts. |
| * |
| * If the certificates check fails, the provider will not be used and any downloadable fonts |
| * requests configured with it will fail. |
| * |
| * @param providerAuthority The authority of the Font Provider to be used for the request. |
| * @param providerPackage The package for the Font Provider to be used for the request. This |
| * is used to verify the identity of the provider. |
| * @param certificates A resource array with the list of sets of hashes for the certificates |
| * the provider should be signed with. This is used to verify the identity of the provider. |
| * Each set in the list represents one collection of signature hashes. Refer to your |
| * font provider's documentation for these values. |
| */ |
| constructor( |
| providerAuthority: String, |
| providerPackage: String, |
| @ArrayRes certificates: Int |
| ) : this(providerAuthority, providerPackage, null, certificates) |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other !is Provider) return false |
| |
| if (providerAuthority != other.providerAuthority) return false |
| if (providerPackage != other.providerPackage) return false |
| if (certificates != other.certificates) return false |
| if (certificatesRes != other.certificatesRes) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = providerAuthority.hashCode() |
| result = 31 * result + providerPackage.hashCode() |
| result = 31 * result + (certificates?.hashCode() ?: 0) |
| result = 31 * result + certificatesRes |
| return result |
| } |
| } |
| } |
| |
| /** |
| * Check if the downloadable fonts provider is available on device. |
| * |
| * This is not necessary for normal usage, but may be useful in debugging downloadable fonts |
| * behavior. |
| * |
| * @param context for looking up font provider in |
| * @return true if the provider is usable for downloadable fonts, false if it's not found |
| * @throws IllegalStateException if the provider is on device, but certificates don't match |
| */ |
| @WorkerThread |
| fun GoogleFont.Provider.isAvailableOnDevice( |
| @Suppress("ContextFirst") context: Context, // extension function |
| ): Boolean = checkAvailable(context.packageManager, context.resources) |
| |
| internal data class GoogleFontImpl constructor( |
| val name: String, |
| private val fontProvider: GoogleFont.Provider, |
| override val weight: FontWeight, |
| override val style: FontStyle, |
| val bestEffort: Boolean |
| ) : AndroidFont(FontLoadingStrategy.Async, GoogleFontTypefaceLoader, FontVariation.Settings()) { |
| fun toFontRequest(): FontRequest { |
| // note: name is not encoded or quoted per spec |
| val query = "name=$name&weight=${weight.weight}" + |
| "&italic=${style.toQueryParam()}&besteffort=${bestEffortQueryParam()}" |
| |
| val certs = fontProvider.certificates |
| return if (certs != null) { |
| FontRequest( |
| fontProvider.providerAuthority, |
| fontProvider.providerPackage, |
| query, |
| certs |
| ) |
| } else { |
| FontRequest( |
| fontProvider.providerAuthority, |
| fontProvider.providerPackage, |
| query, |
| fontProvider.certificatesRes |
| ) |
| } |
| } |
| |
| private fun bestEffortQueryParam() = if (bestEffort) "true" else "false" |
| |
| private fun FontStyle.toQueryParam(): Int = if (this == FontStyle.Italic) 1 else 0 |
| private fun String.encode() = URLEncoder.encode(this, "UTF-8") |
| fun toTypefaceStyle(): Int { |
| val isItalic = style == FontStyle.Italic |
| val isBold = weight >= FontWeight.Bold |
| return when { |
| isItalic && isBold -> Typeface.BOLD_ITALIC |
| isItalic -> Typeface.ITALIC |
| isBold -> Typeface.BOLD |
| else -> Typeface.NORMAL |
| } |
| } |
| |
| override fun toString(): String { |
| return "Font(GoogleFont(\"$name\", bestEffort=$bestEffort), weight=$weight, " + |
| "style=$style)" |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other !is GoogleFontImpl) return false |
| |
| if (name != other.name) return false |
| if (fontProvider != other.fontProvider) return false |
| if (weight != other.weight) return false |
| if (style != other.style) return false |
| if (bestEffort != other.bestEffort) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = name.hashCode() |
| result = 31 * result + fontProvider.hashCode() |
| result = 31 * result + weight.hashCode() |
| result = 31 * result + style.hashCode() |
| result = 31 * result + bestEffort.hashCode() |
| return result |
| } |
| } |
| |
| internal object GoogleFontTypefaceLoader : AndroidFont.TypefaceLoader { |
| override fun loadBlocking(context: Context, font: AndroidFont): Typeface? { |
| error("GoogleFont only support async loading: $font") |
| } |
| override suspend fun awaitLoad(context: Context, font: AndroidFont): Typeface? { |
| return awaitLoad(context, font, DefaultFontsContractCompatLoader) |
| } |
| |
| internal suspend fun awaitLoad( |
| context: Context, |
| font: AndroidFont, |
| loader: FontsContractCompatLoader |
| ): Typeface? { |
| require(font is GoogleFontImpl) { "Only GoogleFontImpl supported (actual $font)" } |
| val fontRequest = font.toFontRequest() |
| val typefaceStyle = font.toTypefaceStyle() |
| |
| return suspendCancellableCoroutine { continuation -> |
| val callback = object : FontRequestCallback() { |
| override fun onTypefaceRetrieved(typeface: Typeface?) { |
| continuation.resume(typeface.recreateWithStyle(typefaceStyle)) |
| } |
| |
| override fun onTypefaceRequestFailed(reason: Int) { |
| // this is entered from any thread |
| continuation.cancel( |
| IllegalStateException("Failed to load $font (reason=$reason, " + |
| "${reasonToString(reason)})") |
| ) |
| } |
| } |
| |
| loader.requestFont( |
| context = context, |
| fontRequest = fontRequest, |
| typefaceStyle = typefaceStyle, |
| handler = asyncHandlerForCurrentThreadOrMainIfNoLooper(), |
| callback = callback |
| ) |
| } |
| } |
| |
| private fun asyncHandlerForCurrentThreadOrMainIfNoLooper(): Handler { |
| val looper = Looper.myLooper() ?: Looper.getMainLooper() |
| return HandlerHelper.createAsync(looper) |
| } |
| } |
| |
| /** |
| * Basically Typeface.create(this, typefaceStyle). |
| * |
| * Can be called from any thread. |
| */ |
| private fun Typeface?.recreateWithStyle(typefaceStyle: Int): Typeface { |
| return if (Build.VERSION.SDK_INT >= 28) { |
| // typeface create is thread safe |
| return Typeface.create(this, typefaceStyle) |
| } else { |
| // typeface.create has a timing condition |
| synchronized(GoogleFontTypefaceLoader) { |
| Typeface.create(this, typefaceStyle) |
| } |
| } |
| } |
| |
| /** |
| * To allow mocking for tests |
| */ |
| internal interface FontsContractCompatLoader { |
| fun requestFont( |
| context: Context, |
| fontRequest: FontRequest, |
| typefaceStyle: Int, |
| handler: Handler, |
| callback: FontRequestCallback |
| ) |
| } |
| |
| /** |
| * Actual implementation of requestFont using androidx.core |
| */ |
| private object DefaultFontsContractCompatLoader : FontsContractCompatLoader { |
| override fun requestFont( |
| context: Context, |
| fontRequest: FontRequest, |
| typefaceStyle: Int, |
| handler: Handler, |
| callback: FontRequestCallback |
| ) { |
| FontsContractCompat.requestFont( |
| context, |
| fontRequest, |
| callback, |
| handler |
| ) |
| } |
| } |
| |
| private fun reasonToString(@FontRequestFailReason reasonCode: Int): String { |
| return when (reasonCode) { |
| FAIL_REASON_PROVIDER_NOT_FOUND -> "The requested provider was not found on this device." |
| FAIL_REASON_WRONG_CERTIFICATES -> "The given provider cannot be authenticated with the " + |
| "certificates given." |
| FAIL_REASON_FONT_LOAD_ERROR -> "Generic error loading font, for example variation " + |
| "settings were not parsable" |
| FAIL_REASON_FONT_NOT_FOUND -> "Font not found, please check availability on " + |
| "GoogleFont.Provider.AllFontsList: https://fonts.gstatic.com/s/a/directory.xml" |
| FAIL_REASON_FONT_UNAVAILABLE -> "The provider found the queried font, but it is " + |
| "currently unavailable." |
| FAIL_REASON_MALFORMED_QUERY -> "The given query was not supported by this provider." |
| FAIL_REASON_SECURITY_VIOLATION -> "Font was not loaded due to security issues. This " + |
| "usually means the font was attempted to load in a restricted context" |
| else -> "Unknown error code" |
| } |
| } |