AndroidPreloadedFont.kt

/*
 * 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.
 */

package androidx.compose.ui.text.font

import android.content.Context
import android.content.res.AssetManager
import android.graphics.Typeface
import android.graphics.fonts.FontVariationAxis
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastMap
import java.io.File

internal sealed class AndroidPreloadedFont constructor(
    final override val weight: FontWeight,
    final override val style: FontStyle,
    variationSettings: FontVariation.Settings
) : AndroidFont(
    FontLoadingStrategy.Blocking,
    AndroidPreloadedFontTypefaceLoader,
    variationSettings
) {
    abstract val cacheKey: String?
    internal abstract fun doLoad(context: Context?): Typeface?

    private var didInitWithContext: Boolean = false
    // subclasses MUST initialize this by calling doLoad(null) - after overriding doLoad as final
    internal var typeface: Typeface? = null

    internal fun loadCached(context: Context): Typeface? {
        if (!didInitWithContext && typeface == null) {
            typeface = doLoad(context)
        }
        didInitWithContext = true
        return typeface
    }
}

private object AndroidPreloadedFontTypefaceLoader : AndroidFont.TypefaceLoader {
    override fun loadBlocking(context: Context, font: AndroidFont): Typeface? =
        (font as? AndroidPreloadedFont)?.loadCached(context)

    override suspend fun awaitLoad(context: Context, font: AndroidFont): Nothing {
        throw UnsupportedOperationException("All preloaded fonts are blocking.")
    }
}

@OptIn(ExperimentalTextApi::class) /* FontVariation.Settings */
internal class AndroidAssetFont constructor(
    val assetManager: AssetManager,
    val path: String,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromAssets(assetManager, path, context, variationSettings)
        } else {
            Typeface.createFromAsset(assetManager, path)
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String = "asset:$path"

    override fun toString(): String {
        return "Font(assetManager, path=$path, weight=$weight, style=$style)"
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is AndroidAssetFont) return false

        if (path != other.path) return false
        if (variationSettings != other.variationSettings) return false

        return true
    }

    override fun hashCode(): Int {
        var result = path.hashCode()
        result = 31 * result + variationSettings.hashCode()
        return result
    }
}

@OptIn(ExperimentalTextApi::class)
internal class AndroidFileFont constructor(
    val file: File,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromFile(file, context, variationSettings)
        } else {
            Typeface.createFromFile(file)
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String? = null
    override fun toString(): String {
        return "Font(file=$file, weight=$weight, style=$style)"
    }
}

@RequiresApi(26)
@OptIn(ExperimentalTextApi::class)
internal class AndroidFileDescriptorFont constructor(
    val fileDescriptor: ParcelFileDescriptor,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromFileDescriptor(
                fileDescriptor,
                context,
                variationSettings
            )
        } else {
            throw IllegalArgumentException("Cannot create font from file descriptor for SDK < 26")
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String? = null
    override fun toString(): String {
        return "Font(fileDescriptor=$fileDescriptor, weight=$weight, style=$style)"
    }
}

@RequiresApi(api = 26)
private object TypefaceBuilderCompat {
    @ExperimentalTextApi
    @DoNotInline
    fun createFromAssets(
        assetManager: AssetManager,
        path: String,
        context: Context?,
        variationSettings: FontVariation.Settings
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(assetManager, path)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @ExperimentalTextApi
    @DoNotInline
    fun createFromFile(
        file: File,
        context: Context?,
        variationSettings: FontVariation.Settings
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(file)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @ExperimentalTextApi
    @DoNotInline
    fun createFromFileDescriptor(
        fileDescriptor: ParcelFileDescriptor,
        context: Context?,
        variationSettings: FontVariation.Settings,
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(fileDescriptor.fileDescriptor)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    @ExperimentalTextApi
    private fun FontVariation.Settings.toVariationSettings(
        context: Context?
    ): Array<FontVariationAxis> {
        val density = if (context != null) {
            Density(context)
        } else if (!needsDensity) {
            // we don't need density, so make a fake one and be on with it
            Density(1f, 1f)
        } else {
            // cannot reach
            throw IllegalStateException("Required density, but not provided")
        }
        return settings.fastMap { setting ->
            FontVariationAxis(setting.axisName, setting.toVariationValue(density))
        }.toTypedArray()
    }
}