mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 04:20:20 +02:00
feat: half swipe notifier
This commit is contained in:
parent
a568b9c1c6
commit
dc30d4ee25
@ -344,6 +344,10 @@
|
||||
"name": "Disable Replay in FF",
|
||||
"description": "Disables the ability to replay with a long press from the Friend Feed"
|
||||
},
|
||||
"half_swipe_notifier": {
|
||||
"name": "Half Swipe Notifier",
|
||||
"description": "Notifies you when someone half swipes into a conversation"
|
||||
},
|
||||
"message_preview_length": {
|
||||
"name": "Message Preview Length",
|
||||
"description": "Specify the amount of messages to get previewed"
|
||||
@ -865,6 +869,12 @@
|
||||
"dialog_message": "Are you sure you want to start a call?"
|
||||
},
|
||||
|
||||
"half_swipe_notifier": {
|
||||
"notification_channel_name": "Half Swipe",
|
||||
"notification_content_dm": "{friend} just half-swiped into your chat for {duration} seconds",
|
||||
"notification_content_group": "{friend} just half-swiped into {group} for {duration} seconds"
|
||||
},
|
||||
|
||||
"download_processor": {
|
||||
"attachment_type": {
|
||||
"snap": "Snap",
|
||||
|
@ -12,6 +12,7 @@ class MessagingTweaks : ConfigContainer() {
|
||||
val hideTypingNotifications = boolean("hide_typing_notifications")
|
||||
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
|
||||
val disableReplayInFF = boolean("disable_replay_in_ff")
|
||||
val halfSwipeNotifier = boolean("half_swipe_notifier") { requireRestart() }
|
||||
val messagePreviewLength = integer("message_preview_length", defaultValue = 20)
|
||||
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
|
||||
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",
|
||||
|
@ -23,10 +23,11 @@ class ConfigurationOverride : Feature("Configuration Override", loadParams = Fea
|
||||
fun getConfigKeyInfo(key: Any?) = runCatching {
|
||||
if (key == null) return@runCatching null
|
||||
val keyClassMethods = key::class.java.methods
|
||||
val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString()
|
||||
val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null
|
||||
val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null
|
||||
val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null
|
||||
ConfigKeyInfo(category, key.toString(), defaultValue)
|
||||
ConfigKeyInfo(category, keyName, defaultValue)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to get config key info", it)
|
||||
}.getOrNull()
|
||||
|
@ -117,7 +117,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
|
||||
private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, message: Message, notificationData: NotificationData) {
|
||||
val actions = mutableListOf<Notification.Action>()
|
||||
actions.addAll(notificationData.notification.actions)
|
||||
actions.addAll(notificationData.notification.actions ?: emptyArray())
|
||||
|
||||
fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) {
|
||||
if (!filter()) return
|
||||
|
@ -0,0 +1,127 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.spying
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import me.rhunk.snapenhance.common.Constants
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
private val peekingConversations = ConcurrentHashMap<String, List<String>>()
|
||||
private val startPeekingTimestamps = ConcurrentHashMap<String, Long>()
|
||||
|
||||
private val svgEyeDrawable by lazy { context.resources.getIdentifier("svg_eye_24x24", "drawable") }
|
||||
private val notificationManager get() = context.androidContext.getSystemService(NotificationManager::class.java)
|
||||
private val translation by lazy { context.translation.getCategory("half_swipe_notifier")}
|
||||
private val channelId by lazy {
|
||||
"peeking".also {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
it,
|
||||
translation["notification_channel_name"],
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun init() {
|
||||
if (!context.config.messaging.halfSwipeNotifier.get()) return
|
||||
lateinit var presenceService: Any
|
||||
|
||||
findClass("com.snapchat.talkcorev3.PresenceService\$CppProxy").hookConstructor(HookStage.AFTER) {
|
||||
presenceService = it.thisObject()
|
||||
}
|
||||
|
||||
PendingIntent::class.java.methods.find { it.name == "getActivity" }?.hook(HookStage.BEFORE) { param ->
|
||||
context.log.verbose(param.args().toList())
|
||||
}
|
||||
|
||||
context.mappings.getMappedClass("callbacks", "PresenceServiceDelegate")
|
||||
.hook("notifyActiveConversationsChanged", HookStage.BEFORE) {
|
||||
val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants)
|
||||
|
||||
if (activeConversations.isEmpty()) {
|
||||
peekingConversations.forEach {
|
||||
val conversationId = it.key
|
||||
val peekingParticipantsIds = it.value
|
||||
peekingParticipantsIds.forEach { userId ->
|
||||
endPeeking(conversationId, userId)
|
||||
}
|
||||
}
|
||||
peekingConversations.clear()
|
||||
return@hook
|
||||
}
|
||||
|
||||
activeConversations.forEach { (conversationId, conversationInfo) ->
|
||||
val peekingParticipantsIds = (conversationInfo?.getObjectField("mPeekingParticipants") as? List<*>)?.map { it.toString() } ?: return@forEach
|
||||
val cachedPeekingParticipantsIds = peekingConversations[conversationId] ?: emptyList()
|
||||
|
||||
val newPeekingParticipantsIds = peekingParticipantsIds - cachedPeekingParticipantsIds.toSet()
|
||||
val exitedPeekingParticipantsIds = cachedPeekingParticipantsIds - peekingParticipantsIds.toSet()
|
||||
|
||||
newPeekingParticipantsIds.forEach { userId ->
|
||||
startPeeking(conversationId.toString(), userId)
|
||||
}
|
||||
|
||||
exitedPeekingParticipantsIds.forEach { userId ->
|
||||
endPeeking(conversationId.toString(), userId)
|
||||
}
|
||||
peekingConversations[conversationId.toString()] = peekingParticipantsIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPeeking(conversationId: String, userId: String) {
|
||||
startPeekingTimestamps[conversationId + userId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun endPeeking(conversationId: String, userId: String) {
|
||||
startPeekingTimestamps[conversationId + userId]?.let { startPeekingTimestamp ->
|
||||
val peekingDuration = (System.currentTimeMillis() - startPeekingTimestamp).milliseconds.inWholeSeconds.toString()
|
||||
val groupName = context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName
|
||||
val friendInfo = context.database.getFriendInfo(userId) ?: return
|
||||
|
||||
Notification.Builder(context.androidContext, channelId)
|
||||
.setContentTitle(groupName ?: friendInfo.displayName ?: friendInfo.mutableUsername)
|
||||
.setContentText(if (groupName != null) {
|
||||
translation.format("notification_content_group",
|
||||
"friend" to (friendInfo.displayName ?: friendInfo.mutableUsername).toString(),
|
||||
"group" to groupName,
|
||||
"duration" to peekingDuration
|
||||
)
|
||||
} else {
|
||||
translation.format("notification_content_dm",
|
||||
"friend" to (friendInfo.displayName ?: friendInfo.mutableUsername).toString(),
|
||||
"duration" to peekingDuration
|
||||
)
|
||||
})
|
||||
.setContentIntent(
|
||||
context.androidContext.packageManager.getLaunchIntentForPackage(
|
||||
Constants.SNAPCHAT_PACKAGE_NAME
|
||||
)?.let {
|
||||
PendingIntent.getActivity(
|
||||
context.androidContext,
|
||||
0, it, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(svgEyeDrawable)
|
||||
.build()
|
||||
.let { notification ->
|
||||
notificationManager.notify(System.nanoTime().toInt(), notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import me.rhunk.snapenhance.core.features.impl.experiments.*
|
||||
import me.rhunk.snapenhance.core.features.impl.global.*
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.*
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks
|
||||
import me.rhunk.snapenhance.core.features.impl.tweaks.BypassScreenshotDetection
|
||||
@ -105,6 +106,7 @@ class FeatureManager(
|
||||
SnapPreview::class,
|
||||
InstantDelete::class,
|
||||
BypassScreenshotDetection::class,
|
||||
HalfSwipeNotifier::class,
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
Loading…
x
Reference in New Issue
Block a user