v1.5.5
This commit is contained in:
@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
|
||||
}
|
||||
|
||||
// You can still set these for legacy use by submodules or scripts:
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.4").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("300").get().toInt())
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.5").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("302").get().toInt())
|
||||
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
|
||||
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
|
||||
// Include version code so each release has a different hash; use random for uniqueness within same version.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## v1.5.5
|
||||
- Fix: Custom Emoji now works for all devices!
|
||||
- New: Story preview in story batch download dialog
|
||||
- Fix: Auto Skip Stories getting stuck
|
||||
- Fix: Stories ending up saving in .dat format
|
||||
|
||||
## v1.5.4
|
||||
- Fix: Streak & Non-Streak category in Bulk Messaging Action for newer versions of snap
|
||||
- Fix: Spoof Coordinates Title
|
||||
|
||||
@@ -52,6 +52,39 @@ enum class FileType(
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun looksLikeIsoBmffVideo(array: ByteArray): Boolean {
|
||||
if (array.size < 12) return false
|
||||
// ISO BMFF containers like MP4 expose an `ftyp` box at byte offset 4.
|
||||
if (array[4] != 'f'.code.toByte() ||
|
||||
array[5] != 't'.code.toByte() ||
|
||||
array[6] != 'y'.code.toByte() ||
|
||||
array[7] != 'p'.code.toByte()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val majorBrand = String(array, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
|
||||
return majorBrand in setOf(
|
||||
"mp41",
|
||||
"mp42",
|
||||
"isom",
|
||||
"iso2",
|
||||
"iso3",
|
||||
"iso4",
|
||||
"iso5",
|
||||
"iso6",
|
||||
"avc1",
|
||||
"dash",
|
||||
"mif1",
|
||||
"msnv",
|
||||
"3gp4",
|
||||
"3gp5",
|
||||
"3gp6",
|
||||
"3g2a",
|
||||
"3g2b"
|
||||
)
|
||||
}
|
||||
|
||||
fun fromFile(file: File): FileType {
|
||||
file.inputStream().use { inputStream ->
|
||||
val buffer = ByteArray(16)
|
||||
@@ -64,7 +97,8 @@ enum class FileType(
|
||||
val headerBytes = ByteArray(16)
|
||||
System.arraycopy(array, 0, headerBytes, 0, 16)
|
||||
val hex = bytesToHex(headerBytes)
|
||||
return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value ?: UNKNOWN
|
||||
return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value
|
||||
?: if (looksLikeIsoBmffVideo(headerBytes)) MP4 else UNKNOWN
|
||||
}
|
||||
|
||||
fun fromInputStream(inputStream: InputStream): FileType {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
@@ -11,11 +12,14 @@ import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -26,17 +30,27 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectGlassCard
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
|
||||
@@ -84,8 +98,12 @@ import me.eternal.purrfectsnap.core.wrapper.impl.media.SnapCipherMode
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.media.toKeyPairUrlSafe
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.media.HybridEncryptionResolver
|
||||
import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.nio.file.Paths
|
||||
import java.util.UUID
|
||||
import java.util.Collections
|
||||
import java.util.IdentityHashMap
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.absoluteValue
|
||||
import android.util.Base64
|
||||
@@ -107,6 +125,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
|
||||
var lastSeenMapParams: ParamMap? = null
|
||||
private set
|
||||
private val storyPreviewCache = mutableMapOf<String, MutableMap<Int, Bitmap>>()
|
||||
@Volatile
|
||||
private var pendingBatchDownloadIndices: MutableList<Int>? = null
|
||||
@Volatile
|
||||
@@ -248,14 +267,67 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
val tr = context.translation.getCategory("download_processor.story_snap_dialog")
|
||||
val cancelStr = context.translation["button.cancel"]
|
||||
val downloadStr = context.translation["button.download"]
|
||||
|
||||
val previewCacheKey = buildString {
|
||||
append(paramMap["STORY_ID"]?.toString() ?: "story")
|
||||
append("|")
|
||||
append(paramMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString() ?: "user")
|
||||
append("|")
|
||||
append(totalCount)
|
||||
}
|
||||
context.runOnUiThread {
|
||||
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
PurrfectOverlayTheme {
|
||||
val selected = remember { mutableStateListOf<Int>().apply { add(currentIndex) } }
|
||||
val previewBitmaps = remember { mutableStateMapOf<Int, Bitmap?>() }
|
||||
val previewLoading = remember { mutableStateMapOf<Int, Boolean>() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!selected.contains(currentIndex)) selected.add(currentIndex)
|
||||
mediaInfoMap[SplitMediaAssetType.ORIGINAL]?.uri?.let { currentUri ->
|
||||
previewLoading[currentIndex] = true
|
||||
previewBitmaps[currentIndex] = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(currentUri) }
|
||||
previewLoading[currentIndex] = false
|
||||
}
|
||||
synchronized(storyPreviewCache) {
|
||||
storyPreviewCache[previewCacheKey]?.forEach { (index, bitmap) ->
|
||||
previewBitmaps[index] = bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(previewCacheKey) {
|
||||
val overlay = context.feature(OperaStoryOverlay::class)
|
||||
val cachedIndices = synchronized(storyPreviewCache) {
|
||||
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }.keys.toSet()
|
||||
}
|
||||
val indicesToScan = (0 until totalCount).filter { it != currentIndex && it !in cachedIndices }
|
||||
|
||||
try {
|
||||
for (targetIndex in indicesToScan) {
|
||||
val jumped = withContext(Dispatchers.Main) {
|
||||
overlay.requestJumpToSnap(targetIndex, totalCount)
|
||||
}
|
||||
if (!jumped) continue
|
||||
|
||||
val reached = waitForStoryIndex(targetIndex)
|
||||
if (!reached) continue
|
||||
|
||||
val uri = lastSeenMediaInfoMap?.get(SplitMediaAssetType.ORIGINAL)?.uri ?: continue
|
||||
previewLoading[targetIndex] = true
|
||||
val bitmap = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(uri) }
|
||||
previewLoading[targetIndex] = false
|
||||
if (bitmap != null) {
|
||||
previewBitmaps[targetIndex] = bitmap
|
||||
synchronized(storyPreviewCache) {
|
||||
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }[targetIndex] = bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
overlay.requestJumpToSnap(currentIndex, totalCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PurrfectGlassCard(
|
||||
@@ -280,7 +352,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = selected.contains(index),
|
||||
@@ -289,6 +362,35 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
},
|
||||
colors = CheckboxDefaults.colors(checkedColor = PurrfectOverlayPalette.glowPrimary)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(Color.White.copy(alpha = 0.06f), RoundedCornerShape(12.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val rowBitmap = previewBitmaps[index]
|
||||
val rowLoading = previewLoading[index] == true
|
||||
when {
|
||||
rowBitmap != null -> Image(
|
||||
bitmap = rowBitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(Color.Transparent, RoundedCornerShape(12.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
rowLoading -> CircularProgressIndicator(
|
||||
color = PurrfectOverlayPalette.glowPrimary,
|
||||
modifier = Modifier.size(22.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
else -> Icon(
|
||||
imageVector = Icons.Outlined.Image,
|
||||
contentDescription = null,
|
||||
tint = PurrfectOverlayPalette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -355,6 +457,49 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForStoryIndex(targetIndex: Int, timeoutMs: Long = 3000L): Boolean {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startedAt < timeoutMs) {
|
||||
if (lastSeenMapParams?.getStorySnapIndex() == targetIndex) return true
|
||||
kotlinx.coroutines.delay(60L)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadStoryPreviewBitmap(uriString: String): Bitmap? {
|
||||
return runCatching {
|
||||
val uri = Uri.parse(uriString)
|
||||
when (uri.scheme?.lowercase()) {
|
||||
"content" -> context.androidContext.contentResolver.openInputStream(uri)?.use(BitmapFactory::decodeStream)
|
||||
"file", null -> BitmapFactory.decodeFile(uri.path)
|
||||
"http", "https" -> {
|
||||
runCatching {
|
||||
OkHttpClient().newCall(Request.Builder().url(uriString).build()).execute().use { response ->
|
||||
response.body?.byteStream()?.use { stream -> BitmapFactory.decodeStream(stream) }
|
||||
}
|
||||
}.getOrNull() ?: run {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(uriString, emptyMap())
|
||||
retriever.frameAtTime
|
||||
} finally {
|
||||
runCatching { retriever.release() }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: run {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(context.androidContext, uri)
|
||||
retriever.frameAtTime
|
||||
} finally {
|
||||
runCatching { retriever.release() }
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun startBatchDownload(indices: MutableList<Int>, allowDuplicate: Boolean) {
|
||||
if (indices.isEmpty()) return
|
||||
val paramMap = lastSeenMapParams ?: return
|
||||
|
||||
@@ -22,6 +22,7 @@ class OperaStoryOverlayState {
|
||||
val totalCountState = mutableIntStateOf(0)
|
||||
val snapSourceState = mutableStateOf<String?>(null)
|
||||
val isInConversationState = mutableStateOf(false)
|
||||
val storyIdentityState = mutableStateOf<String?>(null)
|
||||
|
||||
fun setupDisplayStateHook(
|
||||
context: ModContext,
|
||||
@@ -63,6 +64,14 @@ class OperaStoryOverlayState {
|
||||
?: mediaParamMap["SNAP_POSITION_IN_STORY"]?.toString()?.toIntOrNull()
|
||||
val totalCount = mediaParamMap["snap_story_length"]?.toString()?.toIntOrNull()
|
||||
?: mediaParamMap["NUM_SNAPS_IN_STORY"]?.toString()?.toIntOrNull()
|
||||
val storyIdentity = mediaParamMap["STORY_ID"]?.toString()
|
||||
?.takeIf { it.isNotBlank() && it != "null" }
|
||||
?: mediaParamMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString()
|
||||
?.takeIf { it.isNotBlank() && it != "null" }
|
||||
?: mediaParamMap["PLAYABLE_STORY_SNAP_RECORD"]?.toString()
|
||||
?.substringAfter("storyUserId=", "")
|
||||
?.substringBefore(",")
|
||||
?.takeIf { it.isNotBlank() && it != "null" }
|
||||
|
||||
var mediaOrigin = ""
|
||||
if (showSourceIndicator) {
|
||||
@@ -81,6 +90,7 @@ class OperaStoryOverlayState {
|
||||
totalCountState.intValue = totalCount ?: 0
|
||||
snapSourceState.value = snapSource
|
||||
isInConversationState.value = false
|
||||
storyIdentityState.value = storyIdentity
|
||||
|
||||
onSnapFullyDisplayed?.let { callback ->
|
||||
if (currentIndex != null) callback(currentIndex)
|
||||
|
||||
@@ -26,6 +26,8 @@ class OperaStorySnapJump(
|
||||
private var retryRunnable: Runnable? = null
|
||||
private var nextTapRunnable: Runnable? = null
|
||||
private var lastHandledIndex = -1
|
||||
private var jumpOriginStoryIdentity: String? = null
|
||||
private var jumpOriginTotalCount: Int = 0
|
||||
|
||||
fun simulateTap(forward: Boolean) {
|
||||
val activity = context.mainActivity ?: return
|
||||
@@ -83,6 +85,8 @@ class OperaStorySnapJump(
|
||||
isJumping = false
|
||||
jumpTargetIndex = -1
|
||||
lastHandledIndex = -1
|
||||
jumpOriginStoryIdentity = null
|
||||
jumpOriginTotalCount = 0
|
||||
mainHandler.postDelayed({
|
||||
val overlay = storyFrameLayout()?.findViewWithTag<View>("jump_overlay") ?: return@postDelayed
|
||||
overlay.animate()
|
||||
@@ -99,6 +103,14 @@ class OperaStorySnapJump(
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
if (jumpOriginStoryIdentity != null && overlayState.storyIdentityState.value != null && jumpOriginStoryIdentity != overlayState.storyIdentityState.value) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
if (jumpOriginTotalCount > 0 && overlayState.totalCountState.intValue > 0 && jumpOriginTotalCount != overlayState.totalCountState.intValue) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
|
||||
val forward = jumpTargetIndex > fromIndex
|
||||
simulateTap(forward)
|
||||
@@ -114,6 +126,14 @@ class OperaStorySnapJump(
|
||||
val retry = Runnable {
|
||||
if (!isJumping || gen != jumpGeneration) return@Runnable
|
||||
val currentIdx = overlayState.currentIndexState.intValue
|
||||
if (jumpOriginStoryIdentity != null && overlayState.storyIdentityState.value != null && jumpOriginStoryIdentity != overlayState.storyIdentityState.value) {
|
||||
removeJumpOverlay()
|
||||
return@Runnable
|
||||
}
|
||||
if (jumpOriginTotalCount > 0 && overlayState.totalCountState.intValue > 0 && jumpOriginTotalCount != overlayState.totalCountState.intValue) {
|
||||
removeJumpOverlay()
|
||||
return@Runnable
|
||||
}
|
||||
if (currentIdx == fromIndex) {
|
||||
if (retryCount >= maxRetries) {
|
||||
removeJumpOverlay()
|
||||
@@ -129,6 +149,14 @@ class OperaStorySnapJump(
|
||||
fun onSnapFullyDisplayed(currentIndex: Int) {
|
||||
if (!isJumping || jumpTargetIndex < 0) return
|
||||
if (currentIndex == lastHandledIndex) return
|
||||
if (jumpOriginStoryIdentity != null && overlayState.storyIdentityState.value != null && jumpOriginStoryIdentity != overlayState.storyIdentityState.value) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
if (jumpOriginTotalCount > 0 && overlayState.totalCountState.intValue > 0 && jumpOriginTotalCount != overlayState.totalCountState.intValue) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
|
||||
cancelPendingRetry()
|
||||
cancelPendingNextTap()
|
||||
@@ -138,6 +166,18 @@ class OperaStorySnapJump(
|
||||
return
|
||||
}
|
||||
|
||||
if (lastHandledIndex >= 0) {
|
||||
val expectedForward = jumpTargetIndex > lastHandledIndex
|
||||
if (expectedForward && currentIndex < lastHandledIndex) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
if (!expectedForward && currentIndex > lastHandledIndex) {
|
||||
removeJumpOverlay()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastHandledIndex = currentIndex
|
||||
val gen = jumpGeneration
|
||||
val tapRunnable = Runnable {
|
||||
@@ -179,6 +219,8 @@ class OperaStorySnapJump(
|
||||
jumpTargetIndex = targetIndex
|
||||
lastHandledIndex = -1
|
||||
isJumping = true
|
||||
jumpOriginStoryIdentity = overlayState.storyIdentityState.value
|
||||
jumpOriginTotalCount = overlayState.totalCountState.intValue
|
||||
|
||||
showJumpOverlay()
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
nativeAbis=arm64-v8a
|
||||
|
||||
APP_VERSION_NAME=1.5.4
|
||||
APP_VERSION_CODE=300
|
||||
APP_VERSION_NAME=1.5.5
|
||||
APP_VERSION_CODE=302
|
||||
debug_build_hash=18fe2a814d0e2eb5
|
||||
psIntegrityPinnedSha256=
|
||||
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c
|
||||
|
||||
@@ -1,31 +1,70 @@
|
||||
use std::{ffi::{CStr, CString}, fs};
|
||||
use std::{cell::Cell, ffi::{CStr, CString}};
|
||||
|
||||
use nix::libc::{self, c_uint};
|
||||
|
||||
use crate::{config, def_hook, dobby_hook_sym};
|
||||
|
||||
thread_local! {
|
||||
static FONT_REDIRECT_IN_PROGRESS: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
fn should_redirect_font(pathname: &str) -> bool {
|
||||
let normalized = pathname.replace('\\', "/");
|
||||
let file_name = normalized.rsplit('/').next().unwrap_or(&normalized).to_ascii_lowercase();
|
||||
let is_font_file = file_name.ends_with(".ttf")
|
||||
|| file_name.ends_with(".ttc")
|
||||
|| file_name.ends_with(".otf");
|
||||
let is_system_font_path = normalized.starts_with("/system/fonts/")
|
||||
|| normalized.starts_with("/product/fonts/")
|
||||
|| normalized.starts_with("/system_ext/fonts/")
|
||||
|| normalized.starts_with("/vendor/fonts/");
|
||||
|
||||
is_system_font_path && is_font_file && (
|
||||
file_name.contains("emoji")
|
||||
|| file_name == "noto_color_emoji.ttf"
|
||||
|| file_name == "samsungcoloremoji.ttf"
|
||||
)
|
||||
}
|
||||
|
||||
fn open_custom_font_fd(flags: i32, mode: c_uint) -> Option<i32> {
|
||||
let font_path = config::native_config().custom_emoji_font_path.clone()?;
|
||||
|
||||
match CString::new(font_path.clone()) {
|
||||
Ok(c_font_path) => {
|
||||
let fd = FONT_REDIRECT_IN_PROGRESS.with(|guard| {
|
||||
let was_active = guard.replace(true);
|
||||
let fd = unsafe { libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const u8, flags, mode) };
|
||||
guard.set(was_active);
|
||||
fd
|
||||
});
|
||||
if fd >= 0 {
|
||||
debug!("redirected emoji font open to {}", font_path);
|
||||
Some(fd)
|
||||
} else {
|
||||
debug!("failed to open custom emoji font path (fd={}): {}", fd, font_path);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("custom emoji font path contains null byte, using fallback system font");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def_hook!(
|
||||
open_hook,
|
||||
i32,
|
||||
|path: *const u8, flags: i32, mode: c_uint| {
|
||||
if let Ok(pathname) = CStr::from_ptr(path).to_str() {
|
||||
if pathname == "/system/fonts/NotoColorEmoji.ttf" {
|
||||
if let Some(font_path) = config::native_config().custom_emoji_font_path {
|
||||
if fs::metadata(&font_path).is_ok() {
|
||||
match CString::new(font_path.clone()) {
|
||||
Ok(c_font_path) => {
|
||||
let fd = libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const u8, flags, mode);
|
||||
if fd >= 0 {
|
||||
return fd;
|
||||
}
|
||||
warn!("failed to open custom emoji font path (fd={}): {}", fd, font_path);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("custom emoji font path contains null byte, using fallback system font");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("custom emoji font path does not exist: {}", font_path);
|
||||
if FONT_REDIRECT_IN_PROGRESS.with(|guard| guard.get()) {
|
||||
return open_hook_original.unwrap()(path, flags, mode);
|
||||
}
|
||||
|
||||
if !path.is_null() {
|
||||
if let Ok(pathname) = CStr::from_ptr(path).to_str() {
|
||||
if should_redirect_font(pathname) {
|
||||
if let Some(fd) = open_custom_font_fd(flags, mode) {
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +74,6 @@ def_hook!(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
pub fn init() {
|
||||
if config::native_config().custom_emoji_font_path.is_none() {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user