Skip to content

Commit 4fbb913

Browse files
Arman-ATIrmp22
andcommitted
Add support for custom fonts
Co-authored-by: rmp22 <rmp22@users.noreply.github.com>
1 parent e855ca5 commit 4fbb913

5 files changed

Lines changed: 694 additions & 55 deletions

File tree

res/layout/dialog_font_preview.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright (C) 2025 the RisingOS Revived Android Project
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+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
-->
14+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
15+
android:layout_width="match_parent"
16+
android:layout_height="wrap_content"
17+
android:orientation="vertical"
18+
android:padding="24dp">
19+
20+
<TextView
21+
android:id="@+id/preview_text"
22+
android:layout_width="match_parent"
23+
android:layout_height="wrap_content"
24+
android:text="@string/font_preview_quote"
25+
android:textSize="18sp"
26+
android:lineSpacingMultiplier="1.3" />
27+
28+
</LinearLayout>

res/layout/font_picker_preview.xml

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,93 @@
1717
android:layout_width="match_parent"
1818
android:layout_height="match_parent">
1919

20-
<ScrollView
20+
<androidx.core.widget.NestedScrollView
2121
android:layout_width="match_parent"
2222
android:layout_height="match_parent"
23-
android:fillViewport="true">
23+
android:fillViewport="false"
24+
android:clipToPadding="false">
2425

2526
<LinearLayout
2627
android:layout_width="match_parent"
2728
android:layout_height="wrap_content"
2829
android:orientation="vertical"
29-
android:paddingBottom="24dp">
30+
android:paddingBottom="120dp">
3031

31-
<TextView
32-
android:id="@+id/font_preview_text"
32+
<LinearLayout
3333
android:layout_width="match_parent"
3434
android:layout_height="wrap_content"
35-
android:text="@string/font_preview_text"
36-
android:background="@drawable/custom_preference_background"
37-
android:textSize="20sp"
3835
android:layout_marginTop="64dp"
39-
android:layout_marginBottom="24dp"
4036
android:layout_marginHorizontal="24dp"
41-
android:padding="24dp" />
37+
android:background="@drawable/custom_preference_background"
38+
android:orientation="vertical">
39+
40+
<TextView
41+
android:id="@+id/font_preview_text"
42+
android:layout_width="match_parent"
43+
android:layout_height="wrap_content"
44+
android:text="@string/font_preview_text"
45+
android:textSize="20sp"
46+
android:padding="24dp" />
47+
48+
<View
49+
android:id="@+id/custom_font_divider"
50+
android:layout_width="match_parent"
51+
android:layout_height="1dp"
52+
android:layout_marginHorizontal="16dp"
53+
android:background="?android:attr/dividerHorizontal"
54+
android:visibility="gone" />
55+
56+
<LinearLayout
57+
android:id="@+id/custom_font_info_card"
58+
android:layout_width="match_parent"
59+
android:layout_height="wrap_content"
60+
android:padding="16dp"
61+
android:orientation="vertical"
62+
android:visibility="gone">
63+
64+
<LinearLayout
65+
android:layout_width="match_parent"
66+
android:layout_height="wrap_content"
67+
android:orientation="horizontal"
68+
android:gravity="center_vertical">
69+
70+
<LinearLayout
71+
android:layout_width="0dp"
72+
android:layout_height="wrap_content"
73+
android:layout_weight="1"
74+
android:orientation="vertical">
75+
76+
<TextView
77+
android:layout_width="wrap_content"
78+
android:layout_height="wrap_content"
79+
android:text="@string/custom_font"
80+
android:textStyle="bold"
81+
android:textSize="14sp" />
82+
83+
<TextView
84+
android:id="@+id/custom_font_name_text"
85+
android:layout_width="wrap_content"
86+
android:layout_height="wrap_content"
87+
android:layout_marginTop="4dp"
88+
android:textSize="12sp"
89+
android:alpha="0.7" />
90+
91+
</LinearLayout>
92+
93+
<Button
94+
android:id="@+id/reset_custom_font_button"
95+
android:layout_width="48dp"
96+
android:layout_height="48dp"
97+
android:drawableStart="@drawable/ic_delete"
98+
android:background="?android:attr/selectableItemBackgroundBorderless"
99+
android:contentDescription="@string/reset_custom_font_content_desc"
100+
style="@style/Widget.Material3.Button.IconButton" />
101+
102+
</LinearLayout>
103+
104+
</LinearLayout>
105+
106+
</LinearLayout>
42107

43108
<TextView
44109
android:id="@+id/font_selector"
@@ -82,6 +147,6 @@
82147

83148
</LinearLayout>
84149
</LinearLayout>
85-
</ScrollView>
150+
</androidx.core.widget.NestedScrollView>
86151

87152
</androidx.coordinatorlayout.widget.CoordinatorLayout>

res/values/custom_strings.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@
1515
limitations under the License.
1616
-->
1717
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
18+
<!-- Fonts Screen -->
19+
<string name="font_title">Font</string>
20+
<string name="font_style">Font style</string>
21+
<string name="font_preview_dialog_title">Font Preview</string>
22+
<string name="apply">Apply</string>
23+
<string name="applied">Applied</string>
24+
<string name="reboot_device">Reboot device</string>
25+
<string name="reboot_required_title">Reboot Required</string>
26+
<string name="reboot_required_message">A reboot is required to apply the font changes.\nWould you like to reboot now?</string>
27+
<string name="reboot_now">Reboot Now</string>
28+
<string name="later">Later</string>
29+
<string name="reset_default_title">Reset to Default</string>
30+
<string name="reset_default_message">This will reset your font to the default system font. Are you sure?</string>
31+
<string name="add_font">Add font</string>
32+
<string name="custom_font">Custom font</string>
33+
<string name="no_custom_font">No custom font</string>
34+
<string name="add_custom_font">Add custom font</string>
35+
<string name="change_custom_font">Change custom font</string>
36+
<string name="reset_custom_font_content_desc">Reset custom font</string>
37+
<string name="reboot_required_custom_font_title">Reboot required for custom font changes</string>
38+
<string name="reboot_required_custom_font_desc">Skipping reboot after changing the custom font may cause UI inconsistencies across apps.</string>
39+
<string name="font_preview_quote">Fear is the enemy of innovation.</string>
40+
<string name="font_preview_numbers">1234567890!@#%&amp;*()_+-=</string>
41+
<string name="active_content_desc">Active</string>
42+
<string name="toast_failed_apply_font">Failed to apply font</string>
43+
<string name="toast_invalid_font_file">Invalid font file</string>
44+
1845
<!-- Status bar - Icon tinting -->
1946
<string name="status_bar_tint_icons_title">Statusleisten-Symbole einfärben</string>
2047
<string name="status_bar_tint_icons_summary">Statusleisten-Symbole mit der System-Akzentfarbe einfärben</string>
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright (C) 2025 AxionOS
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+
package com.rising.settings.fragments.ui.fonts
17+
18+
import android.content.Context
19+
import android.graphics.Typeface
20+
import android.graphics.fonts.FontFamilyUpdateRequest
21+
import android.graphics.fonts.FontFileUpdateRequest
22+
import android.graphics.fonts.FontFileUtil
23+
import android.graphics.fonts.FontManager
24+
import android.graphics.fonts.FontStyle
25+
import android.net.Uri
26+
import android.os.FileUtils
27+
import android.os.ParcelFileDescriptor
28+
import android.os.ServiceManager
29+
import android.os.UserHandle
30+
import android.provider.Settings
31+
import android.util.Log
32+
import android.widget.Toast
33+
import com.android.internal.statusbar.IStatusBarService
34+
import com.android.settings.R
35+
import kotlinx.coroutines.Dispatchers
36+
import kotlinx.coroutines.withContext
37+
import org.json.JSONObject
38+
import java.io.File
39+
import java.io.FileInputStream
40+
import java.nio.channels.FileChannel
41+
42+
class ExternalFontInstaller(private val context: Context) {
43+
44+
companion object {
45+
private const val TAG = "ExternalFontInstaller"
46+
private const val CUSTOM_FONT_FILE = "cust_font.ttf"
47+
private const val TEMP_PREVIEW_FONT = "preview_font.ttf"
48+
private const val OVERLAY_CATEGORY_FONT = "android.theme.customization.font"
49+
const val DEFAULT_FONT_FAMILY = "Rookery-Regular"
50+
private const val DEFAULT_FONT_OVERLAY = "com.android.theme.font.rookery"
51+
52+
fun rebootDevice() {
53+
runCatching {
54+
val binder = ServiceManager.getService("statusbar")
55+
val statusBarService = IStatusBarService.Stub.asInterface(binder)
56+
statusBarService.reboot(false, "system_font_change")
57+
}.onFailure {
58+
Log.e(TAG, "Failed to reboot device via statusbar service", it)
59+
}
60+
}
61+
}
62+
63+
private val fontManager: FontManager = context.getSystemService(FontManager::class.java)
64+
65+
suspend fun loadTypefaceFromUri(uri: Uri): Typeface? = withContext(Dispatchers.IO) {
66+
val tempFile = copyUriToCache(uri, TEMP_PREVIEW_FONT) ?: return@withContext null
67+
val postScriptName = extractPostScriptName(tempFile) ?: run {
68+
tempFile.delete()
69+
return@withContext null
70+
}
71+
return@withContext Typeface.createFromFile(tempFile)
72+
}
73+
74+
suspend fun installFontFromUri(uri: Uri): String? {
75+
val fontFile = copyUriToCache(uri, CUSTOM_FONT_FILE) ?: run {
76+
showToast(R.string.toast_failed_apply_font)
77+
return null
78+
}
79+
80+
val postScriptName = extractPostScriptName(fontFile) ?: run {
81+
showToast(R.string.toast_invalid_font_file)
82+
fontFile.delete()
83+
return null
84+
}
85+
86+
if (!applyFontToSystem(fontFile, postScriptName)) {
87+
fontFile.delete()
88+
return null
89+
}
90+
91+
updateThemeOverlays()
92+
cleanupPreviewFont()
93+
return postScriptName
94+
}
95+
96+
private suspend fun copyUriToCache(uri: Uri, fileName: String): File? = withContext(Dispatchers.IO) {
97+
try {
98+
val cacheFile = File(context.cacheDir, fileName)
99+
context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
100+
FileInputStream(pfd.fileDescriptor).use { input ->
101+
cacheFile.outputStream().use { output ->
102+
FileUtils.copy(input, output)
103+
}
104+
}
105+
}
106+
cacheFile
107+
} catch (e: Exception) {
108+
Log.e(TAG, "Failed to copy URI to cache", e)
109+
null
110+
}
111+
}
112+
113+
private fun extractPostScriptName(fontFile: File): String? =
114+
FileInputStream(fontFile).use { fis ->
115+
val buffer = fis.channel.map(FileChannel.MapMode.READ_ONLY, 0, fis.channel.size())
116+
FontFileUtil.getPostScriptName(buffer, 0)
117+
}
118+
119+
private fun applyFontToSystem(fontFile: File, postScriptName: String): Boolean {
120+
return runCatching {
121+
val pfd = ParcelFileDescriptor.open(fontFile, ParcelFileDescriptor.MODE_READ_ONLY)
122+
val fontFileUpdateRequest = FontFileUpdateRequest(pfd, ByteArray(0))
123+
124+
val fontRegular = FontFamilyUpdateRequest.Font.Builder(
125+
postScriptName,
126+
FontStyle()
127+
).build()
128+
129+
val familyRegular = FontFamilyUpdateRequest.FontFamily.Builder(
130+
DEFAULT_FONT_FAMILY,
131+
listOf(fontRegular)
132+
).build()
133+
134+
val updateRequest = FontFamilyUpdateRequest.Builder()
135+
.addFontFileUpdateRequest(fontFileUpdateRequest)
136+
.addFontFamily(familyRegular)
137+
.build()
138+
139+
val result = fontManager.updateFontFamily(
140+
updateRequest,
141+
fontManager.fontConfig.configVersion
142+
)
143+
144+
if (result != FontManager.RESULT_SUCCESS) {
145+
showToast(R.string.toast_failed_apply_font)
146+
false
147+
} else true
148+
}.getOrElse {
149+
Log.e(TAG, "Failed to update system font", it)
150+
false
151+
}
152+
}
153+
154+
private fun updateThemeOverlays() {
155+
val resolver = context.contentResolver
156+
val userId = UserHandle.myUserId()
157+
158+
val json = runCatching {
159+
val current = Settings.Secure.getStringForUser(
160+
resolver,
161+
Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
162+
userId
163+
)
164+
if (current.isNullOrEmpty()) JSONObject() else JSONObject(current)
165+
}.getOrElse { JSONObject() }
166+
167+
runCatching {
168+
if (json.has(OVERLAY_CATEGORY_FONT)) json.remove(OVERLAY_CATEGORY_FONT)
169+
json.put(OVERLAY_CATEGORY_FONT, DEFAULT_FONT_OVERLAY)
170+
171+
Settings.Secure.putStringForUser(
172+
resolver,
173+
Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
174+
json.toString(),
175+
userId
176+
)
177+
}.onFailure { Log.e(TAG, "Failed to persist custom font overlay", it) }
178+
}
179+
180+
private fun showToast(resId: Int) {
181+
Toast.makeText(context, resId, Toast.LENGTH_SHORT).show()
182+
}
183+
184+
fun resetFontUpdates() {
185+
fontManager.clearUpdates()
186+
cleanupPreviewFont()
187+
}
188+
189+
private fun cleanupPreviewFont() {
190+
try {
191+
val previewFile = File(context.cacheDir, TEMP_PREVIEW_FONT)
192+
if (previewFile.exists()) previewFile.delete()
193+
} catch (e: Exception) {
194+
Log.e(TAG, "Failed to cleanup preview font", e)
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)