many fixes & improvements
This commit is contained in:
@@ -83,16 +83,37 @@ class BridgeService : Service() {
|
||||
callback.syncGroup(id)
|
||||
}
|
||||
} ?: run {
|
||||
if (updateOnly) {
|
||||
when (scope) {
|
||||
SocialScope.FRIEND -> database.deleteFriend(id)
|
||||
SocialScope.GROUP -> database.deleteGroup(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
remoteSideContext.log.warn("Failed to sync $scope $id")
|
||||
return
|
||||
}
|
||||
|
||||
when (scope) {
|
||||
SocialScope.FRIEND -> {
|
||||
toParcelable<MessagingFriendInfo>(syncedObject)?.let { database.syncFriend(it) }
|
||||
toParcelable<MessagingFriendInfo>(syncedObject)?.let { database.syncFriend(it) } ?: run {
|
||||
if (updateOnly) {
|
||||
database.deleteFriend(id)
|
||||
return
|
||||
}
|
||||
remoteSideContext.log.warn("Failed to sync $scope $id")
|
||||
return
|
||||
}
|
||||
}
|
||||
SocialScope.GROUP -> {
|
||||
toParcelable<MessagingGroupInfo>(syncedObject)?.let { database.syncGroupInfo(it) }
|
||||
toParcelable<MessagingGroupInfo>(syncedObject)?.let { database.syncGroupInfo(it) } ?: run {
|
||||
if (updateOnly) {
|
||||
database.deleteGroup(id)
|
||||
return
|
||||
}
|
||||
remoteSideContext.log.warn("Failed to sync $scope $id")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
@@ -218,10 +239,10 @@ class BridgeService : Service() {
|
||||
friends: List<String>
|
||||
) {
|
||||
remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends")
|
||||
remoteSideContext.database.receiveMessagingDataCallback(
|
||||
friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) },
|
||||
groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
|
||||
)
|
||||
val parsedFriends = friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) }
|
||||
val parsedGroups = groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
|
||||
remoteSideContext.database.replaceMessagingData(parsedFriends, parsedGroups)
|
||||
remoteSideContext.database.receiveMessagingDataCallback(parsedFriends, parsedGroups)
|
||||
}
|
||||
|
||||
override fun getScopeNotes(id: String): String? {
|
||||
|
||||
@@ -86,6 +86,76 @@ fun AppDatabase.syncFriend(friend: MessagingFriendInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
fun AppDatabase.replaceMessagingData(
|
||||
friends: List<MessagingFriendInfo>,
|
||||
groups: List<MessagingGroupInfo>
|
||||
) {
|
||||
executeAsync {
|
||||
database.beginTransaction()
|
||||
try {
|
||||
val friendIds = friends.map { it.userId }.toSet()
|
||||
val groupIds = groups.map { it.conversationId }.toSet()
|
||||
|
||||
getFriends().forEach { friend ->
|
||||
if (friend.userId !in friendIds) {
|
||||
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(friend.userId))
|
||||
database.execSQL("DELETE FROM streaks WHERE id = ?", arrayOf(friend.userId))
|
||||
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(friend.userId))
|
||||
}
|
||||
}
|
||||
|
||||
getGroups().forEach { group ->
|
||||
if (group.conversationId !in groupIds) {
|
||||
database.execSQL("DELETE FROM groups WHERE conversationId = ?", arrayOf(group.conversationId))
|
||||
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(group.conversationId))
|
||||
}
|
||||
}
|
||||
|
||||
friends.forEach { friend ->
|
||||
database.execSQL(
|
||||
"INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
arrayOf<Any?>(
|
||||
friend.userId,
|
||||
friend.dmConversationId,
|
||||
friend.displayName,
|
||||
friend.mutableUsername,
|
||||
friend.bitmojiId,
|
||||
friend.selfieId
|
||||
)
|
||||
)
|
||||
|
||||
friend.streaks?.takeIf { it.length > 0 }?.also {
|
||||
val streaks = getFriendStreaks(friend.userId)
|
||||
database.execSQL(
|
||||
"INSERT OR REPLACE INTO streaks (id, notify, expirationTimestamp, length) VALUES (?, ?, ?, ?)",
|
||||
arrayOf<Any?>(
|
||||
friend.userId,
|
||||
streaks?.notify != false,
|
||||
it.expirationTimestamp,
|
||||
it.length
|
||||
)
|
||||
)
|
||||
} ?: database.execSQL("DELETE FROM streaks WHERE id = ?", arrayOf(friend.userId))
|
||||
}
|
||||
|
||||
groups.forEach { group ->
|
||||
database.execSQL(
|
||||
"INSERT OR REPLACE INTO groups (conversationId, name, participantsCount) VALUES (?, ?, ?)",
|
||||
arrayOf<Any?>(
|
||||
group.conversationId,
|
||||
group.name,
|
||||
group.participantsCount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AppDatabase.getRules(targetUuid: String): List<MessagingRuleType> {
|
||||
return database.rawQuery(
|
||||
"SELECT type FROM rules WHERE targetUuid = ?", arrayOf(targetUuid)
|
||||
|
||||
@@ -37,11 +37,13 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import kotlinx.coroutines.launch
|
||||
import me.eternal.purrfectsnap.R
|
||||
import me.eternal.purrfectsnap.common.ReceiversConfig
|
||||
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
|
||||
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
|
||||
import me.eternal.purrfectsnap.common.data.SocialScope
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
|
||||
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||
import me.eternal.purrfectsnap.storage.*
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
@@ -58,6 +60,16 @@ class SocialRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLatestSnapshot() {
|
||||
runCatching {
|
||||
context.androidContext.sendBroadcast(
|
||||
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}
|
||||
)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to request latest social snapshot", it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScopeList(
|
||||
scope: SocialScope,
|
||||
@@ -196,7 +208,17 @@ class SocialRootSection : Routes.Route() {
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
context.database.receiveMessagingDataCallback = { friends, groups ->
|
||||
friendList = friends
|
||||
groupList = groups
|
||||
}
|
||||
updateScopeLists()
|
||||
requestLatestSnapshot()
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
context.database.receiveMessagingDataCallback = { _, _ -> }
|
||||
}
|
||||
}
|
||||
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
|
||||
val filteredFriends = remember(friendList, normalizedQuery) {
|
||||
|
||||
@@ -54,6 +54,9 @@ class MappingsWrapper(
|
||||
if (!exists()) {
|
||||
throw Exception("Mappings file does not exist")
|
||||
}
|
||||
mappers.values.forEach { mapper ->
|
||||
mapper.classLoader = context.classLoader
|
||||
}
|
||||
val mappingsObject = JsonParser.parseString(readBytes().toString(Charsets.UTF_8)).asJsonObject.also {
|
||||
mappingUniqueHash = it["unique_hash"].asLong
|
||||
}
|
||||
@@ -61,7 +64,6 @@ class MappingsWrapper(
|
||||
mappingsObject.entrySet().forEach { (key, value) ->
|
||||
mappers.values.firstOrNull { it.mapperName == key }?.let { mapper ->
|
||||
mapper.readFromJson(value.asJsonObject)
|
||||
mapper.classLoader = context.classLoader
|
||||
}
|
||||
}
|
||||
isMappingsLoaded = true
|
||||
@@ -74,6 +76,9 @@ class MappingsWrapper(
|
||||
fileHandleManager.value.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.NATIVE_SIG_CACHE.key).delete()
|
||||
|
||||
val classMapper = ClassMapper(*mappers.values.toTypedArray())
|
||||
mappers.values.forEach { mapper ->
|
||||
mapper.classLoader = context.classLoader
|
||||
}
|
||||
|
||||
runCatching {
|
||||
classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK"))
|
||||
@@ -88,6 +93,8 @@ class MappingsWrapper(
|
||||
writeBytes(result.toString().toByteArray())
|
||||
}
|
||||
|
||||
loadCached()
|
||||
|
||||
return classMapper.getWarns()
|
||||
}
|
||||
|
||||
@@ -99,4 +106,4 @@ class MappingsWrapper(
|
||||
AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.bridge.SyncCallback
|
||||
import me.eternal.purrfectsnap.common.Constants
|
||||
import me.eternal.purrfectsnap.common.ReceiversConfig
|
||||
import me.eternal.purrfectsnap.common.action.EnumAction
|
||||
import me.eternal.purrfectsnap.common.data.FriendLinkType
|
||||
import me.eternal.purrfectsnap.common.bridge.FileHandleScope
|
||||
import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType
|
||||
import me.eternal.purrfectsnap.common.bridge.toWrapper
|
||||
@@ -454,21 +455,29 @@ class PurrfectSnap {
|
||||
)
|
||||
}
|
||||
|
||||
val friends = feedEntries.filter { it.conversationType == 0 }.mapNotNull {
|
||||
val friendUserId = it.friendUserId ?: it.participants?.firstOrNull { it != appContext.database.myUserId }
|
||||
?: return@mapNotNull null
|
||||
val friend = appContext.database.getFriendInfo(friendUserId) ?: return@mapNotNull null
|
||||
|
||||
MessagingFriendInfo(
|
||||
friendUserId,
|
||||
it.key,
|
||||
friend.displayName,
|
||||
friend.mutableUsername ?: friend.usernameForSorting!!,
|
||||
friend.bitmojiAvatarId,
|
||||
friend.bitmojiSelfieId,
|
||||
streaks = null
|
||||
)
|
||||
}
|
||||
val friends = appContext.database.getAllFriends()
|
||||
.asSequence()
|
||||
.filter { friend ->
|
||||
friend.userId != null && when (FriendLinkType.fromValue(friend.friendLinkType)) {
|
||||
FriendLinkType.DELETED,
|
||||
FriendLinkType.BLOCKED,
|
||||
FriendLinkType.SUGGESTED -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.mapNotNull { friend ->
|
||||
val userId = friend.userId ?: return@mapNotNull null
|
||||
MessagingFriendInfo(
|
||||
userId = userId,
|
||||
dmConversationId = appContext.database.getDMConversationId(userId),
|
||||
displayName = friend.displayName,
|
||||
mutableUsername = friend.mutableUsername ?: friend.usernameForSorting ?: return@mapNotNull null,
|
||||
bitmojiId = friend.bitmojiAvatarId,
|
||||
selfieId = friend.bitmojiSelfieId,
|
||||
streaks = null
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
appContext.bridgeClient.passGroupsAndFriends(groups, friends)
|
||||
}
|
||||
@@ -485,9 +494,15 @@ class PurrfectSnap {
|
||||
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
|
||||
override fun syncFriend(uuid: String): String? {
|
||||
return appContext.database.getFriendInfo(uuid)?.let {
|
||||
if (FriendLinkType.fromValue(it.friendLinkType) in setOf(
|
||||
FriendLinkType.DELETED,
|
||||
FriendLinkType.BLOCKED,
|
||||
FriendLinkType.SUGGESTED
|
||||
)
|
||||
) return@let null
|
||||
MessagingFriendInfo(
|
||||
userId = it.userId!!,
|
||||
dmConversationId = null,
|
||||
dmConversationId = appContext.database.getDMConversationId(it.userId!!),
|
||||
displayName = it.displayName,
|
||||
mutableUsername = it.mutableUsername!!,
|
||||
bitmojiId = it.bitmojiAvatarId,
|
||||
|
||||
@@ -27,7 +27,9 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp
|
||||
).contains(ruleType) && getRuleState() != null
|
||||
|
||||
fun canUseRule(conversationId: String): Boolean {
|
||||
if (getRuleState() == null) return false
|
||||
if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) {
|
||||
return false
|
||||
}
|
||||
val state = getState(conversationId)
|
||||
if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) {
|
||||
return !state
|
||||
|
||||
@@ -4,13 +4,31 @@ import me.eternal.purrfectsnap.common.data.MessagingRuleType
|
||||
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
|
||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
|
||||
|
||||
class HideTypingIndicator : MessagingRuleFeature("Hide Typing Indicator", MessagingRuleType.HIDE_TYPING_INDICATOR) {
|
||||
private val messaging: Messaging by lazy { context.feature(Messaging::class) }
|
||||
|
||||
private fun shouldHideTypingIndicator(conversationId: String?): Boolean {
|
||||
return conversationId?.let { canUseRule(it) } ?: false
|
||||
}
|
||||
|
||||
private fun currentConversationId(): String? {
|
||||
return messaging.openedConversationUUID?.toString()
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
context.classCache.presenceSession.hook("processTypingActivity", HookStage.BEFORE, {
|
||||
messaging.openedConversationUUID?.toString()?.let { canUseRule(it) } ?: false
|
||||
shouldHideTypingIndicator(currentConversationId())
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, { param ->
|
||||
val conversationId = currentConversationId() ?: param.argNullable<Any>(0)?.let {
|
||||
runCatching { SnapUUID(it).toString() }.getOrNull()
|
||||
}
|
||||
shouldHideTypingIndicator(conversationId)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.*
|
||||
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
|
||||
import me.eternal.purrfectsnap.mapper.impl.FriendsFeedEventDispatcherMapper
|
||||
import me.eternal.purrfectsnap.mapper.impl.PlatformPresenceActionWrapperMapper
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Future
|
||||
|
||||
@@ -52,8 +53,37 @@ class Messaging : Feature("Messaging") {
|
||||
lastFocusedConversationType = -1
|
||||
}
|
||||
|
||||
private fun currentConversationId(): String? = openedConversationUUID?.toString()
|
||||
|
||||
private fun shouldHideBitmojiPresence(stealthMode: StealthMode): Boolean {
|
||||
return context.config.messaging.hideBitmojiPresence.get() ||
|
||||
currentConversationId()?.let { stealthMode.canUseRule(it) } == true
|
||||
}
|
||||
|
||||
private fun shouldHideTyping(stealthMode: StealthMode, hideTypingIndicator: HideTypingIndicator): Boolean {
|
||||
return context.config.messaging.hideTypingNotifications.get() ||
|
||||
currentConversationId()?.let { stealthMode.canUseRule(it) || hideTypingIndicator.canUseRule(it) } == true
|
||||
}
|
||||
|
||||
private fun shouldHidePeek(stealthMode: StealthMode): Boolean {
|
||||
return context.config.messaging.hidePeekAPeek.get() ||
|
||||
currentConversationId()?.let { stealthMode.canUseRule(it) } == true
|
||||
}
|
||||
|
||||
private fun clearField(instance: Any, typeNamePart: String, shouldClear: Boolean) {
|
||||
if (!shouldClear) return
|
||||
instance.javaClass.declaredFields.forEach { field ->
|
||||
if (field.type.name.contains(typeNamePart)) {
|
||||
field.isAccessible = true
|
||||
field.set(instance, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
val stealthMode = context.feature(StealthMode::class)
|
||||
val hideTypingIndicator = context.feature(HideTypingIndicator::class)
|
||||
|
||||
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
|
||||
synchronized(conversationManagerReadyListeners) {
|
||||
conversationManager = ConversationManager(context, param.thisObject())
|
||||
@@ -98,20 +128,100 @@ class Messaging : Feature("Messaging") {
|
||||
}
|
||||
|
||||
defer {
|
||||
arrayOf("activate", "deactivate").forEach { hook ->
|
||||
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
|
||||
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
|
||||
val conversationId = openedConversationUUID?.toString() ?: return@hook false
|
||||
context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(conversationId)
|
||||
shouldHideBitmojiPresence(stealthMode)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
|
||||
val conversationId = openedConversationUUID?.toString() ?: return@hook false
|
||||
context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(conversationId)
|
||||
shouldHidePeek(stealthMode)
|
||||
}) { it.setResult(null) }
|
||||
|
||||
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, {
|
||||
shouldHideTyping(stealthMode, hideTypingIndicator)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
|
||||
context.mappings.useMapper(PlatformPresenceActionWrapperMapper::class) {
|
||||
classLoader = context.androidContext.classLoader
|
||||
if (classReference.getAsClass() == null) {
|
||||
runCatching { context.mappings.refresh() }.onFailure {
|
||||
context.log.error("Failed to refresh mappings for PlatformPresenceActionWrapper", it)
|
||||
}
|
||||
}
|
||||
|
||||
classReference.getAsClass()?.let { wrapperClass ->
|
||||
val bitmojiMethodNames = mutableSetOf<String>()
|
||||
val typingMethodNames = mutableSetOf<String>()
|
||||
val peekingMethodNames = mutableSetOf<String>()
|
||||
|
||||
wrapperClass.methods.forEach { method ->
|
||||
val parameterTypes = method.parameterTypes
|
||||
|
||||
if (parameterTypes.any { parameterType ->
|
||||
listOf(
|
||||
"PlatformChatVisibleAction",
|
||||
"PlatformChatHiddenAction",
|
||||
"PlatformViewingChatMediaAction",
|
||||
"PlatformUsingReplyCameraAction"
|
||||
).any { parameterType.name.contains(it) }
|
||||
}) {
|
||||
bitmojiMethodNames.add(method.name)
|
||||
}
|
||||
|
||||
if (parameterTypes.any { parameterType ->
|
||||
parameterType.name.contains("PlatformTypingAction")
|
||||
}) {
|
||||
typingMethodNames.add(method.name)
|
||||
}
|
||||
|
||||
if (parameterTypes.any { parameterType ->
|
||||
parameterType.name.contains("PlatformStartPeekingAction")
|
||||
}) {
|
||||
peekingMethodNames.add(method.name)
|
||||
}
|
||||
}
|
||||
|
||||
bitmojiMethodNames.forEach { methodName ->
|
||||
wrapperClass.hook(methodName, HookStage.BEFORE, {
|
||||
shouldHideBitmojiPresence(stealthMode)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
typingMethodNames.forEach { methodName ->
|
||||
wrapperClass.hook(methodName, HookStage.BEFORE, {
|
||||
shouldHideTyping(stealthMode, hideTypingIndicator)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
peekingMethodNames.forEach { methodName ->
|
||||
wrapperClass.hook(methodName, HookStage.BEFORE, {
|
||||
shouldHidePeek(stealthMode)
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
wrapperClass.hookConstructor(HookStage.AFTER) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
clearField(instance, "PlatformChatVisibleAction", shouldHideBitmojiPresence(stealthMode))
|
||||
clearField(instance, "PlatformChatHiddenAction", shouldHideBitmojiPresence(stealthMode))
|
||||
clearField(instance, "PlatformViewingChatMediaAction", shouldHideBitmojiPresence(stealthMode))
|
||||
clearField(instance, "PlatformUsingReplyCameraAction", shouldHideBitmojiPresence(stealthMode))
|
||||
clearField(instance, "PlatformTypingAction", shouldHideTyping(stealthMode, hideTypingIndicator))
|
||||
clearField(instance, "PlatformStartPeekingAction", shouldHidePeek(stealthMode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//get last opened snap for media downloader
|
||||
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
|
||||
openedConversationUUID = event.conversationId
|
||||
|
||||
@@ -22,7 +22,6 @@ class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALT
|
||||
|
||||
|
||||
override fun init() {
|
||||
if (getRuleState() == null) return
|
||||
val isConversationInStealthMode: (SnapUUID) -> Boolean = { canUseRule(it.toString()) }
|
||||
|
||||
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->
|
||||
|
||||
@@ -27,16 +27,18 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
||||
override fun init() {
|
||||
val config = context.config.camera
|
||||
|
||||
config.startupDefaultCamera.getNullable()?.let { defaultCamera ->
|
||||
context.database.setCameraType(if (defaultCamera == "back") "BACK_FACING" else "FRONT_FACING")
|
||||
}
|
||||
|
||||
val frontCameraId by lazy {
|
||||
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
||||
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
|
||||
} }.getOrNull()
|
||||
}
|
||||
|
||||
val backCameraId by lazy {
|
||||
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
||||
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK }
|
||||
} }.getOrNull()
|
||||
}
|
||||
|
||||
if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) {
|
||||
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
|
||||
val permission = param.arg<String>(0)
|
||||
@@ -47,11 +49,24 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
||||
}
|
||||
|
||||
var isLastCameraFront = false
|
||||
var startupCameraApplied = false
|
||||
|
||||
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
|
||||
val cameraManager = param.thisObject() as? CameraManager ?: return@hook
|
||||
val cameraId = param.arg<String>(0)
|
||||
var cameraId = param.arg<String>(0)
|
||||
val disabledCameras = config.disableCameras.get()
|
||||
val startupDefaultCamera = config.startupDefaultCamera.getNullable()
|
||||
|
||||
if (startupDefaultCamera != null && !startupCameraApplied) {
|
||||
val preferredCameraId = if (startupDefaultCamera == "back") backCameraId else frontCameraId
|
||||
if (preferredCameraId != null) {
|
||||
if (preferredCameraId != cameraId) {
|
||||
param.setArg(0, preferredCameraId)
|
||||
cameraId = preferredCameraId
|
||||
}
|
||||
startupCameraApplied = true
|
||||
}
|
||||
}
|
||||
|
||||
if (disabledCameras.size >= 2) {
|
||||
param.setResult(null)
|
||||
|
||||
@@ -31,41 +31,97 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookCallbackMethod(
|
||||
hookedCallbacks: MutableSet<String>,
|
||||
callbackClassName: String,
|
||||
methodName: String,
|
||||
block: (param: me.eternal.purrfectsnap.core.util.hook.HookAdapter) -> Unit
|
||||
) {
|
||||
val hookKey = "$callbackClassName#$methodName"
|
||||
if (!hookedCallbacks.add(hookKey)) return
|
||||
runCatching {
|
||||
findClass(callbackClassName).hook(methodName, HookStage.BEFORE) { param ->
|
||||
block(param)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.warn("Failed to hook $methodName on $callbackClassName")
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (!context.config.userInterface.hideFriendFeedEntry.get()) return
|
||||
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
arrayOf(
|
||||
"FetchAndSyncFeedWithConversationIdsCallback" to "onFetchAndSyncFeedComplete",
|
||||
"FetchFeedCallback" to "onFetchFeedComplete",
|
||||
"FetchFeedEntriesCallback" to "onFetchFeedEntriesComplete",
|
||||
"QueryFeedCallback" to "onQueryFeedComplete",
|
||||
"FeedManagerDelegate" to "onFeedEntriesUpdated",
|
||||
"FeedManagerDelegate" to "onInternalSyncFeed",
|
||||
).forEach { (callbackName, methodName) ->
|
||||
findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
classLoader = context.androidContext.classLoader
|
||||
val hasFetchAndSyncCallback = callbacks.getAsMap()?.entries?.any {
|
||||
it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback")
|
||||
} == true
|
||||
if (callbacks.getClass("SyncFeedCallback") == null || !hasFetchAndSyncCallback) {
|
||||
runCatching { context.mappings.refresh() }.onFailure {
|
||||
context.log.error("Failed to refresh mappings for HideFriendFeedEntry callbacks", it)
|
||||
}
|
||||
classLoader = context.androidContext.classLoader
|
||||
}
|
||||
val callbackMap = callbacks.getAsMap().orEmpty()
|
||||
val hookedCallbacks = mutableSetOf<String>()
|
||||
|
||||
callbackMap.entries.forEach { (callbackName, callbackClassName) ->
|
||||
when {
|
||||
callbackName.startsWith("FetchAndSyncFeed") && callbackName.endsWith("Callback") -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchAndSyncFeedComplete") { param ->
|
||||
val deletedConversations: ArrayList<Any> = param.arg(2)
|
||||
filterFriendFeed(param.arg(0), deletedConversations)
|
||||
|
||||
if (deletedConversations.any {
|
||||
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
|
||||
context.database.getFeedEntryByConversationId(uuid) != null
|
||||
}) {
|
||||
param.setArg(4, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callbackName.contains("SyncFeed") && callbackName.endsWith("Callback") -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onSyncFeedComplete") { param ->
|
||||
filterFriendFeed(param.arg(0), param.argNullable(2))
|
||||
}
|
||||
}
|
||||
|
||||
callbackName == "FetchFeedCallback" || callbackName.contains("FetchFeedCallback") -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedComplete") { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
}
|
||||
}
|
||||
|
||||
callbackName == "FetchFeedEntriesCallback" || callbackName.contains("FetchFeedEntriesCallback") -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedEntriesComplete") { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
}
|
||||
}
|
||||
|
||||
callbackName == "QueryFeedCallback" || callbackName.contains("QueryFeedCallback") -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onQueryFeedComplete") { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
}
|
||||
}
|
||||
|
||||
callbackName == "FeedManagerDelegate" -> {
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFeedEntriesUpdated") { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
}
|
||||
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onInternalSyncFeed") { param ->
|
||||
filterFriendFeed(param.arg(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.getAsMap()?.entries?.firstOrNull { it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback") }
|
||||
?.value
|
||||
?.let { findClass(it) }
|
||||
?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param ->
|
||||
val deletedConversations: ArrayList<Any> = param.arg(2)
|
||||
filterFriendFeed(param.arg(0), deletedConversations)
|
||||
|
||||
if (deletedConversations.any {
|
||||
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
|
||||
context.database.getFeedEntryByConversationId(uuid) != null
|
||||
}) {
|
||||
param.setArg(4, true)
|
||||
}
|
||||
} ?: context.log.warn("Failed to hook FetchAndSyncFeedCallback")
|
||||
callbacks.getClass("SyncFeedCallback")
|
||||
?.hook("onSyncFeedComplete", HookStage.BEFORE) { param ->
|
||||
filterFriendFeed(param.arg(0), param.arg(2))
|
||||
} ?: context.log.warn("Failed to hook SyncFeedCallback")
|
||||
if (callbackMap.entries.none { it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback") }) {
|
||||
context.log.warn("Failed to hook FetchAndSyncFeedCallback")
|
||||
}
|
||||
if (callbackMap.entries.none { it.key.contains("SyncFeed") && it.key.endsWith("Callback") }) {
|
||||
context.log.warn("Failed to hook SyncFeedCallback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class ClassMapper(
|
||||
FoldingLayoutMapper(),
|
||||
PlatformClientAttestationMapper(),
|
||||
ChatMediaDrawerMapper(),
|
||||
PlatformPresenceActionWrapperMapper(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package me.eternal.purrfectsnap.mapper.impl
|
||||
|
||||
import me.eternal.purrfectsnap.mapper.AbstractClassMapper
|
||||
import me.eternal.purrfectsnap.mapper.ext.getClassName
|
||||
|
||||
class PlatformPresenceActionWrapperMapper : AbstractClassMapper("PlatformPresenceActionWrapper") {
|
||||
val classReference = classReference("class")
|
||||
|
||||
init {
|
||||
mapper {
|
||||
classes.firstOrNull { classDef ->
|
||||
classDef.fields.any { field ->
|
||||
field.type == "Lcom/snap/presence/PlatformChatVisibleAction;"
|
||||
}
|
||||
}?.let { classReference.set(it.getClassName()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user