Skip to content

Commit 0133673

Browse files
committed
feat: add ability to use custom fonts in all apps
Refs: FossifyOrg/General-Discussion#323
1 parent 2af4c92 commit 0133673

25 files changed

+618
-54
lines changed

commons/src/main/kotlin/org/fossify/commons/activities/BaseSimpleActivity.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ import android.provider.DocumentsContract
1818
import android.provider.MediaStore
1919
import android.provider.Settings
2020
import android.telecom.TelecomManager
21+
import android.util.AttributeSet
22+
import android.view.LayoutInflater
2123
import android.view.Menu
2224
import android.view.MenuItem
2325
import android.view.View
2426
import android.widget.EditText
2527
import android.widget.ImageView
28+
import android.widget.TextView
2629
import android.widget.Toast
2730
import androidx.activity.OnBackPressedCallback
2831
import androidx.activity.addCallback
2932
import androidx.annotation.RequiresApi
3033
import androidx.core.app.ActivityCompat
3134
import androidx.core.net.toUri
3235
import androidx.core.util.Pair
36+
import androidx.core.view.LayoutInflaterCompat
3337
import androidx.core.view.ViewCompat
3438
import androidx.core.view.WindowCompat
3539
import androidx.core.view.get
@@ -44,6 +48,7 @@ import org.fossify.commons.dialogs.WritePermissionDialog
4448
import org.fossify.commons.dialogs.WritePermissionDialog.WritePermissionDialogMode
4549
import org.fossify.commons.extensions.adjustAlpha
4650
import org.fossify.commons.extensions.applyColorFilter
51+
import org.fossify.commons.extensions.applyFontToTextView
4752
import org.fossify.commons.extensions.baseConfig
4853
import org.fossify.commons.extensions.buildDocumentUriSdk30
4954
import org.fossify.commons.extensions.canManageMedia
@@ -183,6 +188,7 @@ abstract class BaseSimpleActivity : EdgeToEdgeActivity() {
183188
setTheme(getThemeId(showTransparentTop = true))
184189
}
185190

191+
installFontInflaterFactory()
186192
super.onCreate(savedInstanceState)
187193
WindowCompat.enableEdgeToEdge(window)
188194
registerBackPressedCallback()
@@ -194,6 +200,25 @@ abstract class BaseSimpleActivity : EdgeToEdgeActivity() {
194200
}
195201
}
196202

203+
private fun installFontInflaterFactory() {
204+
val inflater = layoutInflater
205+
if (inflater.factory2 != null) return
206+
207+
val appCompatDelegate = delegate
208+
LayoutInflaterCompat.setFactory2(inflater, object : LayoutInflater.Factory2 {
209+
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
210+
val view = appCompatDelegate.createView(parent, name, context, attrs)
211+
val textView = view as? TextView ?: return view
212+
applyFontToTextView(textView)
213+
return view
214+
}
215+
216+
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
217+
return onCreateView(null, name, context, attrs)
218+
}
219+
})
220+
}
221+
197222
override fun onResume() {
198223
super.onResume()
199224
if (useDynamicTheme) {

commons/src/main/kotlin/org/fossify/commons/activities/CustomizationActivity.kt

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package org.fossify.commons.activities
22

33
import android.content.ContentValues
44
import android.graphics.Color
5+
import android.graphics.Typeface
6+
import android.net.Uri
57
import android.os.Bundle
8+
import androidx.activity.result.contract.ActivityResultContracts
69
import org.fossify.commons.R
710
import org.fossify.commons.databinding.ActivityCustomizationBinding
811
import org.fossify.commons.dialogs.ColorPickerDialog
@@ -11,16 +14,19 @@ import org.fossify.commons.dialogs.ConfirmationDialog
1114
import org.fossify.commons.dialogs.LineColorPickerDialog
1215
import org.fossify.commons.dialogs.PurchaseThankYouDialog
1316
import org.fossify.commons.dialogs.RadioGroupDialog
17+
import org.fossify.commons.extensions.applyFontToViewRecursively
1418
import org.fossify.commons.extensions.baseConfig
1519
import org.fossify.commons.extensions.beVisibleIf
1620
import org.fossify.commons.extensions.canAccessGlobalConfig
1721
import org.fossify.commons.extensions.checkAppIconColor
1822
import org.fossify.commons.extensions.getColoredMaterialStatusBarColor
1923
import org.fossify.commons.extensions.getContrastColor
24+
import org.fossify.commons.extensions.getFilenameFromUri
2025
import org.fossify.commons.extensions.getProperPrimaryColor
2126
import org.fossify.commons.extensions.getProperTextColor
2227
import org.fossify.commons.extensions.getThemeId
2328
import org.fossify.commons.extensions.isDynamicTheme
29+
import org.fossify.commons.extensions.isFontFile
2430
import org.fossify.commons.extensions.isSystemInDarkMode
2531
import org.fossify.commons.extensions.isThankYouInstalled
2632
import org.fossify.commons.extensions.setFillWithStroke
@@ -32,9 +38,16 @@ import org.fossify.commons.extensions.withGlobalConfig
3238
import org.fossify.commons.helpers.APP_ICON_IDS
3339
import org.fossify.commons.helpers.APP_LAUNCHER_NAME
3440
import org.fossify.commons.helpers.DARK_GREY
41+
import org.fossify.commons.helpers.FONT_TYPE_CUSTOM
42+
import org.fossify.commons.helpers.FONT_TYPE_MONOSPACE
43+
import org.fossify.commons.helpers.FONT_TYPE_SYSTEM_DEFAULT
44+
import org.fossify.commons.helpers.FontHelper
3545
import org.fossify.commons.helpers.MyContentProvider.COL_ACCENT_COLOR
3646
import org.fossify.commons.helpers.MyContentProvider.COL_APP_ICON_COLOR
3747
import org.fossify.commons.helpers.MyContentProvider.COL_BACKGROUND_COLOR
48+
import org.fossify.commons.helpers.MyContentProvider.COL_FONT_DATA
49+
import org.fossify.commons.helpers.MyContentProvider.COL_FONT_NAME
50+
import org.fossify.commons.helpers.MyContentProvider.COL_FONT_TYPE
3851
import org.fossify.commons.helpers.MyContentProvider.COL_PRIMARY_COLOR
3952
import org.fossify.commons.helpers.MyContentProvider.COL_TEXT_COLOR
4053
import org.fossify.commons.helpers.MyContentProvider.COL_THEME_TYPE
@@ -48,6 +61,7 @@ import org.fossify.commons.models.GlobalConfig
4861
import org.fossify.commons.models.MyTheme
4962
import org.fossify.commons.models.RadioItem
5063
import org.fossify.commons.models.isGlobalThemingEnabled
64+
import java.io.File
5165
import kotlin.math.abs
5266

5367
class CustomizationActivity : BaseSimpleActivity() {
@@ -69,12 +83,19 @@ class CustomizationActivity : BaseSimpleActivity() {
6983
private var curAppIconColor = 0
7084
private var curSelectedThemeId = 0
7185
private var originalAppIconColor = 0
86+
private var curFontType = 0
87+
private var curFontFileName = ""
7288
private var lastSavePromptTS = 0L
7389
private var hasUnsavedChanges = false
7490
private val predefinedThemes = LinkedHashMap<Int, MyTheme>()
7591
private var curPrimaryLineColorPicker: LineColorPickerDialog? = null
7692
private var globalConfig: GlobalConfig? = null
7793

94+
private val fontFilePicker =
95+
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
96+
uri?.let { handleFontFileSelected(it) }
97+
}
98+
7899
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
79100

80101
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
@@ -437,12 +458,16 @@ class CustomizationActivity : BaseSimpleActivity() {
437458
primaryColor = curPrimaryColor
438459
accentColor = curAccentColor
439460
appIconColor = curAppIconColor
461+
fontType = curFontType
462+
fontName = curFontFileName
440463
}
441464

442465
if (didAppIconColorChange) {
443466
checkAppIconColor()
444467
}
445468

469+
FontHelper.clearCache()
470+
446471
baseConfig.isGlobalThemeEnabled = binding.applyToAllSwitch.isChecked
447472
baseConfig.isSystemThemeEnabled = curSelectedThemeId == THEME_SYSTEM
448473

@@ -461,6 +486,13 @@ class CustomizationActivity : BaseSimpleActivity() {
461486
put(COL_PRIMARY_COLOR, curPrimaryColor)
462487
put(COL_ACCENT_COLOR, curAccentColor)
463488
put(COL_APP_ICON_COLOR, curAppIconColor)
489+
put(COL_FONT_TYPE, curFontType)
490+
put(COL_FONT_NAME, curFontFileName)
491+
if (curFontType == FONT_TYPE_CUSTOM && curFontFileName.isNotEmpty()) {
492+
FontHelper.getFontData(this@CustomizationActivity, curFontFileName)?.let {
493+
put(COL_FONT_DATA, it)
494+
}
495+
}
464496
}
465497
)
466498
}
@@ -490,6 +522,8 @@ class CustomizationActivity : BaseSimpleActivity() {
490522
curPrimaryColor = baseConfig.primaryColor
491523
curAccentColor = baseConfig.accentColor
492524
curAppIconColor = baseConfig.appIconColor
525+
curFontType = baseConfig.fontType
526+
curFontFileName = baseConfig.fontName
493527
}
494528

495529
private fun setupColorsPickers() {
@@ -526,6 +560,8 @@ class CustomizationActivity : BaseSimpleActivity() {
526560
}
527561
}
528562
}
563+
564+
setupFontPicker()
529565
}
530566

531567
private fun hasColorChanged(old: Int, new: Int) = abs(old - new) > 1
@@ -536,6 +572,100 @@ class CustomizationActivity : BaseSimpleActivity() {
536572
refreshMenuItems()
537573
}
538574

575+
private fun setupFontPicker() {
576+
updateFontDisplay()
577+
binding.customizationFontHolder.setOnClickListener {
578+
fontPickerClicked()
579+
}
580+
}
581+
582+
private fun updateFontDisplay() {
583+
binding.customizationFont.text = when (curFontType) {
584+
FONT_TYPE_MONOSPACE -> getString(R.string.font_monospace)
585+
FONT_TYPE_CUSTOM -> curFontFileName.ifEmpty { getString(R.string.select_font_file) }
586+
else -> getString(R.string.font_system_default)
587+
}
588+
}
589+
590+
private fun fontPickerClicked() {
591+
val items = arrayListOf(
592+
RadioItem(FONT_TYPE_SYSTEM_DEFAULT, getString(R.string.font_system_default)),
593+
RadioItem(FONT_TYPE_MONOSPACE, getString(R.string.font_monospace)),
594+
RadioItem(FONT_TYPE_CUSTOM, getString(R.string.select_font_file))
595+
)
596+
597+
RadioGroupDialog(this, items, curFontType) { selected ->
598+
val selectedType = selected as Int
599+
if (selectedType == FONT_TYPE_CUSTOM) {
600+
openFontFilePicker()
601+
} else {
602+
curFontType = selectedType
603+
curFontFileName = ""
604+
fontChanged()
605+
}
606+
}
607+
}
608+
609+
private fun openFontFilePicker() {
610+
try {
611+
fontFilePicker.launch(
612+
arrayOf(
613+
"font/ttf",
614+
"font/otf",
615+
"application/x-font-ttf",
616+
"application/x-font-otf",
617+
"*/*"
618+
)
619+
)
620+
} catch (e: Exception) {
621+
toast(R.string.system_service_disabled)
622+
}
623+
}
624+
625+
private fun handleFontFileSelected(uri: Uri) {
626+
try {
627+
val fileName = getFilenameFromUri(uri)
628+
if (fileName.isEmpty() || !fileName.isFontFile()) {
629+
toast(R.string.invalid_font_file)
630+
return
631+
}
632+
633+
val fontData = contentResolver.openInputStream(uri)?.use { it.readBytes() }
634+
if (fontData == null) {
635+
toast(R.string.invalid_font_file)
636+
return
637+
}
638+
639+
val tempFile = File(cacheDir, fileName)
640+
tempFile.writeBytes(fontData)
641+
try {
642+
Typeface.createFromFile(tempFile)
643+
} catch (_: Exception) {
644+
tempFile.delete()
645+
toast(R.string.invalid_font_file)
646+
return
647+
}
648+
tempFile.delete()
649+
650+
if (FontHelper.saveFontData(this, fontData, fileName)) {
651+
curFontType = FONT_TYPE_CUSTOM
652+
curFontFileName = fileName
653+
fontChanged()
654+
} else {
655+
toast(R.string.invalid_font_file)
656+
}
657+
} catch (_: Exception) {
658+
toast(R.string.invalid_font_file)
659+
}
660+
}
661+
662+
private fun fontChanged() {
663+
hasUnsavedChanges = true
664+
updateFontDisplay()
665+
applyFontToViewRecursively(window.decorView)
666+
refreshMenuItems()
667+
}
668+
539669
private fun setCurrentTextColor(color: Int) {
540670
curTextColor = color
541671
updateLabelColors(color)
@@ -707,6 +837,8 @@ class CustomizationActivity : BaseSimpleActivity() {
707837
binding.customizationPrimaryColorLabel,
708838
binding.customizationAccentColorLabel,
709839
binding.customizationAppIconColorLabel,
840+
binding.customizationFontLabel,
841+
binding.customizationFont,
710842
binding.applyToAllLabel,
711843
binding.applyToAllNote
712844
).forEach {
@@ -717,6 +849,7 @@ class CustomizationActivity : BaseSimpleActivity() {
717849
private fun updateHeaderColors(primaryColor: Int = getProperPrimaryColor()) {
718850
arrayListOf(
719851
binding.settingsThemeAndColorsLabel,
852+
binding.settingsFontLabel,
720853
binding.settingsAllFossifyAppsLabel
721854
).forEach {
722855
it.setTextColor(primaryColor)

commons/src/main/kotlin/org/fossify/commons/compose/components/LinkifyTextComponent.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.fossify.commons.R
1616
import org.fossify.commons.compose.extensions.MyDevices
1717
import org.fossify.commons.compose.theme.AppThemeSurface
1818
import org.fossify.commons.compose.theme.SimpleTheme
19+
import org.fossify.commons.extensions.applyFontToTextView
1920
import org.fossify.commons.extensions.fromHtml
2021
import org.fossify.commons.extensions.removeUnderlines
2122

@@ -40,6 +41,8 @@ fun LinkifyTextComponent(
4041
textView.textAlignment = textAlignment
4142
textView.textSize = fontSize.value
4243
textView.movementMethod = LinkMovementMethod.getInstance()
44+
context.applyFontToTextView(textView)
45+
4346
if (removeUnderlines) {
4447
customLinkifyTextView.removeUnderlines()
4548
}

0 commit comments

Comments
 (0)