This commit is contained in:
ΞTΞRNAL
2026-03-28 12:57:15 +05:30
parent 00c5d60b7b
commit 21fba253b1
11 changed files with 226 additions and 10 deletions

View File

@@ -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.8").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("306").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.9").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("308").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.

View File

@@ -1,3 +1,7 @@
## v1.5.9
- New: Block Calls Feature!
- New: Unlock Zoom Limit Feature!
## v1.5.8
- New: Implemented 30Mbps Video and 320kbps/48kHz Audio bitrates(tq to Kaladin)
- Fix: Advanced hardware ISP processing modes for superior dynamic range and less noisy video footage(tq to Kaladin)

View File

@@ -1292,6 +1292,10 @@
"name": "Call Start Confirmation",
"description": "Shows a confirmation dialog when starting a call"
},
"block_calls": {
"name": "Block Calls",
"description": "Blocks Snapchat call session updates so call UI and incoming call overlays do not appear"
},
"unlimited_conversation_pinning": {
"name": "Unlimited Conversation Pinning",
"description": "Allows you to pin an unlimited amount of conversations locally"
@@ -2010,6 +2014,14 @@
"camera_tweaks": { "name": "Upgraded Camera Engine", "description": "Enables professional hardware ISP processing modes for better dynamic range" }, "audio_video": { "name": "Upgraded Audio and Video", "description": "Increases Video bitrate to 30Mbps and Audio to 320kbps/48kHz" }, "video_record_timer": {
"name": "Video Recording Timer",
"description": "Shows a recording timer overlay when recording video"
},
"unlock_zoom_limit": {
"name": "Unlock Zoom Limit",
"description": "Overrides the max camera zoom Snapchat reads from the device"
},
"max_zoom_override": {
"name": "Max Zoom Override",
"description": "Maximum zoom ratio to report to Snapchat, for example 120"
}
}
},

View File

@@ -58,6 +58,12 @@ class Camera : ConfigContainer() {
val overrideFrontResolution get() = _overrideFrontResolution
val overrideBackResolution get() = _overrideBackResolution
val videoRecordTimer = boolean("video_record_timer")
val unlockZoomLimit = boolean("unlock_zoom_limit") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val maxZoomOverride = float("max_zoom_override", 120f) {
requireRestart()
addFlags(ConfigFlag.NO_TRANSLATE)
inputCheck = { (it.toFloatOrNull() ?: 0f) in 1f..500f }
}
val audioVideoOptimizations = boolean("audio_video", defaultValue = true) { requireRestart() }
val cameraOptimizations = boolean("camera_tweaks", defaultValue = false) { addNotices(FeatureNotice.UNSTABLE); requireRestart() }

View File

@@ -222,6 +222,7 @@ class MessagingTweaks : ConfigContainer() {
val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
val blockCalls = boolean("block_calls") { requireRestart() }
val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() }
val disableSnapModeRestrictions = boolean("disable_snap_mode_restrictions") { requireRestart() }
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",

View File

@@ -114,6 +114,7 @@ class FeatureManager(
HideFriendFeedEntry(),
RequerySqlite(),
RefreshFriendSuggestions(),
BlockCalls(),
CallButtonsOverride(),
SnapPreview(),
BypassScreenshotDetection(),

View File

@@ -7,10 +7,10 @@ import me.eternal.purrfectsnap.core.util.hook.hook
class DisableTelecomFramework: Feature("Disable Telecom Framework") {
override fun init() {
if (!context.config.global.disableTelecomFramework.get()) return
if (!context.config.global.disableTelecomFramework.get() && !context.config.messaging.blockCalls.get()) return
ContextWrapper::class.java.hook("getSystemService", HookStage.BEFORE) { param ->
if (param.arg<Any>(0).toString() == "telecom") param.setResult(null)
}
}
}
}

View File

@@ -0,0 +1,171 @@
package me.eternal.purrfectsnap.core.features.impl.messaging
import android.app.Notification
import android.app.NotificationManager
import android.content.Intent
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
import me.eternal.purrfectsnap.core.event.events.impl.AddViewEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.ui.hideViewCompletely
import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
import me.eternal.purrfectsnap.core.util.ktx.setObjectField
import java.nio.ByteBuffer
class BlockCalls : Feature("Block Calls") {
private fun isBlockedCallNotificationType(type: String?): Boolean {
return type?.lowercase() in setOf(
"initiate_audio",
"initiate_video",
"abandon_audio",
"abandon_video"
)
}
private fun shouldBlockVolatilePayload(eventData: ProtoReader): Boolean {
val dump = eventData.toString().lowercase()
return listOf(
"\"calluuid\"",
"\"callaction\"",
"\"messagetype\":\"caller_push\"",
"\"messagetype\":\"streamer_data_v2\"",
"\"messagetype\":\"callee_push\"",
"\"messagetype\":\"caller_hangup\"",
"\"messagetype\":\"call_end\""
).any { it in dump }
}
override fun init() {
if (!context.config.messaging.blockCalls.get()) return
runCatching {
findClass("com.google.firebase.messaging.FirebaseMessagingService")
.methods
.first {
it.declaringClass.name == "com.google.firebase.messaging.FirebaseMessagingService" &&
it.returnType == Void::class.javaPrimitiveType &&
it.parameterCount == 1 &&
it.parameterTypes[0] == Intent::class.java
}
.hook(HookStage.BEFORE) { param ->
val intent = param.argNullable<Intent>(0) ?: return@hook
if (!isBlockedCallNotificationType(intent.getStringExtra("type"))) return@hook
context.log.verbose("Blocked Firebase call message ${intent.getStringExtra("type")}", "BlockCalls")
param.setResult(null)
}
}
runCatching {
NotificationManager::class.java.findRestrictedMethod { it.name == "notifyAsUser" }?.hook(HookStage.BEFORE) { param ->
val notification = param.argNullable<Notification>(2) ?: return@hook
val notificationType = notification.extras
?.getBundle("system_notification_extras")
?.getString("notification_type")
if (!isBlockedCallNotificationType(notificationType)) return@hook
context.log.verbose("Blocked call notification $notificationType", "BlockCalls")
param.setResult(null)
}
}
runCatching {
findClass("com.snapchat.client.duplex.MessageHandler\$CppProxy").hook("onReceive", HookStage.BEFORE) { param ->
val buffer = param.argNullable<ByteBuffer>(0) ?: return@hook
val duplicate = buffer.duplicate().apply { position(0) }
val bytes = ByteArray(duplicate.limit())
duplicate.get(bytes)
val reader = ProtoReader(bytes)
val eventType = reader.getString(1, 1) ?: return@hook
if (eventType != "volatile") return@hook
val eventData = reader.followPath(1, 2) ?: return@hook
if (!shouldBlockVolatilePayload(eventData)) return@hook
context.log.verbose("Blocked volatile call payload", "BlockCalls")
param.setResult(null)
}
}
val talkCoreNames = listOf(
"com.snapchat.talkcorev3.TalkCore\$CppProxy",
"com.snapchat.talkcorev4.TalkCore\$CppProxy",
"com.snapchat.talkcore.TalkCore\$CppProxy"
)
talkCoreNames.forEach { className ->
runCatching {
findClass(className).apply {
hook("updateTSCallingSession", HookStage.BEFORE) { param ->
val params = param.argNullable<Any>(0)
val conversationId = params?.getObjectFieldOrNull("mConversationId")?.toString()
val inCall = params?.getObjectFieldOrNull("mInCall") as? Boolean
context.log.verbose(
"Blocked talk session update inCall=$inCall convo=$conversationId",
"BlockCalls"
)
param.setResult(null)
}
hook("disposeTSCallingSession", HookStage.BEFORE) { param ->
context.log.verbose("Blocked talk session dispose", "BlockCalls")
param.setResult(null)
}
}
}
}
listOf(
"com.snapchat.talkcorev3.TSCallingStateUpdateParams",
"com.snapchat.talkcorev4.TSCallingStateUpdateParams",
"com.snapchat.talkcore.TSCallingStateUpdateParams"
).forEach { className ->
runCatching {
findClass(className).hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
val inCall = instance.getObjectFieldOrNull("mInCall") as? Boolean ?: return@hookConstructor
if (!inCall) return@hookConstructor
instance.setObjectField("mInCall", false)
context.log.verbose("Forced TSCallingStateUpdateParams.mInCall=false", "BlockCalls")
}
}
}
context.event.subscribe(AddViewEvent::class) { event ->
val viewName = event.viewClassName.lowercase()
val parentName = event.parent.javaClass.name.lowercase()
val exactCallUi = setOf(
"com.snap.talk.callviewwrapper",
"com.snap.talk.core.callcontainer"
)
val callUiParents = setOf(
"com.snap.talk.core.callcontainer"
)
if (viewName in exactCallUi || parentName in callUiParents) {
context.log.verbose(
"Suppressed view ${event.viewClassName} parent=${event.parent.javaClass.name}",
"BlockCalls"
)
event.view.hideViewCompletely()
return@subscribe
}
if (viewName.endsWith("callbuttonsview") ||
(viewName.contains("call") && (
viewName.contains("overlay") ||
viewName.contains("incoming") ||
viewName.contains("ringing") ||
viewName.contains("ringer")
))
) {
event.view.hideViewCompletely()
}
}
}
}

View File

@@ -80,12 +80,13 @@ class CallButtonsOverride : Feature("CallButtonsOverride") {
override fun init() {
val hideUiComponents by context.config.userInterface.hideUiComponents
val blockCalls = context.config.messaging.blockCalls.get()
val hideProfileCallButtons = hideUiComponents.contains("hide_profile_call_buttons")
val hideChatCallButtons = hideUiComponents.contains("hide_chat_call_buttons")
val hideProfileCallButtons = blockCalls || hideUiComponents.contains("hide_profile_call_buttons")
val hideChatCallButtons = blockCalls || hideUiComponents.contains("hide_chat_call_buttons")
val callStartConfirmation = context.config.messaging.callStartConfirmation.get()
if (!hideProfileCallButtons && !hideChatCallButtons && !callStartConfirmation) return
if (!hideProfileCallButtons && !hideChatCallButtons && !callStartConfirmation && !blockCalls) return
var actionSheetVideoCallButtonId = -1
var actionSheetAudioCallButtonId = -1
@@ -111,7 +112,7 @@ class CallButtonsOverride : Feature("CallButtonsOverride") {
}
onNextActivityCreate {
if (callStartConfirmation) {
if (callStartConfirmation || blockCalls) {
(runCatching { findClass("com.snap.valdi.views.ValdiRootView") }.getOrNull()
?: findClass("com.snap.composer.views.ComposerRootView"))
.hook("dispatchTouchEvent", HookStage.BEFORE) { param ->
@@ -122,6 +123,10 @@ class CallButtonsOverride : Feature("CallButtonsOverride") {
if (childComposerView.children().count {
it::class.java == childComposerView::class.java
} != 2) return@hook
if (blockCalls) {
param.setResult(true)
return@hook
}
hookTouchEvent(param, param.arg(0)) {
param.invokeOriginal()
}
@@ -131,6 +136,10 @@ class CallButtonsOverride : Feature("CallButtonsOverride") {
val view = param.thisObject<View>().takeIf { it.id != -1 } ?: return@hook
if (view.id != actionSheetAudioCallButtonId && view.id != actionSheetVideoCallButtonId) return@hook
if (blockCalls) {
param.setResult(true)
return@hook
}
hookTouchEvent(param, param.arg(0)) {
arrayOf(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0),

View File

@@ -135,6 +135,18 @@ class CameraTweaks : Feature("Camera Tweaks") {
}
}
if (config.unlockZoomLimit.get()) {
val maxZoom = config.maxZoomOverride.get().coerceAtLeast(1f)
when {
key == CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM -> {
param.setResult(maxZoom)
}
key.name == "android.control.zoomRatioRange" -> {
param.setResult(Range(1f, maxZoom))
}
}
}
if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) {
val isFrontCamera = param.invokeOriginal(
arrayOf(CameraCharacteristics.LENS_FACING)

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.5.8
APP_VERSION_CODE=306
APP_VERSION_NAME=1.5.9
APP_VERSION_CODE=308
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c