diff --git a/build.gradle b/build.gradle index 6497b8abf2..1f756798bb 100644 --- a/build.gradle +++ b/build.gradle @@ -217,6 +217,9 @@ android { aidl.srcDirs = ['lawnchair/aidl'] res.srcDirs = ['lawnchair/res'] manifest.srcFile "lawnchair/AndroidManifest.xml" + assets { + srcDirs 'lawnchair/assets' + } } lawnWithoutQuickstep { diff --git a/lawnchair/AndroidManifest.xml b/lawnchair/AndroidManifest.xml index 8bc1421416..39dcb39f7b 100644 --- a/lawnchair/AndroidManifest.xml +++ b/lawnchair/AndroidManifest.xml @@ -18,6 +18,10 @@ --> + + + + + + + + + + + + + + + + + + diff --git a/lawnchair/res/values/id.xml b/lawnchair/res/values/id.xml new file mode 100644 index 0000000000..0919d0c967 --- /dev/null +++ b/lawnchair/res/values/id.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index c9d4afac82..c6875fb4e2 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -119,4 +119,18 @@ To use Notification Dots, grant %1$s access to notifications. Change Settings Restart Lawnchair + + Font not found. + Italic + Thin + Extralight + + Light + Regular + Medium + Semibold + + Bold + Extrabold + Black diff --git a/lawnchair/src/app/lawnchair/font/FontCache.kt b/lawnchair/src/app/lawnchair/font/FontCache.kt new file mode 100644 index 0000000000..812a1d7c63 --- /dev/null +++ b/lawnchair/src/app/lawnchair/font/FontCache.kt @@ -0,0 +1,378 @@ +/* + * This file is part of Lawnchair Launcher. + * + * Lawnchair Launcher is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lawnchair Launcher is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Lawnchair Launcher. If not, see . + */ + +package app.lawnchair.font + +import android.content.Context +import android.content.res.AssetManager +import android.graphics.Typeface +import android.net.Uri +import androidx.annotation.Keep +import androidx.core.provider.FontRequest +import androidx.core.provider.FontsContractCompat +import app.lawnchair.font.googlefonts.GoogleFontsListing +import app.lawnchair.util.uiHelperHandler +import com.android.launcher3.R +import com.android.launcher3.util.MainThreadInitializedObject +import kotlinx.coroutines.* +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.lang.Exception +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class FontCache private constructor(private val context: Context) { + + private val scope = CoroutineScope(CoroutineName("FontCache")) + + private val deferredFonts = HashMap>() + private val weightNameMap: Map = mapOf( + Pair("100", R.string.font_weight_thin), + Pair("200", R.string.font_weight_extra_light), + Pair("300", R.string.font_weight_light), + Pair("400", R.string.font_weight_regular), + Pair("500", R.string.font_weight_medium), + Pair("600", R.string.font_weight_semi_bold), + Pair("700", R.string.font_weight_bold), + Pair("800", R.string.font_weight_extra_bold), + Pair("900", R.string.font_weight_extra_black) + ).mapValues { context.getString(it.value) } + + suspend fun loadFont(font: Font): Typeface? { + return loadFontAsync(font).await() + } + + private fun loadFontAsync(font: Font): Deferred { + return deferredFonts.getOrPut(font) { + scope.async(Dispatchers.IO) { + font.load() + } + } + } + + class Family(val displayName: String, val variants: Map) { + + constructor(font: Font) : this(font.displayName, mapOf(Pair("regular", font))) + + val default = variants.getOrElse("regular") { variants.values.first() } + } + + abstract class Font { + + abstract val fullDisplayName: String + abstract val displayName: String + open val familySorter get() = fullDisplayName + open val isAvailable get() = true + + abstract suspend fun load(): Typeface? + + open fun saveToJson(obj: JSONObject) { + obj.put(KEY_CLASS_NAME, this::class.java.name) + } + + open fun createWithWeight(weight: Int): Font { + return this + } + + fun toJsonString(): String { + val obj = JSONObject() + saveToJson(obj) + return obj.toString() + } + + interface LoadCallback { + + fun onFontLoaded(typeface: Typeface?) + } + + companion object { + + fun fromJsonString(context: Context, jsonString: String): Font { + val obj = JSONObject(jsonString) + val className = obj.getString(KEY_CLASS_NAME) + val clazz = Class.forName(className) + val constructor = clazz.getMethod("fromJson", Context::class.java, JSONObject::class.java) + return constructor.invoke(null, context, obj) as Font + } + } + } + + abstract class TypefaceFont(protected val typeface: Typeface?) : Font() { + + override val fullDisplayName = typeface.toString() + override val displayName get() = fullDisplayName + + override suspend fun load(): Typeface? { + return typeface + } + + override fun equals(other: Any?): Boolean { + return other is TypefaceFont && typeface == other.typeface + } + + override fun hashCode(): Int { + return fullDisplayName.hashCode() + } + } + + class DummyFont : TypefaceFont(null) { + + private val hashCode = "DummyFont".hashCode() + + override fun equals(other: Any?): Boolean { + return other is DummyFont + } + + override fun hashCode(): Int { + return hashCode + } + + companion object { + + @Keep + @JvmStatic + fun fromJson(context: Context, obj: JSONObject): Font { + return DummyFont() + } + } + } + + class TTFFont(context: Context, private val file: File) : + TypefaceFont(createTypeface(file)) { + + private val actualName: String = Uri.decode(file.name) + override val isAvailable = typeface != null + override val fullDisplayName: String = if (typeface == null) + context.getString(R.string.pref_fonts_missing_font) else actualName + + fun delete() = file.delete() + + override fun saveToJson(obj: JSONObject) { + super.saveToJson(obj) + obj.put(KEY_FONT_NAME, fullDisplayName) + } + + override fun equals(other: Any?): Boolean { + return other is TTFFont && actualName == other.actualName + } + + override fun hashCode() = actualName.hashCode() + + companion object { + + fun createTypeface(file: File): Typeface? { + return try { + Typeface.createFromFile(file) + } catch (e: Exception) { + null + } + } + + fun getFontsDir(context: Context): File { + return File(context.filesDir, "customFonts").apply { mkdirs() } + } + + fun getFile(context: Context, name: String): File { + return File(getFontsDir(context), Uri.encode(name)) + } + + @Keep + @JvmStatic + fun fromJson(context: Context, obj: JSONObject): Font { + val fontName = obj.getString(KEY_FONT_NAME) + return TTFFont(context, getFile(context, fontName)) + } + } + } + + class SystemFont( + val family: String, + val style: Int = Typeface.NORMAL) : TypefaceFont(Typeface.create(family, style)) { + + private val hashCode = "SystemFont|$family|$style".hashCode() + + override val fullDisplayName = family + + override fun saveToJson(obj: JSONObject) { + super.saveToJson(obj) + obj.put(KEY_FAMILY_NAME, family) + obj.put(KEY_STYLE, style) + } + + override fun equals(other: Any?): Boolean { + return other is SystemFont && family == other.family && style == other.style + } + + override fun hashCode(): Int { + return hashCode + } + + override fun createWithWeight(weight: Int): Font { + if (weight >= 700) { + return SystemFont(family, Typeface.BOLD) + } + return super.createWithWeight(weight) + } + + companion object { + + @Keep + @JvmStatic + fun fromJson(context: Context, obj: JSONObject): Font { + val family = obj.getString(KEY_FAMILY_NAME) + val style = obj.getInt(KEY_STYLE) + return SystemFont(family, style) + } + } + } + + class AssetFont( + assets: AssetManager, + private val name: String) : TypefaceFont(Typeface.createFromAsset(assets, "$name.ttf")) { + + private val hashCode = "AssetFont|$name".hashCode() + + override val fullDisplayName = name + + override fun equals(other: Any?): Boolean { + return other is AssetFont && name == other.name + } + + override fun hashCode(): Int { + return hashCode + } + } + + class GoogleFont( + private val context: Context, + private val family: String, + private val variant: String = "regular", + private val variants: Array = emptyArray()) : Font() { + + private val hashCode = "GoogleFont|$family|$variant".hashCode() + + override val displayName = createVariantName() + override val fullDisplayName = "$family $displayName" + override val familySorter = "${GoogleFontsListing.getWeight(variant)}${GoogleFontsListing.isItalic(variant)}" + + private fun createVariantName(): String { + if (variant == "italic") return context.getString(R.string.font_variant_italic) + val weight = GoogleFontsListing.getWeight(variant) + val weightString = INSTANCE.get(context).weightNameMap[weight] ?: weight + val italicString = if (GoogleFontsListing.isItalic(variant)) + " " + context.getString(R.string.font_variant_italic) else "" + return "$weightString$italicString" + } + + override suspend fun load(): Typeface? { + val request = FontRequest( + "com.google.android.gms.fonts", // ProviderAuthority + "com.google.android.gms", // ProviderPackage + GoogleFontsListing.buildQuery(family, variant), // Query + R.array.com_google_android_gms_fonts_certs) + + return suspendCoroutine { + FontsContractCompat.requestFont(context, request, object: FontsContractCompat.FontRequestCallback() { + override fun onTypefaceRetrieved(typeface: Typeface) { + it.resume(typeface) + } + + override fun onTypefaceRequestFailed(reason: Int) { + it.resume(null) + } + }, uiHelperHandler) + } + } + + override fun saveToJson(obj: JSONObject) { + super.saveToJson(obj) + obj.put(KEY_FAMILY_NAME, family) + obj.put(KEY_VARIANT, variant) + val variantsArray = JSONArray() + variants.forEach { variantsArray.put(it) } + obj.put(KEY_VARIANTS, variantsArray) + } + + override fun equals(other: Any?): Boolean { + return other is GoogleFont && family == other.family && variant == other.variant + } + + override fun hashCode(): Int { + return hashCode + } + + override fun createWithWeight(weight: Int): Font { + if (weight == -1) return this + val currentWeight = GoogleFontsListing.getWeight(variant).toInt() + if (weight == currentWeight) return this + val newVariant = if (weight > currentWeight) + findHeavier(weight, currentWeight, GoogleFontsListing.isItalic(variant)) + else + findLighter(weight, currentWeight, GoogleFontsListing.isItalic(variant)) + if (newVariant != null) { + return GoogleFont(context, family, newVariant, variants) + } + return super.createWithWeight(weight) + } + + private fun findHeavier(weight: Int, minWeight: Int, italic: Boolean): String? { + val variants = variants.filter { it.contains("italic") == italic } + return variants.lastOrNull { + val variantWeight = GoogleFontsListing.getWeight(it).toInt() + variantWeight in minWeight..weight + } ?: variants.firstOrNull { + GoogleFontsListing.getWeight(it).toInt() >= minWeight + } + } + + private fun findLighter(weight: Int, maxWeight: Int, italic: Boolean): String? { + val variants = variants.filter { it.contains("italic") == italic } + return variants.firstOrNull { + val variantWeight = GoogleFontsListing.getWeight(it).toInt() + variantWeight in weight..maxWeight + } ?: variants.lastOrNull { + GoogleFontsListing.getWeight(it).toInt() <= maxWeight + } + } + + companion object { + + @Keep + @JvmStatic + fun fromJson(context: Context, obj: JSONObject): Font { + val family = obj.getString(KEY_FAMILY_NAME) + val variant = obj.getString(KEY_VARIANT) + val variantsArray = obj.optJSONArray(KEY_VARIANTS) ?: JSONArray() + val variants = Array(variantsArray.length()) { variantsArray.getString(it) } + return GoogleFont(context, family, variant, variants) + } + } + } + + companion object { + @JvmField + val INSTANCE = MainThreadInitializedObject(::FontCache) + + private const val KEY_CLASS_NAME = "className" + private const val KEY_FAMILY_NAME = "family" + private const val KEY_STYLE = "style" + private const val KEY_VARIANT = "variant" + private const val KEY_VARIANTS = "variants" + private const val KEY_FONT_NAME = "font" + } +} \ No newline at end of file diff --git a/lawnchair/src/app/lawnchair/font/FontManager.kt b/lawnchair/src/app/lawnchair/font/FontManager.kt new file mode 100644 index 0000000000..d8544f59ec --- /dev/null +++ b/lawnchair/src/app/lawnchair/font/FontManager.kt @@ -0,0 +1,82 @@ +package app.lawnchair.font + +import android.content.Context +import android.graphics.Typeface +import android.util.AttributeSet +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.lifecycle.lifecycleScope +import app.lawnchair.util.lookupLifecycleOwner +import com.android.launcher3.R +import com.android.launcher3.util.MainThreadInitializedObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class FontManager private constructor(private val context: Context) { + + private val loaderManager by lazy { FontCache.INSTANCE.get(context) } + private val specMap by lazy { createFontMap() } + + private val uiRegular = FontCache.GoogleFont(context, "Google Sans") + private val uiMedium = FontCache.GoogleFont(context, "Google Sans", "500") + private val uiTextMedium = FontCache.GoogleFont(context, "Google Sans Text", "500") + + private fun createFontMap(): Map { + val sansSerif = Typeface.SANS_SERIF + + val map = mutableMapOf() + map[R.id.font_base_icon] = FontSpec(uiTextMedium, sansSerif) + map[R.id.font_button] = FontSpec(uiMedium, sansSerif) + map[R.id.font_smartspace_text] = FontSpec(uiRegular, sansSerif) + return map + } + + fun overrideFont(textView: TextView, attrs: AttributeSet?) { + val context = textView.context + var a = context.obtainStyledAttributes(attrs, R.styleable.CustomFont) + var fontType = a.getResourceId(R.styleable.CustomFont_customFontType, -1) + var fontWeight = a.getInt(R.styleable.CustomFont_customFontWeight, -1) + val ap = a.getResourceId(R.styleable.CustomFont_android_textAppearance, -1) + a.recycle() + + if (ap != -1) { + a = context.obtainStyledAttributes(ap, R.styleable.CustomFont) + if (fontType == -1) { + fontType = a.getResourceId(R.styleable.CustomFont_customFontType, -1) + } + if (fontWeight == -1) { + fontWeight = a.getInt(R.styleable.CustomFont_customFontWeight, -1) + } + a.recycle() + } + + if (fontType != -1) { + setCustomFont(textView, fontType, fontWeight) + } + } + + @JvmOverloads + fun setCustomFont(textView: TextView, @IdRes type: Int, style: Int = -1) { + val spec = specMap[type] ?: return + val lifecycleOwner = textView.context.lookupLifecycleOwner() + lifecycleOwner?.lifecycleScope?.launch { + val typeface = loaderManager.loadFont(spec.font.createWithWeight(style)) + if (typeface != null) { + launch(Dispatchers.Main) { + textView.typeface = typeface + } + } + } + } + + class FontSpec(val loader: () -> FontCache.Font, val fallback: Typeface) { + constructor(font: FontCache.Font, fallback: Typeface) : this({ font }, fallback) + + val font get() = loader() + } + + companion object { + @JvmField + val INSTANCE = MainThreadInitializedObject(::FontManager) + } +} diff --git a/lawnchair/src/app/lawnchair/font/googlefonts/GoogleFontsListing.kt b/lawnchair/src/app/lawnchair/font/googlefonts/GoogleFontsListing.kt new file mode 100644 index 0000000000..027d72576d --- /dev/null +++ b/lawnchair/src/app/lawnchair/font/googlefonts/GoogleFontsListing.kt @@ -0,0 +1,105 @@ +/* + * This file is part of Lawnchair Launcher. + * + * Lawnchair Launcher is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lawnchair Launcher is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Lawnchair Launcher. If not, see . + */ + +package app.lawnchair.font.googlefonts + +import android.content.Context +import android.content.res.Resources +import app.lawnchair.util.toArrayList +import com.android.launcher3.util.MainThreadInitializedObject +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import org.json.JSONObject +import java.util.* + +class GoogleFontsListing private constructor(private val context: Context) { + private val scope = CoroutineScope(CoroutineName("GoogleFontsListing")) + + private val dataProvider = MockDataProvider(context.resources) + private val fonts by lazy { scope.async(Dispatchers.IO) { loadFontListing() } } + + private fun loadFontListing(): List { + val json = dataProvider.getFontListing() + return parseFontListing(json) + } + + private fun parseFontListing(json: JSONObject): List { + val fonts = ArrayList() + val items = json.getJSONArray(KEY_ITEMS) + for (i in (0 until items.length())) { + val font = items.getJSONObject(i) + val family = font.getString(KEY_FAMILY) + val variants = font.getJSONArray(KEY_VARIANTS).toArrayList() + fonts.add(GoogleFontInfo(family, variants)) + } + fonts.add(GoogleFontInfo("Google Sans", listOf("regular", "italic", "500", "500italic", "700", "700italic"))) + fonts.add(GoogleFontInfo("Google Sans Text", listOf("regular", "italic", "500", "500italic", "700", "700italic"))) + fonts.sort() + return fonts + } + + suspend fun getFonts(): List { + return fonts.await() + } + + interface DataProvider { + + fun getFontListing(): JSONObject + } + + class MockDataProvider(private val res: Resources) : DataProvider { + + override fun getFontListing(): JSONObject { + val json = res.assets.open("google_fonts.json").bufferedReader().use { it.readText() } + return JSONObject(json) + } + } + + class GoogleFontInfo(val family: String, val variants: List) : Comparable { + + override fun compareTo(other: GoogleFontInfo): Int { + return family.compareTo(other.family) + } + } + + companion object { + + @JvmField + val INSTANCE = MainThreadInitializedObject(::GoogleFontsListing) + + private const val KEY_ITEMS = "items" + private const val KEY_FAMILY = "family" + private const val KEY_VARIANTS = "variants" + + fun getWeight(variant: String): String { + if (variant == "italic") return "400" + return variant.replace("italic", "").replace("regular", "400") + } + + fun isItalic(variant: String): Boolean { + return variant.contains("italic") + } + + fun buildQuery(family: String, variant: String): String { + val weight = getWeight(variant) + val italic = isItalic(variant) + return "name=$family&weight=$weight&italic=${if (italic) 1 else 0}&besteffort=1" + } + } +} diff --git a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt index e13f4cda78..1be009c37a 100644 --- a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt +++ b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt @@ -28,6 +28,7 @@ import com.android.launcher3.R import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Themes import com.android.systemui.shared.system.QuickStepContract +import org.json.JSONArray import java.util.concurrent.Callable import java.util.concurrent.ExecutionException import kotlin.system.exitProcess @@ -110,3 +111,12 @@ fun overrideAllAppsTextColor(textView: TextView) { textView.setTextColor(Themes.getAttrColor(context, R.attr.allAppsAlternateTextColor)) } } + +@Suppress("UNCHECKED_CAST") +fun JSONArray.toArrayList(): ArrayList { + val arrayList = ArrayList() + for (i in (0 until length())) { + arrayList.add(get(i) as T) + } + return arrayList +} diff --git a/lawnchair/src/app/lawnchair/util/handlers.kt b/lawnchair/src/app/lawnchair/util/handlers.kt new file mode 100644 index 0000000000..3774d951de --- /dev/null +++ b/lawnchair/src/app/lawnchair/util/handlers.kt @@ -0,0 +1,9 @@ +package app.lawnchair.util + +import android.os.Handler +import android.os.Looper +import com.android.launcher3.util.Executors + +val mainHandler = Handler(Looper.getMainLooper()) +val workerHandler = Executors.MODEL_EXECUTOR.handler +val uiHelperHandler = Executors.UI_HELPER_EXECUTOR.handler diff --git a/lawnchair/src/app/lawnchair/util/lifecycle.kt b/lawnchair/src/app/lawnchair/util/lifecycle.kt index 1e1170f393..7e9f753c55 100644 --- a/lawnchair/src/app/lawnchair/util/lifecycle.kt +++ b/lawnchair/src/app/lawnchair/util/lifecycle.kt @@ -1,9 +1,12 @@ package app.lawnchair.util +import android.content.Context +import android.content.ContextWrapper import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner private val LocalLifecycleState = compositionLocalOf { error("CompositionLocal LocalLifecycleState not present") @@ -34,3 +37,11 @@ private fun observeLifecycleState(): Lifecycle.State { return state } + +fun Context.lookupLifecycleOwner(): LifecycleOwner? { + return when (this) { + is LifecycleOwner -> this + is ContextWrapper -> baseContext.lookupLifecycleOwner() + else -> null + } +} diff --git a/lawnchair/src/app/lawnchair/views/CustomButton.kt b/lawnchair/src/app/lawnchair/views/CustomButton.kt new file mode 100644 index 0000000000..be4d414f7c --- /dev/null +++ b/lawnchair/src/app/lawnchair/views/CustomButton.kt @@ -0,0 +1,15 @@ +package app.lawnchair.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.Button +import app.lawnchair.font.FontManager + +class CustomButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : Button(context, attrs) { + + init { + FontManager.INSTANCE.get(context).overrideFont(this, attrs) + } +} diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml index be8d3405ec..436e61110d 100644 --- a/quickstep/res/layout/overview_actions_container.xml +++ b/quickstep/res/layout/overview_actions_container.xml @@ -31,7 +31,7 @@ android:layout_height="1dp" android:layout_weight="1" /> -