This commit is contained in:
ΞTΞRNAL
2026-04-01 14:55:05 +05:30
parent 6491288513
commit c71e06095d
25 changed files with 3100 additions and 631 deletions

View File

@@ -7,6 +7,8 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.core.app.CoreComponentFactory import androidx.core.app.CoreComponentFactory
@@ -30,12 +32,14 @@ import androidx.work.WorkManager
import me.eternal.purrfectsnap.bridge.BridgeService import me.eternal.purrfectsnap.bridge.BridgeService
import me.eternal.purrfectsnap.common.BuildConfig import me.eternal.purrfectsnap.common.BuildConfig
import me.eternal.purrfectsnap.common.Constants import me.eternal.purrfectsnap.common.Constants
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.action.EnumAction import me.eternal.purrfectsnap.common.action.EnumAction
import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerWrapper import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerWrapper
import me.eternal.purrfectsnap.common.bridge.wrapper.MappingsWrapper import me.eternal.purrfectsnap.common.bridge.wrapper.MappingsWrapper
import me.eternal.purrfectsnap.common.config.ModConfig import me.eternal.purrfectsnap.common.config.ModConfig
import me.eternal.purrfectsnap.common.logger.fatalCrash import me.eternal.purrfectsnap.common.logger.fatalCrash
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper
import me.eternal.purrfectsnap.common.util.constantLazyBridge import me.eternal.purrfectsnap.common.util.constantLazyBridge
import me.eternal.purrfectsnap.common.util.getPurgeTime import me.eternal.purrfectsnap.common.util.getPurgeTime
import me.eternal.purrfectsnap.e2ee.E2EEImplementation import me.eternal.purrfectsnap.e2ee.E2EEImplementation
@@ -275,6 +279,67 @@ class RemoteSideContext(
androidContext.startActivity(intent) androidContext.startActivity(intent)
} }
fun requestSocialSnapshotRefresh(
openSnapchatFirst: Boolean = true,
snapchatWarmupDelayMs: Long = 1200L,
returnDelayMs: Long = 1200L
) {
fun sendSocialSnapshotBroadcast() {
runCatching {
androidContext.sendBroadcast(
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}
)
}.onFailure {
log.error("Failed to request latest social snapshot", it)
}
}
if (!openSnapchatFirst) {
sendSocialSnapshotBroadcast()
return
}
val snapchatIntent = androidContext.packageManager
.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME)
?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (snapchatIntent == null) {
shortToast(translation["toast_snapchat_not_installed"])
sendSocialSnapshotBroadcast()
return
}
val returnIntent = Intent(androidContext, MainActivity::class.java).apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
}
val mainHandler = Handler(Looper.getMainLooper())
runCatching {
androidContext.startActivity(snapchatIntent)
mainHandler.postDelayed(
{
runCatching {
androidContext.startActivity(returnIntent)
}.onFailure {
log.error("Failed to return to PurrfectSnap after Snapchat handoff", it)
}
mainHandler.postDelayed(
{ sendSocialSnapshotBroadcast() },
returnDelayMs
)
},
snapchatWarmupDelayMs
)
}.onFailure {
log.error("Failed to launch Snapchat for social snapshot refresh", it)
sendSocialSnapshotBroadcast()
}
}
private fun scheduleAnnouncementCheck() { private fun scheduleAnnouncementCheck() {
val workManager = WorkManager.getInstance(androidContext) val workManager = WorkManager.getInstance(androidContext)
val constraints = Constraints.Builder() val constraints = Constraints.Builder()

View File

@@ -93,24 +93,6 @@ fun AppDatabase.replaceMessagingData(
executeAsync { executeAsync {
database.beginTransaction() database.beginTransaction()
try { 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 -> friends.forEach { friend ->
database.execSQL( database.execSQL(
"INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)", "INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)",

View File

@@ -9,6 +9,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@@ -43,8 +46,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -72,6 +77,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import me.eternal.purrfectsnap.common.ui.TopBarActionButton import me.eternal.purrfectsnap.common.ui.TopBarActionButton
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.ManagerTheme import me.eternal.purrfectsnap.ui.manager.ManagerTheme
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
@@ -80,6 +86,7 @@ import me.eternal.purrfectsnap.ui.util.Dialog
import me.eternal.purrfectsnap.ui.util.DialogProperties import me.eternal.purrfectsnap.ui.util.DialogProperties
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.UUID
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -162,6 +169,28 @@ class FeaturesRootSection : Routes.Route() {
} }
} }
internal fun isRandomizedProfileEnabled(): Boolean {
return context.config.root.experimental.spoof.randomizeDeviceProfile.globalState == true
}
internal fun requestFreshRandomizedProfile() {
val randomizeConfig = context.config.root.experimental.spoof.randomizeDeviceProfile
randomizeConfig.profileGenerationToken.set(UUID.randomUUID().toString())
randomizeConfig.currentProfileSnapshot.set("")
}
internal fun getRandomizedProfileSnapshot(): String {
context.config.load()
return context.config.root.experimental.spoof.randomizeDeviceProfile.currentProfileSnapshot.getNullable()
?.takeIf { it.isNotBlank() }
?: (context.translation["manager.dialogs.randomize_device_profile.empty"]
?: "No generated profile is available yet. Enable the feature in Snapchat first.")
}
internal fun isRandomizedProfileActionProperty(propertyName: String): Boolean {
return propertyName == "generate_fresh_profile_action" || propertyName == "view_current_profile_action"
}
fun navigateToMainRoot() { fun navigateToMainRoot() {
routes.navController.navigate(routeInfo.id, NavOptions.Builder() routes.navController.navigate(routeInfo.id, NavOptions.Builder()
.setPopUpTo(routes.navController.graph.findStartDestination().id, false) .setPopUpTo(routes.navController.graph.findStartDestination().id, false)
@@ -330,9 +359,17 @@ class FeaturesRootSection : Routes.Route() {
} }
@Composable @Composable
internal fun PropertyAction(property: PropertyPair<*>, registerClickCallback: ( () -> Unit ) -> (() -> Unit)) { internal fun PropertyAction(
property: PropertyPair<*>,
onConfigChanged: () -> Unit,
registerClickCallback: (() -> Unit) -> (() -> Unit)
) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var dialogComposable by remember { mutableStateOf<@Composable () -> Unit>({}) } var dialogComposable by remember { mutableStateOf<@Composable () -> Unit>({}) }
var showRandomProfileProgressDialog by remember { mutableStateOf(false) }
var randomProfileStatus by remember { mutableStateOf("") }
var showCurrentRandomProfileDialog by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
fun registerDialogOnClickCallback() = registerClickCallback { showDialog = true } fun registerDialogOnClickCallback() = registerClickCallback { showDialog = true }
@@ -348,7 +385,64 @@ class FeaturesRootSection : Routes.Route() {
} }
val propertyValue = property.value val propertyValue = property.value
fun persistConfig() = context.config.writeConfig() val randomProfileEnabled = isRandomizedProfileEnabled()
val isRandomizedProfileContainer = property.name == "randomize_device_profile"
fun persistConfig() {
context.config.writeConfig()
onConfigChanged()
}
if (showRandomProfileProgressDialog) {
AestheticDialog(
onDismissRequest = {},
title = context.translation["manager.dialogs.randomize_device_profile.title"]
?: "Generating random device profile",
text = randomProfileStatus,
icon = Icons.Filled.AutoAwesome,
confirmButtonText = "",
onConfirm = {},
loading = true,
showIcon = false,
showCloseButton = false,
confirmEnabled = false
)
}
if (showCurrentRandomProfileDialog) {
val profileSnapshot = getRandomizedProfileSnapshot()
val clipboardManager = LocalClipboardManager.current
AestheticDialog(
onDismissRequest = { showCurrentRandomProfileDialog = false },
title = context.translation["manager.dialogs.randomize_device_profile.view_title"]
?: "Current randomized profile",
text = "",
icon = Icons.Filled.Visibility,
dismissButtonText = context.translation["button.copy"] ?: "Copy",
onDismiss = {
clipboardManager.setText(AnnotatedString(profileSnapshot))
context.shortToast(
context.translation["manager.dialogs.randomize_device_profile.copied"]
?: "Randomized profile copied"
)
},
confirmButtonText = context.translation["button.positive"],
onConfirm = { showCurrentRandomProfileDialog = false },
showCloseButton = false,
customContent = {
SelectionContainer {
Text(
text = profileSnapshot,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 360.dp)
.verticalScroll(rememberScrollState()),
color = PurrfectPalette.textSecondary,
textAlign = TextAlign.Start
)
}
}
)
}
if (property.key.params.flags.contains(ConfigFlag.USER_IMPORT)) { if (property.key.params.flags.contains(ConfigFlag.USER_IMPORT)) {
registerDialogOnClickCallback() registerDialogOnClickCallback()
@@ -518,12 +612,12 @@ class FeaturesRootSection : Routes.Route() {
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
Switch( Switch(
checked = state, checked = state,
onCheckedChange = { onCheckedChange = { requestedState ->
if (context.config.root.global.uiSettings.hapticFeedback.get()) { if (context.config.root.global.uiSettings.hapticFeedback.get()) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
} }
state = state.not() state = requestedState
propertyValue.setAny(state) propertyValue.setAny(requestedState)
persistConfig() persistConfig()
}, },
colors = purrfectSwitchColors() colors = purrfectSwitchColors()
@@ -566,6 +660,45 @@ class FeaturesRootSection : Routes.Route() {
} }
DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
if (dataType == DataProcessors.Type.STRING && isRandomizedProfileActionProperty(property.name)) {
val actionLabel = when (property.name) {
"generate_fresh_profile_action" -> context.translation[property.key.propertyName()] ?: "Generate Fresh Profile"
"view_current_profile_action" -> context.translation[property.key.propertyName()] ?: "View Current Profile"
else -> property.name
}
Button(
onClick = {
if (property.name == "generate_fresh_profile_action") {
showRandomProfileProgressDialog = true
coroutineScope.launch {
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.allocating"]
?: "Allocating a randomized device fingerprint"
delay(260)
requestFreshRandomizedProfile()
persistConfig()
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.finalizing"]
?: "Finalizing the all-in-one profile and disabling manual overrides"
delay(260)
showRandomProfileProgressDialog = false
context.shortToast(
context.translation["manager.dialogs.randomize_device_profile.refresh_requested"]
?: "Fresh randomized profile requested. Restart Snapchat to apply it."
)
}
} else {
showCurrentRandomProfileDialog = true
}
},
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
contentColor = Color.White
)
) {
Text(actionLabel, maxLines = 1)
}
return
}
dialogComposable = { dialogComposable = {
when (dataType) { when (dataType) {
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
@@ -660,12 +793,35 @@ class FeaturesRootSection : Routes.Route() {
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
Switch( Switch(
checked = state, checked = state,
onCheckedChange = { onCheckedChange = { requestedState ->
if (context.config.root.global.uiSettings.hapticFeedback.get()) { if (context.config.root.global.uiSettings.hapticFeedback.get()) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
} }
state = state.not() if (isRandomizedProfileContainer && requestedState) {
container.globalState = state showRandomProfileProgressDialog = true
coroutineScope.launch {
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.allocating"]
?: "Allocating a randomized device fingerprint"
delay(260)
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.network"]
?: "Preparing network, locale, and telephony values"
delay(260)
container.globalState = true
state = true
persistConfig()
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.done"]
?: "Randomized device profile generated"
delay(220)
showRandomProfileProgressDialog = false
context.log.info("Enabled randomized device profile mode from manager UI")
}
return@Switch
}
state = requestedState
container.globalState = requestedState
if (!requestedState && isRandomizedProfileContainer) {
context.log.info("Disabled randomized device profile mode from manager UI")
}
persistConfig() persistConfig()
}, },
colors = purrfectSwitchColors() colors = purrfectSwitchColors()
@@ -722,7 +878,12 @@ class FeaturesRootSection : Routes.Route() {
} }
@Composable @Composable
internal fun PropertyCard(property: PropertyPair<*>, onOpen: (() -> Unit)? = null) { internal fun PropertyCard(
property: PropertyPair<*>,
configRefreshNonce: Int,
onConfigChanged: () -> Unit,
onOpen: (() -> Unit)? = null
) {
val isAphelion = remember { context.config.root.global.uiSettings.managerTheme.get() == "APHELION" } val isAphelion = remember { context.config.root.global.uiSettings.managerTheme.get() == "APHELION" }
var clickCallback by remember { mutableStateOf<(() -> Unit)?>(null) } var clickCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
val noticeColorMap = remember { val noticeColorMap = remember {
@@ -736,6 +897,7 @@ class FeaturesRootSection : Routes.Route() {
val versionCheck = remember { property.key.params.versionCheck } val versionCheck = remember { property.key.params.versionCheck }
val versionCheckPair = remember(property) { versionCheck?.checkVersion(context.installationSummary.snapchatInfo?.versionCode ?: return@remember null)} val versionCheckPair = remember(property) { versionCheck?.checkVersion(context.installationSummary.snapchatInfo?.versionCode ?: return@remember null)}
val isComponentDisabled = remember { versionCheckPair != null && versionCheck?.isDisabled == true } val isComponentDisabled = remember { versionCheckPair != null && versionCheck?.isDisabled == true }
val isInteractionEnabled = !isComponentDisabled
val cardShape = RoundedCornerShape(22.dp) val cardShape = RoundedCornerShape(22.dp)
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@@ -753,8 +915,9 @@ class FeaturesRootSection : Routes.Route() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 7.dp) .padding(horizontal = 12.dp, vertical = 7.dp)
.graphicsLayer { if (isComponentDisabled) alpha = 0.5f } .graphicsLayer { if (!isInteractionEnabled) alpha = 0.5f }
.clickable( .clickable(
enabled = isInteractionEnabled,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null indication = null
) { ) {
@@ -852,7 +1015,7 @@ class FeaturesRootSection : Routes.Route() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
PropertyAction(property, registerClickCallback = { callback -> PropertyAction(property, onConfigChanged = onConfigChanged, registerClickCallback = { callback ->
if (property.key.propertyTranslationPath().startsWith("rules.properties")) { if (property.key.propertyTranslationPath().startsWith("rules.properties")) {
clickCallback = { clickCallback = {
routes.manageRuleFeature.navigate { routes.manageRuleFeature.navigate {
@@ -1406,6 +1569,7 @@ class FeaturesRootSection : Routes.Route() {
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
var controlsHeight by remember { mutableStateOf(100.dp) } var controlsHeight by remember { mutableStateOf(100.dp) }
var configRefreshNonce by rememberSaveable { mutableStateOf(0) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -1479,7 +1643,12 @@ class FeaturesRootSection : Routes.Route() {
upsertHistory(liveSearchQuery, sharedSearchHistory) upsertHistory(liveSearchQuery, sharedSearchHistory)
} }
} else null } else null
PropertyCard(item, onOpen = onOpen) PropertyCard(
property = item,
configRefreshNonce = configRefreshNonce,
onConfigChanged = { configRefreshNonce++ },
onOpen = onOpen
)
} }
} }
item { Spacer(modifier = Modifier.height(12.dp)) } item { Spacer(modifier = Modifier.height(12.dp)) }
@@ -1634,9 +1803,14 @@ class FeaturesRootSection : Routes.Route() {
onBack: (() -> Unit)? = null, onBack: (() -> Unit)? = null,
) { ) {
PropertiesView( PropertiesView(
properties = remember { properties = remember(configContainer.globalState) {
configContainer.properties.map { (it.key to it.value).toPropertyPair() as PropertyPair<Any> }.filter { configContainer.properties.map { (it.key to it.value).toPropertyPair() as PropertyPair<Any> }.filter {
!it.key.params.flags.contains(ConfigFlag.HIDDEN) !it.key.params.flags.contains(ConfigFlag.HIDDEN) &&
(
configContainer !== context.config.root.experimental.spoof.randomizeDeviceProfile ||
configContainer.globalState == true ||
!isRandomizedProfileActionProperty(it.key.name)
)
} }
}, },
stateKey = stateKey, stateKey = stateKey,

View File

@@ -24,11 +24,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.* import kotlinx.coroutines.*
import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper import me.eternal.purrfectsnap.storage.getFriends
import me.eternal.purrfectsnap.storage.getGroups
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage
@@ -219,39 +219,70 @@ class AddFriendDialog(
var hasFetchError by remember { mutableStateOf(false) } var hasFetchError by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val updateSnapshot: (List<MessagingFriendInfo>, List<MessagingGroupInfo>) -> Unit = { friends, groups -> fun applySnapshot(
coroutineScope.launch { friends: List<MessagingFriendInfo>,
cachedFriends = friends.run { groups: List<MessagingGroupInfo>
if (pinnedIds != null) { ) {
sortedBy { -pinnedIds.indexOf(it.userId) } cachedFriends = friends.run {
} else friends if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.userId) }
} else {
this
} }
cachedGroups = groups.run { }
if (pinnedIds != null) { cachedGroups = groups.run {
sortedBy { -pinnedIds.indexOf(it.conversationId) } if (pinnedIds != null) {
} else groups sortedBy { -pinnedIds.indexOf(it.conversationId) }
} else {
this
} }
}
if (friends.isNotEmpty() || groups.isNotEmpty()) {
timeoutJob?.cancel() timeoutJob?.cancel()
hasFetchError = false hasFetchError = false
} }
} }
val updateSnapshot: (List<MessagingFriendInfo>, List<MessagingGroupInfo>) -> Unit = { friends, groups ->
coroutineScope.launch {
applySnapshot(friends, groups)
}
}
withContext(Dispatchers.IO) {
applySnapshot(
context.database.getFriends(descOrder = true),
context.database.getGroups()
)
}
if (context.bridgeService != null) { if (context.bridgeService != null) {
context.bridgeService?.requestEphemeralSocialSnapshot(updateSnapshot) context.bridgeService?.requestEphemeralSocialSnapshot(updateSnapshot)
} else { } else {
context.database.receiveMessagingDataCallback = updateSnapshot context.database.receiveMessagingDataCallback = updateSnapshot
} }
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}.also { context.requestSocialSnapshotRefresh()
runCatching {
context.androidContext.sendBroadcast(it) coroutineScope.launch(Dispatchers.IO) {
}.onFailure { repeat(25) {
context.log.error("Failed to send broadcast", it) delay(1000)
hasFetchError = true val dbFriends = context.database.getFriends(descOrder = true)
val dbGroups = context.database.getGroups()
if (dbFriends.isNotEmpty() || dbGroups.isNotEmpty()) {
withContext(Dispatchers.Main) {
applySnapshot(dbFriends, dbGroups)
}
return@launch
}
} }
} }
timeoutJob = coroutineScope.launch { timeoutJob = coroutineScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
delay(20000) delay(25000)
hasFetchError = true if ((cachedFriends?.isNullOrEmpty() != false) && (cachedGroups?.isNullOrEmpty() != false)) {
hasFetchError = true
}
} }
} }
} }

View File

@@ -38,13 +38,11 @@ import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.R
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.data.SocialScope import me.eternal.purrfectsnap.common.data.SocialScope
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie 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.storage.*
import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.ManagerTheme import me.eternal.purrfectsnap.ui.manager.ManagerTheme
@@ -63,13 +61,7 @@ class SocialRootSection : Routes.Route() {
} }
internal fun requestLatestSnapshot() { internal fun requestLatestSnapshot() {
runCatching { context.requestSocialSnapshotRefresh()
context.androidContext.sendBroadcast(
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}
)
}.onFailure {
context.log.error("Failed to request latest social snapshot", it)
}
} }
@Composable @Composable

View File

@@ -314,6 +314,7 @@ fun HomeSettings.AphelionSettingsScreen(nav: NavBackStackEntry) {
PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_cant_login_button", text = translation["disable_cant_login_button_label"] ?: "Disable Can't Login Button")
} }
} }
} }

View File

@@ -57,8 +57,17 @@ fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) {
var searchActive by rememberSaveable { mutableStateOf(false) } var searchActive by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.database.receiveMessagingDataCallback = { friends, groups ->
friendList = friends
groupList = groups
}
updateScopeLists() updateScopeLists()
} }
DisposableEffect(Unit) {
onDispose {
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
val normalizedQuery = remember(searchQuery) { searchQuery.trim() } val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
val filteredFriends = remember(friendList, normalizedQuery) { val filteredFriends = remember(friendList, normalizedQuery) {
if (normalizedQuery.isBlank()) { if (normalizedQuery.isBlank()) {

View File

@@ -921,6 +921,7 @@ object LegacyTheme : ThemeContract {
PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"]) PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"])
PreferenceToggle(context.sharedPreferences, key = "disable_cant_login_button", text = translation["disable_cant_login_button_label"] ?: "Disable Can't Login Button")
} }
} }
} }

View File

@@ -11,7 +11,10 @@ import android.provider.Settings
import android.view.* import android.view.*
import android.view.View.OnAttachStateChangeListener import android.view.View.OnAttachStateChangeListener
import androidx.activity.ComponentDialog import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -47,6 +50,8 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID import java.util.UUID
@@ -197,6 +202,21 @@ private fun InlineDialog(
val screenWidthDp = with(density) { displayMetrics.widthPixels.toDp() } val screenWidthDp = with(density) { displayMetrics.widthPixels.toDp() }
val screenHeightDp = with(density) { displayMetrics.heightPixels.toDp() } val screenHeightDp = with(density) { displayMetrics.heightPixels.toDp() }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val fallbackBackDispatcherOwner = remember(onDismissRequest) {
object : OnBackPressedDispatcherOwner {
private val lifecycleRegistry = LifecycleRegistry(this).apply {
currentState = Lifecycle.State.RESUMED
}
private val dispatcher = OnBackPressedDispatcher(onDismissRequest)
override val lifecycle: Lifecycle
get() = lifecycleRegistry
override val onBackPressedDispatcher: OnBackPressedDispatcher
get() = dispatcher
}
}
val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current ?: fallbackBackDispatcherOwner
var visible by remember { mutableStateOf(false) } var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -212,43 +232,45 @@ private fun InlineDialog(
), ),
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest
) { ) {
Box( CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides backDispatcherOwner) {
modifier = Modifier Box(
.width(screenWidthDp) modifier = Modifier
.height(screenHeightDp) .width(screenWidthDp)
.then( .height(screenHeightDp)
if (dismissOnClickOutside) { .then(
Modifier.clickable( if (dismissOnClickOutside) {
interactionSource = interactionSource, Modifier.clickable(
indication = null, interactionSource = interactionSource,
onClick = onDismissRequest indication = null,
) onClick = onDismissRequest
} else { )
Modifier } else {
} Modifier
) }
.semantics { dialog() }, )
contentAlignment = androidx.compose.ui.Alignment.Center .semantics { dialog() },
) { contentAlignment = androidx.compose.ui.Alignment.Center
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(180)) + scaleIn(
initialScale = 0.92f,
animationSpec = spring(dampingRatio = 0.82f, stiffness = 520f)
),
exit = fadeOut(animationSpec = tween(120)) + scaleOut(
targetScale = 0.96f,
animationSpec = tween(120)
)
) { ) {
Box( AnimatedVisibility(
modifier = Modifier.clickable( visible = visible,
interactionSource = interactionSource, enter = fadeIn(animationSpec = tween(180)) + scaleIn(
indication = null, initialScale = 0.92f,
onClick = {} animationSpec = spring(dampingRatio = 0.82f, stiffness = 520f)
),
exit = fadeOut(animationSpec = tween(120)) + scaleOut(
targetScale = 0.96f,
animationSpec = tween(120)
) )
) { ) {
content() Box(
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = {}
)
) {
content()
}
} }
} }
} }

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
} }
// You can still set these for legacy use by submodules or scripts: // You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.1").get()) rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.2").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("312").get().toInt()) rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("314").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // 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. // Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -532,6 +532,18 @@
"title": "Export Sensitive Data?", "title": "Export Sensitive Data?",
"content": "Do you want to export the config with sensitive data? (Such as location coordinates, etc.)" "content": "Do you want to export the config with sensitive data? (Such as location coordinates, etc.)"
}, },
"randomize_device_profile": {
"title": "Generating random device profile",
"done": "Randomized device profile generated",
"view_title": "Current randomized profile",
"empty": "No generated profile is available yet. Enable the feature in Snapchat first.",
"refresh_requested": "Fresh randomized profile requested. Restart Snapchat to apply it.",
"phase": {
"allocating": "Allocating a randomized device fingerprint",
"network": "Preparing network, locale, and telephony values",
"finalizing": "Finalizing the all-in-one profile and disabling manual overrides"
}
},
"messaging_action": { "messaging_action": {
"title": "Choose content types to process", "title": "Choose content types to process",
"select_all_button": "Select All" "select_all_button": "Select All"
@@ -898,7 +910,8 @@
"notices": { "notices": {
"unstable": "\u26a0 Unstable", "unstable": "\u26a0 Unstable",
"ban_risk": "\u26a0 This feature may cause bans", "ban_risk": "\u26a0 This feature may cause bans",
"internal_behavior": "\u26a0 This may break Snapchat internal behaviour" "internal_behavior": "\u26a0 This may break Snapchat internal behaviour",
"randomize_device_profile_override": "Controlled by Randomized Device Profile"
}, },
"properties": { "properties": {
"downloader": { "downloader": {
@@ -1174,6 +1187,14 @@
"name": "Hide Bitmoji Presence", "name": "Hide Bitmoji Presence",
"description": "Prevents your Bitmoji from popping up while in Chat" "description": "Prevents your Bitmoji from popping up while in Chat"
}, },
"spoof_viewing_gallery_presence": {
"name": "Spoof Viewing Gallery Presence",
"description": "Keeps your Bitmoji visible in Chat while viewing chat media"
},
"spoof_reply_camera_presence": {
"name": "Spoof Reply Camera Presence",
"description": "Keeps your Bitmoji visible in Chat while using the reply camera"
},
"hide_typing_notifications": { "hide_typing_notifications": {
"name": "Hide Typing Notifications", "name": "Hide Typing Notifications",
"description": "Prevents anyone from knowing you're typing a message" "description": "Prevents anyone from knowing you're typing a message"
@@ -2013,6 +2034,32 @@
"name": "Force Wi-Fi Transport Flag", "name": "Force Wi-Fi Transport Flag",
"description": "Force network transport to report Wi-Fi instead of mobile data" "description": "Force network transport to report Wi-Fi instead of mobile data"
}, },
"randomize_device_profile": {
"name": "Randomized Device Profile",
"description": "Generate and apply a full randomized device, network, locale, and settings profile in one restart-safe profile",
"properties": {
"show_activation_overlay": {
"name": "Show Activation Overlay",
"description": "Show the in-app toast when the randomized profile becomes active"
},
"randomize_ip_address": {
"name": "Randomize IP Address",
"description": "Generate and spoof a randomized IP address whenever a fresh randomized profile is created"
},
"persistent_app_language": {
"name": "Persistent App Language",
"description": "Force Snapchat to stay on a specific supported app language"
},
"generate_fresh_profile_action": {
"name": "Generate Fresh Profile",
"description": "Request a newly generated randomized profile"
},
"view_current_profile_action": {
"name": "View Current Profile",
"description": "Inspect the latest randomized profile snapshot"
}
}
},
"spoof_device_id": { "spoof_device_id": {
"name": "Spoof Device ID", "name": "Spoof Device ID",
"description": "Override the Android ID sent to Snapchat", "description": "Override the Android ID sent to Snapchat",
@@ -2348,7 +2395,10 @@
"custom_android_id": { "custom_android_id": {
"null": "Use real Android ID" "null": "Use real Android ID"
}, },
"add_friend_source_spoof": { "persistent_app_language": {
"system_default": "System Default"
},
"add_friend_source_spoof": {
"added_by_username": "By Username", "added_by_username": "By Username",
"added_by_mention": "By Mention", "added_by_mention": "By Mention",
"added_by_group_chat": "By Group Chat", "added_by_group_chat": "By Group Chat",
@@ -2965,6 +3015,10 @@
"stopped_speaking": "Stopped Speaking", "stopped_speaking": "Stopped Speaking",
"started_peeking": "Started Peeking", "started_peeking": "Started Peeking",
"stopped_peeking": "Stopped Peeking", "stopped_peeking": "Stopped Peeking",
"started_using_reply_camera": "Started Using Reply Camera",
"stopped_using_reply_camera": "Stopped Using Reply Camera",
"started_viewing_chat_media": "Started Viewing Chat Media",
"stopped_viewing_chat_media": "Stopped Viewing Chat Media",
"message_read": "Message Read", "message_read": "Message Read",
"message_deleted": "Message Deleted", "message_deleted": "Message Deleted",
"message_saved": "Message Saved", "message_saved": "Message Saved",
@@ -2977,7 +3031,9 @@
"snap_replayed_twice": "Snap Replayed Twice", "snap_replayed_twice": "Snap Replayed Twice",
"snap_screenshot": "Snap Screenshot", "snap_screenshot": "Snap Screenshot",
"snap_screen_record": "Snap Screen Record", "snap_screen_record": "Snap Screen Record",
"i_can_see_you": "I Can See You" "i_can_see_you": "I Can See You",
"i_can_see_you_2": "I Can See You 2",
"i_can_see_you_3": "I Can See You 3"
}, },
"cleared_from_feed": "Cleared from feed", "cleared_from_feed": "Cleared from feed",
"tracker_actions": { "tracker_actions": {
@@ -3186,6 +3242,10 @@
"stopped_speaking": "{friend} stopped speaking in {conversation}", "stopped_speaking": "{friend} stopped speaking in {conversation}",
"started_peeking": "{friend} started peeking in {conversation}", "started_peeking": "{friend} started peeking in {conversation}",
"stopped_peeking": "{friend} stopped peeking in {conversation}", "stopped_peeking": "{friend} stopped peeking in {conversation}",
"started_using_reply_camera": "{friend} opened the reply camera in {conversation}",
"stopped_using_reply_camera": "{friend} closed the reply camera in {conversation}",
"started_viewing_chat_media": "{friend} started viewing chat media in {conversation}",
"stopped_viewing_chat_media": "{friend} stopped viewing chat media in {conversation}",
"message_read": "{friend} read a message in {conversation}", "message_read": "{friend} read a message in {conversation}",
"message_deleted": "{friend} deleted a message in {conversation}", "message_deleted": "{friend} deleted a message in {conversation}",
"message_saved": "{friend} saved a message in {conversation}", "message_saved": "{friend} saved a message in {conversation}",
@@ -3198,7 +3258,9 @@
"snap_replayed_twice": "{friend} replayed a snap twice in {conversation}", "snap_replayed_twice": "{friend} replayed a snap twice in {conversation}",
"snap_screenshot": "{friend} took a screenshot in {conversation}", "snap_screenshot": "{friend} took a screenshot in {conversation}",
"snap_screen_record": "{friend} screen recorded in {conversation}", "snap_screen_record": "{friend} screen recorded in {conversation}",
"i_can_see_you": "{friend} activity in {conversation}: {details}" "i_can_see_you": "{friend} activity in {conversation}: {details}",
"i_can_see_you_2": "{friend} gallery activity in {conversation}: {details}",
"i_can_see_you_3": "{friend} reply camera activity in {conversation}: {details}"
}, },
"friend_mutation_observer": { "friend_mutation_observer": {
"notification_channel_name": "Friend Mutation Observer", "notification_channel_name": "Friend Mutation Observer",
@@ -3351,6 +3413,10 @@
"stopped_speaking": "Stopped speaking", "stopped_speaking": "Stopped speaking",
"started_peeking": "Started peeking", "started_peeking": "Started peeking",
"stopped_peeking": "Stopped peeking", "stopped_peeking": "Stopped peeking",
"started_using_reply_camera": "Opened reply camera",
"stopped_using_reply_camera": "Closed reply camera",
"started_viewing_chat_media": "Started viewing chat media",
"stopped_viewing_chat_media": "Stopped viewing chat media",
"message_read": "Read message", "message_read": "Read message",
"message_deleted": "Deleted message", "message_deleted": "Deleted message",
"message_saved": "Saved message", "message_saved": "Saved message",
@@ -3402,6 +3468,10 @@
"stopped_speaking": "stopped speaking", "stopped_speaking": "stopped speaking",
"started_peeking": "started peeking", "started_peeking": "started peeking",
"stopped_peeking": "stopped peeking", "stopped_peeking": "stopped peeking",
"started_using_reply_camera": "opened the reply camera",
"stopped_using_reply_camera": "closed the reply camera",
"started_viewing_chat_media": "started viewing chat media",
"stopped_viewing_chat_media": "stopped viewing chat media",
"message_read": "read a message", "message_read": "read a message",
"message_deleted": "deleted a message", "message_deleted": "deleted a message",
"message_saved": "saved a message", "message_saved": "saved a message",
@@ -3414,7 +3484,9 @@
"snap_replayed_twice": "replayed a snap twice", "snap_replayed_twice": "replayed a snap twice",
"snap_screenshot": "took a screenshot", "snap_screenshot": "took a screenshot",
"snap_screen_record": "screen recorded", "snap_screen_record": "screen recorded",
"i_can_see_you": "was active" "i_can_see_you": "was active",
"i_can_see_you_2": "was viewing gallery",
"i_can_see_you_3": "was using the reply camera"
} }
} }
}, },
@@ -3474,6 +3546,7 @@
"disable_feature_loading_label": "Disable Feature Loading", "disable_feature_loading_label": "Disable Feature Loading",
"disable_auto_mapper_label": "Disable Auto Mapper", "disable_auto_mapper_label": "Disable Auto Mapper",
"disable_bypass_indicator_label": "Disable Bypass Indicator", "disable_bypass_indicator_label": "Disable Bypass Indicator",
"disable_cant_login_button_label": "Disable Can't Login Button",
"friend_list": { "friend_list": {
"manage_title": "Manage Friend List", "manage_title": "Manage Friend List",
"export_description": "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.", "export_description": "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.",

View File

@@ -570,6 +570,19 @@
"title": "Export Sensitive Data?", "title": "Export Sensitive Data?",
"content": "Do you want to export the config with sensitive data? (Such as location coordinates, etc.)" "content": "Do you want to export the config with sensitive data? (Such as location coordinates, etc.)"
}, },
"randomize_device_profile": {
"title": "Generating random device profile",
"done": "Randomized device profile generated",
"view_title": "Current randomized profile",
"copied": "Randomized profile copied",
"empty": "No generated profile is available yet. Enable the feature in Snapchat first.",
"refresh_requested": "Fresh randomized profile requested. Restart Snapchat to apply it.",
"phase": {
"allocating": "Allocating a randomized device fingerprint",
"network": "Preparing network, locale, and telephony values",
"finalizing": "Finalizing the all-in-one profile and disabling manual overrides"
}
},
"messaging_action": { "messaging_action": {
"title": "Choose content types to process", "title": "Choose content types to process",
"select_all_button": "Select All" "select_all_button": "Select All"
@@ -1238,6 +1251,14 @@
"name": "Hide Bitmoji Presence", "name": "Hide Bitmoji Presence",
"description": "Prevents your Bitmoji from popping up while in Chat" "description": "Prevents your Bitmoji from popping up while in Chat"
}, },
"spoof_viewing_gallery_presence": {
"name": "Spoof Viewing Gallery Presence",
"description": "Keeps your Bitmoji visible in Chat while viewing chat media"
},
"spoof_reply_camera_presence": {
"name": "Spoof Reply Camera Presence",
"description": "Keeps your Bitmoji visible in Chat while using the reply camera"
},
"hide_typing_notifications": { "hide_typing_notifications": {
"name": "Hide Typing Notifications", "name": "Hide Typing Notifications",
"description": "Prevents anyone from knowing you're typing a message" "description": "Prevents anyone from knowing you're typing a message"
@@ -2119,6 +2140,418 @@
"name": "Force Wi-Fi Transport Flag", "name": "Force Wi-Fi Transport Flag",
"description": "Force network transport to report Wi-Fi instead of mobile data" "description": "Force network transport to report Wi-Fi instead of mobile data"
}, },
"randomize_device_profile": {
"name": "Randomized Device Profile",
"description": "Generate and apply a full randomized device, network, locale, and settings profile",
"properties": {
"show_activation_overlay": {
"name": "Show Activation Overlay",
"description": "Show the in-app toast when the randomized profile becomes active"
},
"randomize_ip_address": {
"name": "Randomize IP Address",
"description": "Generate and spoof a randomized IP address whenever a fresh randomized profile is created"
},
"spoof_build_properties": {
"name": "Spoof Build Properties",
"description": "Apply randomized build fields, fingerprints, and device property values"
},
"build_properties": {
"name": "Build Properties",
"description": "Enable build property spoofing and fine-tune its subsets",
"properties": {
"device_identity": {
"name": "Device Identity",
"description": "Enable device identity spoofing and fine-tune its values",
"properties": {
"manufacturer_model": {
"name": "Manufacturer And Model",
"description": "Randomize the reported manufacturer and model"
},
"brand_product": {
"name": "Brand And Product",
"description": "Randomize the reported brand, device, and product values"
},
"hardware_board": {
"name": "Hardware And Board",
"description": "Randomize the reported hardware and board values"
}
}
},
"build_version": {
"name": "Build Version",
"description": "Enable build version spoofing and fine-tune its values",
"properties": {
"fingerprint": {
"name": "Fingerprint",
"description": "Randomize the reported build fingerprint"
},
"display": {
"name": "Display ID",
"description": "Randomize the reported build display ID"
},
"host": {
"name": "Host",
"description": "Randomize the reported build host"
},
"bootloader": {
"name": "Bootloader",
"description": "Randomize the reported bootloader value"
},
"build_time": {
"name": "Build Time",
"description": "Randomize the reported build timestamp"
}
}
},
"abi_lists": {
"name": "ABI Lists",
"description": "Enable ABI spoofing and fine-tune its values",
"properties": {
"combined_abis": {
"name": "Combined ABI List",
"description": "Randomize the combined supported ABI list"
},
"split_abis": {
"name": "32-bit And 64-bit ABI Lists",
"description": "Randomize the split 32-bit and 64-bit ABI lists"
}
}
},
"system_properties": {
"name": "System Properties",
"description": "Expose randomized values through Android system property lookups",
"properties": {
"build": {
"name": "Build Properties",
"description": "Expose randomized build values through system properties"
},
"locale": {
"name": "Locale Properties",
"description": "Expose randomized locale values through system properties"
},
"telephony": {
"name": "Telephony Properties",
"description": "Expose randomized telephony values through system properties"
}
}
}
}
},
"spoof_locale": {
"name": "Spoof Locale",
"description": "Apply the randomized locale and language hooks"
},
"locale_options": {
"name": "Locale Details",
"description": "Enable locale spoofing and fine-tune its subsets",
"properties": {
"locale": {
"name": "Locale",
"description": "Enable locale spoofing and fine-tune language and region values",
"properties": {
"language": {
"name": "Language",
"description": "Randomize the reported language value"
},
"region": {
"name": "Region",
"description": "Randomize the reported region value"
}
}
},
"time": {
"name": "Time",
"description": "Enable time spoofing and fine-tune time-related values",
"properties": {
"time_zone_id": {
"name": "Time Zone ID",
"description": "Randomize the reported time zone ID"
},
"time_zone_display_name": {
"name": "Time Zone Display Name",
"description": "Randomize the reported time zone display name"
},
"auto_time": {
"name": "Auto Time",
"description": "Randomize the global auto-time setting"
},
"auto_time_zone": {
"name": "Auto Time Zone",
"description": "Randomize the global auto-time-zone setting"
}
}
}
}
},
"spoof_telephony": {
"name": "Spoof Telephony",
"description": "Apply randomized carrier, SIM, and phone capability values"
},
"telephony_options": {
"name": "Telephony Details",
"description": "Enable telephony spoofing and fine-tune its subsets",
"properties": {
"mms": {
"name": "MMS",
"description": "Enable MMS spoofing and fine-tune MMS values",
"properties": {
"user_agent": {
"name": "User Agent",
"description": "Randomize the MMS user agent string"
}
}
},
"network_identity": {
"name": "Network Identity",
"description": "Enable network identity spoofing and fine-tune network values",
"properties": {
"network_type": {
"name": "Network Type",
"description": "Randomize the reported network type"
},
"operator_numeric": {
"name": "Operator Numeric",
"description": "Randomize the reported operator numeric code"
},
"operator_name": {
"name": "Operator Name",
"description": "Randomize the reported operator name"
},
"country_iso": {
"name": "Country ISO",
"description": "Randomize the reported network country ISO"
}
}
},
"sim_identity": {
"name": "SIM Identity",
"description": "Enable SIM identity spoofing and fine-tune SIM values",
"properties": {
"country_iso": {
"name": "Country ISO",
"description": "Randomize the reported SIM country ISO"
},
"operator_numeric": {
"name": "Operator Numeric",
"description": "Randomize the reported SIM operator numeric code"
},
"operator_name": {
"name": "Operator Name",
"description": "Randomize the reported SIM operator name"
},
"sim_state": {
"name": "SIM State",
"description": "Randomize the reported SIM state"
},
"has_icc_card": {
"name": "ICC Card",
"description": "Randomize whether a SIM card is reported as present"
}
}
},
"phone_capabilities": {
"name": "Phone Capabilities",
"description": "Enable phone capability spoofing and fine-tune capability values",
"properties": {
"phone_count": {
"name": "Phone Count",
"description": "Randomize the reported phone count"
},
"hearing_aid": {
"name": "Hearing Aid",
"description": "Randomize hearing aid compatibility support"
},
"tty": {
"name": "TTY",
"description": "Randomize TTY support"
},
"world_phone": {
"name": "World Phone",
"description": "Randomize world phone support"
},
"roaming": {
"name": "Roaming",
"description": "Randomize roaming status"
},
"sms_voice": {
"name": "SMS And Voice",
"description": "Randomize SMS and voice capability support"
},
"phone_type": {
"name": "Phone Type",
"description": "Randomize the reported phone type"
}
}
}
}
},
"spoof_settings": {
"name": "Spoof Settings",
"description": "Apply the randomized Android settings overrides"
},
"settings_options": {
"name": "Settings Details",
"description": "Enable settings spoofing and fine-tune its namespaces",
"properties": {
"secure": {
"name": "Secure Settings",
"description": "Enable secure settings spoofing and fine-tune secure values",
"properties": {
"base": {
"name": "Base",
"description": "Randomize the base secure settings values"
},
"tts": {
"name": "Text To Speech",
"description": "Randomize text-to-speech secure settings values"
}
}
},
"system": {
"name": "System Settings",
"description": "Enable system settings spoofing and fine-tune system values",
"properties": {
"base": {
"name": "Base",
"description": "Randomize the base system settings values"
},
"bluetooth": {
"name": "Bluetooth",
"description": "Randomize Bluetooth-related system settings values"
}
}
},
"global": {
"name": "Global Settings",
"description": "Enable global settings spoofing and fine-tune global values",
"properties": {
"base": {
"name": "Base",
"description": "Randomize the base global settings values"
}
}
}
}
},
"spoof_network": {
"name": "Spoof Network",
"description": "Apply randomized Wi-Fi and DNS network values"
},
"network_options": {
"name": "Network Details",
"description": "Enable network spoofing and fine-tune its subsets",
"properties": {
"wifi": {
"name": "Wi-Fi Info",
"description": "Enable Wi-Fi spoofing and fine-tune Wi-Fi values",
"properties": {
"ssid": {
"name": "SSID",
"description": "Randomize the reported Wi-Fi SSID"
},
"rssi": {
"name": "Signal Strength",
"description": "Randomize the reported Wi-Fi RSSI value"
}
}
},
"dns": {
"name": "DNS",
"description": "Enable DNS spoofing and fine-tune DNS values",
"properties": {
"servers": {
"name": "Servers",
"description": "Randomize the reported DNS server list"
},
"search_domains": {
"name": "Search Domains",
"description": "Randomize the reported DNS search domains"
},
"private_dns": {
"name": "Private DNS",
"description": "Randomize the reported private DNS values"
}
}
},
"captive_portal": {
"name": "Captive Portal",
"description": "Enable captive portal spoofing and fine-tune portal values",
"properties": {
"capability": {
"name": "Capability",
"description": "Randomize the reported captive portal capability"
}
}
}
}
},
"spoof_identifiers": {
"name": "Spoof Identifiers",
"description": "Apply randomized Android ID, advertising ID, and hardware address overrides"
},
"identifier_options": {
"name": "Identifier Details",
"description": "Enable identifier spoofing and fine-tune its subsets",
"properties": {
"android_id": {
"name": "Android ID",
"description": "Enable Android ID spoofing and fine-tune Android ID values",
"properties": {
"string_value": {
"name": "String Value",
"description": "Randomize the string Android ID value"
},
"long_value": {
"name": "Long Value",
"description": "Randomize the long Android ID value"
}
}
},
"advertising_id": {
"name": "Advertising ID",
"description": "Enable advertising ID spoofing and fine-tune advertising ID values",
"properties": {
"settings_value": {
"name": "Settings Value",
"description": "Randomize the advertising ID returned through settings"
},
"play_services": {
"name": "Play Services",
"description": "Randomize the advertising ID returned through Play Services"
}
}
},
"hardware_addresses": {
"name": "Hardware Addresses",
"description": "Enable hardware address spoofing and fine-tune address values",
"properties": {
"wifi_mac": {
"name": "Wi-Fi MAC",
"description": "Randomize the reported Wi-Fi MAC address"
},
"bluetooth_mac": {
"name": "Bluetooth MAC",
"description": "Randomize the reported Bluetooth MAC address"
}
}
}
}
},
"persistent_app_language": {
"name": "Persistent App Language",
"description": "Force Snapchat to stay on a specific supported app language"
},
"generate_fresh_profile_action": {
"name": "Generate Fresh Profile",
"description": "Request a newly generated randomized profile"
},
"view_current_profile_action": {
"name": "View Current Profile",
"description": "Inspect the latest randomized profile snapshot"
}
}
},
"spoof_device_id": { "spoof_device_id": {
"name": "Spoof Device ID", "name": "Spoof Device ID",
"description": "Override the Android ID sent to Snapchat", "description": "Override the Android ID sent to Snapchat",
@@ -2471,6 +2904,9 @@
"custom_android_id": { "custom_android_id": {
"null": "Use real Android ID" "null": "Use real Android ID"
}, },
"persistent_app_language": {
"system_default": "System Default"
},
"add_friend_source_spoof": { "add_friend_source_spoof": {
"added_by_username": "By Username", "added_by_username": "By Username",
"added_by_mention": "By Mention", "added_by_mention": "By Mention",
@@ -3085,6 +3521,7 @@
"positive": "Yes", "positive": "Yes",
"negative": "No", "negative": "No",
"cancel": "Cancel", "cancel": "Cancel",
"copy": "Copy",
"save": "Save", "save": "Save",
"open": "Open", "open": "Open",
"download": "Download", "download": "Download",
@@ -3102,6 +3539,10 @@
"stopped_speaking": "Stopped Speaking", "stopped_speaking": "Stopped Speaking",
"started_peeking": "Started Peeking", "started_peeking": "Started Peeking",
"stopped_peeking": "Stopped Peeking", "stopped_peeking": "Stopped Peeking",
"started_using_reply_camera": "Started Using Reply Camera",
"stopped_using_reply_camera": "Stopped Using Reply Camera",
"started_viewing_chat_media": "Started Viewing Chat Media",
"stopped_viewing_chat_media": "Stopped Viewing Chat Media",
"message_read": "Message Read", "message_read": "Message Read",
"message_deleted": "Message Deleted", "message_deleted": "Message Deleted",
"message_saved": "Message Saved", "message_saved": "Message Saved",
@@ -3114,7 +3555,9 @@
"snap_replayed_twice": "Snap Replayed Twice", "snap_replayed_twice": "Snap Replayed Twice",
"snap_screenshot": "Snap Screenshot", "snap_screenshot": "Snap Screenshot",
"snap_screen_record": "Snap Screen Record", "snap_screen_record": "Snap Screen Record",
"i_can_see_you": "I Can See You" "i_can_see_you": "I Can See You",
"i_can_see_you_2": "I Can See You 2",
"i_can_see_you_3": "I Can See You 3"
}, },
"cleared_from_feed": "Cleared from feed", "cleared_from_feed": "Cleared from feed",
"tracker_actions": { "tracker_actions": {
@@ -3350,6 +3793,10 @@
"stopped_speaking": "{friend} stopped speaking in {conversation}", "stopped_speaking": "{friend} stopped speaking in {conversation}",
"started_peeking": "{friend} started peeking in {conversation}", "started_peeking": "{friend} started peeking in {conversation}",
"stopped_peeking": "{friend} stopped peeking in {conversation}", "stopped_peeking": "{friend} stopped peeking in {conversation}",
"started_using_reply_camera": "{friend} opened the reply camera in {conversation}",
"stopped_using_reply_camera": "{friend} closed the reply camera in {conversation}",
"started_viewing_chat_media": "{friend} started viewing chat media in {conversation}",
"stopped_viewing_chat_media": "{friend} stopped viewing chat media in {conversation}",
"message_read": "{friend} read a message in {conversation}", "message_read": "{friend} read a message in {conversation}",
"message_deleted": "{friend} deleted a message in {conversation}", "message_deleted": "{friend} deleted a message in {conversation}",
"message_saved": "{friend} saved a message in {conversation}", "message_saved": "{friend} saved a message in {conversation}",
@@ -3362,7 +3809,9 @@
"snap_replayed_twice": "{friend} replayed a snap twice in {conversation}", "snap_replayed_twice": "{friend} replayed a snap twice in {conversation}",
"snap_screenshot": "{friend} took a screenshot in {conversation}", "snap_screenshot": "{friend} took a screenshot in {conversation}",
"snap_screen_record": "{friend} screen recorded in {conversation}", "snap_screen_record": "{friend} screen recorded in {conversation}",
"i_can_see_you": "{friend} activity in {conversation}: {details}" "i_can_see_you": "{friend} activity in {conversation}: {details}",
"i_can_see_you_2": "{friend} gallery activity in {conversation}: {details}",
"i_can_see_you_3": "{friend} reply camera activity in {conversation}: {details}"
}, },
"friend_mutation_observer": { "friend_mutation_observer": {
"notification_channel_name": "Friend Mutation Observer", "notification_channel_name": "Friend Mutation Observer",
@@ -3522,9 +3971,13 @@
"started_typing": "Started typing", "started_typing": "Started typing",
"stopped_typing": "Stopped typing", "stopped_typing": "Stopped typing",
"started_speaking": "Started speaking", "started_speaking": "Started speaking",
"stopped_speaking": "Stopped speaking", "stopped_speaking": "Stopped speaking",
"started_peeking": "Started peeking", "started_peeking": "Started peeking",
"stopped_peeking": "Stopped peeking", "stopped_peeking": "Stopped peeking",
"started_using_reply_camera": "Opened reply camera",
"stopped_using_reply_camera": "Closed reply camera",
"started_viewing_chat_media": "Started viewing chat media",
"stopped_viewing_chat_media": "Stopped viewing chat media",
"message_read": "Read message", "message_read": "Read message",
"message_deleted": "Deleted message", "message_deleted": "Deleted message",
"message_saved": "Saved message", "message_saved": "Saved message",
@@ -3576,6 +4029,10 @@
"stopped_speaking": "stopped speaking", "stopped_speaking": "stopped speaking",
"started_peeking": "started peeking", "started_peeking": "started peeking",
"stopped_peeking": "stopped peeking", "stopped_peeking": "stopped peeking",
"started_using_reply_camera": "opened the reply camera",
"stopped_using_reply_camera": "closed the reply camera",
"started_viewing_chat_media": "started viewing chat media",
"stopped_viewing_chat_media": "stopped viewing chat media",
"message_read": "read a message", "message_read": "read a message",
"message_deleted": "deleted a message", "message_deleted": "deleted a message",
"message_saved": "saved a message", "message_saved": "saved a message",
@@ -3588,7 +4045,9 @@
"snap_replayed_twice": "replayed a snap twice", "snap_replayed_twice": "replayed a snap twice",
"snap_screenshot": "took a screenshot", "snap_screenshot": "took a screenshot",
"snap_screen_record": "screen recorded", "snap_screen_record": "screen recorded",
"i_can_see_you": "was active" "i_can_see_you": "was active",
"i_can_see_you_2": "was viewing gallery",
"i_can_see_you_3": "was using the reply camera"
} }
} }
}, },
@@ -3648,6 +4107,7 @@
"disable_feature_loading_label": "Disable Feature Loading", "disable_feature_loading_label": "Disable Feature Loading",
"disable_auto_mapper_label": "Disable Auto Mapper", "disable_auto_mapper_label": "Disable Auto Mapper",
"disable_bypass_indicator_label": "Disable Bypass Indicator", "disable_bypass_indicator_label": "Disable Bypass Indicator",
"disable_cant_login_button_label": "Disable Can't Login Button",
"friend_list": { "friend_list": {
"manage_title": "Manage Friend List", "manage_title": "Manage Friend List",
"export_description": "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.", "export_description": "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.",

View File

@@ -63,7 +63,7 @@ class Experimental : ConfigContainer() {
} }
val nativeHooks = container("native_hooks", NativeHooks()) { icon = Icons.Default.Memory; requireRestart() } val nativeHooks = container("native_hooks", NativeHooks()) { icon = Icons.Default.Memory; requireRestart() }
val spoof = container("spoof", Spoof()) { icon = Icons.Default.Fingerprint ; addNotices(FeatureNotice.BAN_RISK); requireRestart() } val spoof = container("spoof", Spoof()) { icon = Icons.Default.Fingerprint ; requireRestart() }
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() } val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
val mediaFilePicker = boolean("media_file_picker") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val mediaFilePicker = boolean("media_file_picker") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); } val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }

View File

@@ -207,6 +207,8 @@ class MessagingTweaks : ConfigContainer() {
val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() } val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() }
val hidePeekAPeek = boolean("hide_peek_a_peek") val hidePeekAPeek = boolean("hide_peek_a_peek")
val hideBitmojiPresence = boolean("hide_bitmoji_presence") val hideBitmojiPresence = boolean("hide_bitmoji_presence")
val spoofViewingGalleryPresence = boolean("spoof_viewing_gallery_presence")
val spoofReplyCameraPresence = boolean("spoof_reply_camera_presence")
val hideTypingNotifications = boolean("hide_typing_notifications") val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() } val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() }

View File

@@ -4,10 +4,193 @@ import me.eternal.purrfectsnap.common.config.ConfigContainer
import me.eternal.purrfectsnap.common.config.ConfigFlag import me.eternal.purrfectsnap.common.config.ConfigFlag
class Spoof : ConfigContainer(hasGlobalState = true) { class Spoof : ConfigContainer(hasGlobalState = true) {
companion object {
val supportedSnapchatLanguages = listOf(
"ar", "bn", "bn-BD", "bn-IN", "da", "de", "el", "en-GB", "es", "es-AR", "es-ES", "es-MX",
"fi", "fil", "fil-PH", "fr", "gu", "gu-IN", "hi", "hi-IN", "in", "it", "ja", "kn", "kn-IN",
"ko", "ml", "ml-IN", "mr", "mr-IN", "ms", "ms-MY", "nb", "nl", "pa", "pa-IN", "pl", "pt",
"pt-PT", "ro", "ru", "sv", "ta", "ta-IN", "te", "te-IN", "th", "th-TH", "tr", "ur", "ur-PK",
"vi", "vi-VN", "zh", "zh-CN", "zh-TW"
)
}
inner class RandomizedDeviceProfileConfig : ConfigContainer(hasGlobalState = true) {
inner class RandomizedBuildVersionConfig : ConfigContainer(hasGlobalState = true) {
val fingerprint = boolean("fingerprint", defaultValue = true) { requireRestart() }
val display = boolean("display", defaultValue = true) { requireRestart() }
val host = boolean("host", defaultValue = true) { requireRestart() }
val bootloader = boolean("bootloader", defaultValue = true) { requireRestart() }
val buildTime = boolean("build_time", defaultValue = true) { requireRestart() }
}
inner class RandomizedDeviceIdentityConfig : ConfigContainer(hasGlobalState = true) {
val manufacturerModel = boolean("manufacturer_model", defaultValue = true) { requireRestart() }
val brandProduct = boolean("brand_product", defaultValue = true) { requireRestart() }
val hardwareBoard = boolean("hardware_board", defaultValue = true) { requireRestart() }
}
inner class RandomizedAbiListsConfig : ConfigContainer(hasGlobalState = true) {
val combinedAbis = boolean("combined_abis", defaultValue = true) { requireRestart() }
val splitAbis = boolean("split_abis", defaultValue = true) { requireRestart() }
}
inner class RandomizedSystemPropertiesConfig : ConfigContainer(hasGlobalState = true) {
val build = boolean("build", defaultValue = true) { requireRestart() }
val locale = boolean("locale", defaultValue = true) { requireRestart() }
val telephony = boolean("telephony", defaultValue = true) { requireRestart() }
}
inner class RandomizedBuildPropertiesConfig : ConfigContainer(hasGlobalState = true) {
val deviceIdentity = container("device_identity", RandomizedDeviceIdentityConfig().apply { globalState = true })
val abiLists = container("abi_lists", RandomizedAbiListsConfig().apply { globalState = true })
val systemProperties = container("system_properties", RandomizedSystemPropertiesConfig().apply { globalState = true })
val buildVersion = container("build_version", RandomizedBuildVersionConfig().apply { globalState = true })
}
inner class RandomizedLocaleValueConfig : ConfigContainer(hasGlobalState = true) {
val language = boolean("language", defaultValue = true) { requireRestart() }
val region = boolean("region", defaultValue = true) { requireRestart() }
}
inner class RandomizedTimeConfig : ConfigContainer(hasGlobalState = true) {
val timeZoneId = boolean("time_zone_id", defaultValue = false) { requireRestart() }
val timeZoneDisplayName = boolean("time_zone_display_name", defaultValue = false) { requireRestart() }
val autoTime = boolean("auto_time", defaultValue = false) { requireRestart() }
val autoTimeZone = boolean("auto_time_zone", defaultValue = false) { requireRestart() }
}
inner class RandomizedLocaleConfig : ConfigContainer(hasGlobalState = true) {
val locale = container("locale", RandomizedLocaleValueConfig().apply { globalState = true })
val time = container("time", RandomizedTimeConfig().apply { globalState = true })
}
inner class RandomizedMmsConfig : ConfigContainer(hasGlobalState = true) {
val userAgent = boolean("user_agent", defaultValue = true) { requireRestart() }
}
inner class RandomizedNetworkIdentityConfig : ConfigContainer(hasGlobalState = true) {
val networkType = boolean("network_type", defaultValue = true) { requireRestart() }
val operatorNumeric = boolean("operator_numeric", defaultValue = true) { requireRestart() }
val operatorName = boolean("operator_name", defaultValue = true) { requireRestart() }
val countryIso = boolean("country_iso", defaultValue = true) { requireRestart() }
}
inner class RandomizedSimIdentityConfig : ConfigContainer(hasGlobalState = true) {
val countryIso = boolean("country_iso", defaultValue = true) { requireRestart() }
val operatorNumeric = boolean("operator_numeric", defaultValue = true) { requireRestart() }
val operatorName = boolean("operator_name", defaultValue = true) { requireRestart() }
val simState = boolean("sim_state", defaultValue = true) { requireRestart() }
val hasIccCard = boolean("has_icc_card", defaultValue = true) { requireRestart() }
}
inner class RandomizedPhoneCapabilitiesConfig : ConfigContainer(hasGlobalState = true) {
val phoneCount = boolean("phone_count", defaultValue = true) { requireRestart() }
val hearingAid = boolean("hearing_aid", defaultValue = true) { requireRestart() }
val tty = boolean("tty", defaultValue = true) { requireRestart() }
val worldPhone = boolean("world_phone", defaultValue = true) { requireRestart() }
val roaming = boolean("roaming", defaultValue = true) { requireRestart() }
val smsVoice = boolean("sms_voice", defaultValue = true) { requireRestart() }
val phoneType = boolean("phone_type", defaultValue = true) { requireRestart() }
}
inner class RandomizedTelephonyConfig : ConfigContainer(hasGlobalState = true) {
val mms = container("mms", RandomizedMmsConfig().apply { globalState = true })
val networkIdentity = container("network_identity", RandomizedNetworkIdentityConfig().apply { globalState = true })
val simIdentity = container("sim_identity", RandomizedSimIdentityConfig().apply { globalState = true })
val phoneCapabilities = container("phone_capabilities", RandomizedPhoneCapabilitiesConfig().apply { globalState = true })
}
inner class RandomizedSecureSettingsConfig : ConfigContainer(hasGlobalState = true) {
val base = boolean("base", defaultValue = true) { requireRestart() }
val tts = boolean("tts", defaultValue = true) { requireRestart() }
}
inner class RandomizedSystemSettingsConfig : ConfigContainer(hasGlobalState = true) {
val base = boolean("base", defaultValue = true) { requireRestart() }
val bluetooth = boolean("bluetooth", defaultValue = true) { requireRestart() }
}
inner class RandomizedGlobalSettingsConfig : ConfigContainer(hasGlobalState = true) {
val base = boolean("base", defaultValue = true) { requireRestart() }
}
inner class RandomizedSettingsConfig : ConfigContainer(hasGlobalState = true) {
val secure = container("secure", RandomizedSecureSettingsConfig().apply { globalState = true })
val system = container("system", RandomizedSystemSettingsConfig().apply { globalState = true })
val global = container("global", RandomizedGlobalSettingsConfig().apply { globalState = true })
}
inner class RandomizedWifiConfig : ConfigContainer(hasGlobalState = true) {
val ssid = boolean("ssid", defaultValue = true) { requireRestart() }
val rssi = boolean("rssi", defaultValue = true) { requireRestart() }
}
inner class RandomizedDnsConfig : ConfigContainer(hasGlobalState = true) {
val servers = boolean("servers", defaultValue = true) { requireRestart() }
val searchDomains = boolean("search_domains", defaultValue = true) { requireRestart() }
val privateDns = boolean("private_dns", defaultValue = true) { requireRestart() }
}
inner class RandomizedCaptivePortalConfig : ConfigContainer(hasGlobalState = true) {
val capability = boolean("capability", defaultValue = true) { requireRestart() }
}
inner class RandomizedNetworkConfig : ConfigContainer(hasGlobalState = true) {
val wifi = container("wifi", RandomizedWifiConfig().apply { globalState = true })
val dns = container("dns", RandomizedDnsConfig().apply { globalState = true })
val captivePortal = container("captive_portal", RandomizedCaptivePortalConfig().apply { globalState = true })
}
inner class RandomizedAndroidIdConfig : ConfigContainer(hasGlobalState = true) {
val stringValue = boolean("string_value", defaultValue = true) { requireRestart() }
val longValue = boolean("long_value", defaultValue = true) { requireRestart() }
}
inner class RandomizedAdvertisingIdConfig : ConfigContainer(hasGlobalState = true) {
val settingsValue = boolean("settings_value", defaultValue = true) { requireRestart() }
val playServices = boolean("play_services", defaultValue = true) { requireRestart() }
}
inner class RandomizedHardwareAddressesConfig : ConfigContainer(hasGlobalState = true) {
val wifiMac = boolean("wifi_mac", defaultValue = true) { requireRestart() }
val bluetoothMac = boolean("bluetooth_mac", defaultValue = true) { requireRestart() }
}
inner class RandomizedIdentifiersConfig : ConfigContainer(hasGlobalState = true) {
val androidId = container("android_id", RandomizedAndroidIdConfig().apply { globalState = true })
val advertisingId = container("advertising_id", RandomizedAdvertisingIdConfig().apply { globalState = true })
val hardwareAddresses = container("hardware_addresses", RandomizedHardwareAddressesConfig().apply { globalState = true })
}
val showActivationOverlay = boolean("show_activation_overlay", defaultValue = false)
val randomizeIpAddress = boolean("randomize_ip_address", defaultValue = false) { requireRestart() }
val buildProperties = container("build_properties", RandomizedBuildPropertiesConfig().apply { globalState = true })
val localeOptions = container("locale_options", RandomizedLocaleConfig().apply { globalState = true })
val telephonyOptions = container("telephony_options", RandomizedTelephonyConfig().apply { globalState = true })
val settingsOptions = container("settings_options", RandomizedSettingsConfig().apply { globalState = true })
val networkOptions = container("network_options", RandomizedNetworkConfig().apply { globalState = true })
val identifierOptions = container("identifier_options", RandomizedIdentifiersConfig().apply { globalState = true })
val persistentAppLanguage = unique("persistent_app_language", *supportedSnapchatLanguages.toTypedArray()) {
requireRestart()
addFlags(ConfigFlag.NO_TRANSLATE)
disabledKey = "system_default"
customOptionTranslationPath = "features.options.persistent_app_language"
}
val generateFreshProfileAction = string("generate_fresh_profile_action")
val viewCurrentProfileAction = string("view_current_profile_action")
val profileGenerationToken = string("profile_generation_token") {
addFlags(ConfigFlag.HIDDEN)
}
val currentProfileSnapshot = string("current_profile_snapshot") {
addFlags(ConfigFlag.HIDDEN)
}
}
inner class SpoofDeviceIdConfig : ConfigContainer() { inner class SpoofDeviceIdConfig : ConfigContainer() {
val spoofAndroidId = boolean("spoof_android_id") { requireRestart() } val spoofAndroidId = boolean("spoof_android_id") { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
val customAndroidId = string("custom_android_id") { val customAndroidId = string("custom_android_id") {
requireRestart() requireRestart()
addFlags(ConfigFlag.HIDDEN)
inputCheck = { it.isEmpty() || (it.length == 16 && it.all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' }) } inputCheck = { it.isEmpty() || (it.length == 16 && it.all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' }) }
} }
} }
@@ -16,8 +199,9 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
val removeVpnTransportFlag = boolean("remove_vpn_transport_flag") { requireRestart() } val removeVpnTransportFlag = boolean("remove_vpn_transport_flag") { requireRestart() }
val removeMockLocationFlag = boolean("remove_mock_location_flag") { requireRestart() } val removeMockLocationFlag = boolean("remove_mock_location_flag") { requireRestart() }
val forceWifiTransportFlag = boolean("force_wifi_transport_flag") { requireRestart() } val forceWifiTransportFlag = boolean("force_wifi_transport_flag") { requireRestart() }
val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart() } val randomizeDeviceProfile = container("randomize_device_profile", RandomizedDeviceProfileConfig()) { requireRestart() }
val spoofDevice = boolean("spoof_device") { requireRestart() } val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
val spoofDevice = boolean("spoof_device") { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
val deviceModel = unique("device_model", val deviceModel = unique("device_model",
"none", "none",
"random", "random",
@@ -38,6 +222,7 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
"realme GT 6" "realme GT 6"
) { ) {
requireRestart() requireRestart()
addFlags(ConfigFlag.HIDDEN)
customOptionTranslationPath = "features.options.device_model" customOptionTranslationPath = "features.options.device_model"
} }
} }

View File

@@ -9,7 +9,9 @@ data class FriendPresenceState(
val typing: Boolean, val typing: Boolean,
val wasTyping: Boolean, val wasTyping: Boolean,
val speaking: Boolean, val speaking: Boolean,
val peeking: Boolean val peeking: Boolean,
val usingReplyCamera: Boolean,
val viewingChatMedia: Boolean
) )
open class SessionEvent( open class SessionEvent(
@@ -44,6 +46,8 @@ enum class SessionEventType(
SNAP_SCREENSHOT("snap_screenshot"), SNAP_SCREENSHOT("snap_screenshot"),
SNAP_SCREEN_RECORD("snap_screen_record"), SNAP_SCREEN_RECORD("snap_screen_record"),
I_CAN_SEE_YOU("i_can_see_you"), I_CAN_SEE_YOU("i_can_see_you"),
I_CAN_SEE_YOU_2("i_can_see_you_2"),
I_CAN_SEE_YOU_3("i_can_see_you_3"),
} }
enum class TrackerEventType( enum class TrackerEventType(
@@ -58,6 +62,10 @@ enum class TrackerEventType(
STOPPED_SPEAKING("stopped_speaking"), STOPPED_SPEAKING("stopped_speaking"),
STARTED_PEEKING("started_peeking"), STARTED_PEEKING("started_peeking"),
STOPPED_PEEKING("stopped_peeking"), STOPPED_PEEKING("stopped_peeking"),
STARTED_USING_REPLY_CAMERA("started_using_reply_camera"),
STOPPED_USING_REPLY_CAMERA("stopped_using_reply_camera"),
STARTED_VIEWING_CHAT_MEDIA("started_viewing_chat_media"),
STOPPED_VIEWING_CHAT_MEDIA("stopped_viewing_chat_media"),
// mcs events // mcs events
MESSAGE_READ("message_read"), MESSAGE_READ("message_read"),
@@ -73,6 +81,8 @@ enum class TrackerEventType(
SNAP_SCREENSHOT("snap_screenshot"), SNAP_SCREENSHOT("snap_screenshot"),
SNAP_SCREEN_RECORD("snap_screen_record"), SNAP_SCREEN_RECORD("snap_screen_record"),
I_CAN_SEE_YOU("i_can_see_you"), I_CAN_SEE_YOU("i_can_see_you"),
I_CAN_SEE_YOU_2("i_can_see_you_2"),
I_CAN_SEE_YOU_3("i_can_see_you_3"),
} }

View File

@@ -319,6 +319,7 @@ class SecurityFeatures(
loginHelpComposable = { loginHelpComposable = {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var isLoginScreen by remember { mutableStateOf(false) } var isLoginScreen by remember { mutableStateOf(false) }
val disableHelpButton = context.bridgeClient.getDebugProp("disable_cant_login_button", "false") == "true"
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
while (true) { while (true) {
@@ -329,13 +330,13 @@ class SecurityFeatures(
} }
} }
if (isLoginScreen) { if (isLoginScreen && !disableHelpButton) {
LoginSignupHelpButton( LoginSignupHelpButton(
onClick = { showDialog = true } onClick = { showDialog = true }
) )
} }
if (isLoginScreen && showDialog) { if (isLoginScreen && !disableHelpButton && showDialog) {
LoginSignupHelpDialog( LoginSignupHelpDialog(
onDismiss = { showDialog = false } onDismiss = { showDialog = false }
) )
@@ -433,6 +434,9 @@ class SecurityFeatures(
context.features.addActivityCreateListener { activity -> context.features.addActivityCreateListener { activity ->
if (!activity.javaClass.name.endsWith("LoginSignupActivity")) return@addActivityCreateListener if (!activity.javaClass.name.endsWith("LoginSignupActivity")) return@addActivityCreateListener
if (context.bridgeClient.getDebugProp("disable_cant_login_button", "false") == "true") {
return@addActivityCreateListener
}
activity.findViewById<ViewGroup>(android.R.id.content).apply { activity.findViewById<ViewGroup>(android.R.id.content).apply {
visibility = ViewGroup.INVISIBLE visibility = ViewGroup.INVISIBLE

View File

@@ -30,6 +30,7 @@ import me.eternal.purrfectsnap.common.data.SocialScope
import me.eternal.purrfectsnap.common.ui.OverlayType import me.eternal.purrfectsnap.common.ui.OverlayType
import me.eternal.purrfectsnap.common.util.toSerialized import me.eternal.purrfectsnap.common.util.toSerialized
import me.eternal.purrfectsnap.core.ModContext import me.eternal.purrfectsnap.core.ModContext
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -234,10 +235,48 @@ class BridgeClient(
fun passGroupsAndFriends(groups: List<MessagingGroupInfo>, friends: List<MessagingFriendInfo>) = fun passGroupsAndFriends(groups: List<MessagingGroupInfo>, friends: List<MessagingFriendInfo>) =
safeServiceCall { safeServiceCall {
service.passGroupsAndFriends( val serializedGroups = groups.mapNotNull { it.toSerialized() }
groups.mapNotNull { it.toSerialized() }, val serializedFriends = friends.mapNotNull { it.toSerialized() }
friends.mapNotNull { it.toSerialized() } val maxChunkBytes = 128 * 1024
fun chunkSerialized(values: List<String>): List<List<String>> {
if (values.isEmpty()) return listOf(emptyList())
val result = mutableListOf<List<String>>()
val currentChunk = mutableListOf<String>()
var currentSize = 0
values.forEach { value ->
val valueSize = value.toByteArray(StandardCharsets.UTF_8).size + 32
if (currentChunk.isNotEmpty() && currentSize + valueSize > maxChunkBytes) {
result += currentChunk.toList()
currentChunk.clear()
currentSize = 0
}
currentChunk += value
currentSize += valueSize
}
if (currentChunk.isNotEmpty()) {
result += currentChunk.toList()
}
return result
}
val groupChunks = chunkSerialized(serializedGroups)
val friendChunks = chunkSerialized(serializedFriends)
val chunkCount = maxOf(groupChunks.size, friendChunks.size)
context.log.info(
"Sending social snapshot in $chunkCount chunk(s): " +
"${serializedGroups.size} groups, ${serializedFriends.size} friends"
) )
repeat(chunkCount) { index ->
service.passGroupsAndFriends(
groupChunks.getOrElse(index) { emptyList() },
friendChunks.getOrElse(index) { emptyList() }
)
}
} }
fun getRules(targetUuid: String): List<MessagingRuleType> = safeServiceCall { fun getRules(targetUuid: String): List<MessagingRuleType> = safeServiceCall {

View File

@@ -15,193 +15,358 @@ data class DeviceInfo(
val host: String val host: String
) )
data class DeviceBuildProfile(
val androidRelease: String,
val display: String,
val buildId: String,
val incremental: String,
val host: String,
val bootloader: String? = null
)
data class DeviceCapabilityProfile(
val supportedAbis: List<String>,
val supported32BitAbis: List<String>,
val supported64BitAbis: List<String>,
val phoneCount: Int,
val isHearingAidCompatibilitySupported: Boolean,
val isTtySupported: Boolean,
val isWorldPhone: Boolean,
val isSmsCapable: Boolean,
val isVoiceCapable: Boolean,
val phoneType: Int,
val phoneTypeString: String
)
data class DeviceTemplate(
val marketingName: String,
val deviceInfo: DeviceInfo,
val builds: List<DeviceBuildProfile>,
val capabilities: DeviceCapabilityProfile
)
object DeviceSpoofer { object DeviceSpoofer {
private val defaultCapabilities = DeviceCapabilityProfile(
supportedAbis = listOf("arm64-v8a", "armeabi-v7a", "armeabi"),
supported32BitAbis = listOf("armeabi-v7a", "armeabi"),
supported64BitAbis = listOf("arm64-v8a"),
phoneCount = 2,
isHearingAidCompatibilitySupported = true,
isTtySupported = false,
isWorldPhone = true,
isSmsCapable = true,
isVoiceCapable = true,
phoneType = 1,
phoneTypeString = "PHONE_TYPE_GSM"
)
private val singleSimCapabilities = defaultCapabilities.copy(phoneCount = 1)
private val devices = mapOf( private val devices = mapOf(
"Pixel 8 Pro" to DeviceInfo( "Pixel 8 Pro" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 8 Pro",
model = "Pixel 8 Pro", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "husky", model = "Pixel 8 Pro",
product = "husky", brand = "google",
hardware = "husky", device = "husky",
board = "husky", product = "husky",
bootloader = "husky-1.0-11003666", hardware = "husky",
display = "UQ1A.231205.015", board = "husky",
host = "abfarm-release-rbe-64-00163" bootloader = "husky-1.0-11003666",
display = "UQ1A.231205.015",
host = "abfarm-release-rbe-64-00163"
),
builds = listOf(
DeviceBuildProfile("14", "UQ1A.231205.015", "UQ1A.231205.015", "11003666", "abfarm-release-rbe-64-00163", "husky-1.0-11003666"),
DeviceBuildProfile("15", "AP4A.250205.002", "AP4A.250205.002", "12141234", "abfarm-release-rbe-64-00171", "husky-1.0-12141234")
),
capabilities = defaultCapabilities.copy(phoneCount = 1, isWorldPhone = false)
), ),
"Pixel 9 Pro XL" to DeviceInfo( "Pixel 9 Pro XL" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 9 Pro XL",
model = "Pixel 9 Pro XL", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "komodo", model = "Pixel 9 Pro XL",
product = "komodo", brand = "google",
hardware = "komodo", device = "komodo",
board = "komodo", product = "komodo",
bootloader = "komodo-1.0-12110753", hardware = "komodo",
display = "AP3A.241105.008", board = "komodo",
host = "abfarm-release-rbe-64-00163" bootloader = "komodo-1.0-12110753",
display = "AP3A.241105.008",
host = "abfarm-release-rbe-64-00163"
),
builds = listOf(
DeviceBuildProfile("14", "AP3A.241105.008", "AP3A.241105.008", "12110753", "abfarm-release-rbe-64-00163", "komodo-1.0-12110753"),
DeviceBuildProfile("15", "BP1A.250105.006", "BP1A.250105.006", "13120567", "abfarm-release-rbe-65-00088", "komodo-1.0-13120567")
),
capabilities = defaultCapabilities.copy(phoneCount = 1, isWorldPhone = false)
), ),
"Pixel 10" to DeviceInfo( "Pixel 10" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 10",
model = "Pixel 10", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "frankel", model = "Pixel 10",
product = "frankel", brand = "google",
hardware = "tensor_g5", device = "frankel",
board = "frankel", product = "frankel",
bootloader = "frankel-1.0-12345678", hardware = "tensor_g5",
display = "BP1A.250105.002", board = "frankel",
host = "abfarm-release-rbe-65-00200" bootloader = "frankel-1.0-12345678",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00200"
),
builds = listOf(
DeviceBuildProfile("15", "BP1A.250105.002", "BP1A.250105.002", "12345678", "abfarm-release-rbe-65-00200", "frankel-1.0-12345678")
),
capabilities = defaultCapabilities.copy(phoneCount = 1, isWorldPhone = false)
), ),
"Pixel 10 Pro" to DeviceInfo( "Pixel 10 Pro" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 10 Pro",
model = "Pixel 10 Pro", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "blazer", model = "Pixel 10 Pro",
product = "blazer", brand = "google",
hardware = "tensor_g5", device = "blazer",
board = "blazer", product = "blazer",
bootloader = "blazer-1.0-12345679", hardware = "tensor_g5",
display = "BP1A.250105.002", board = "blazer",
host = "abfarm-release-rbe-65-00201" bootloader = "blazer-1.0-12345679",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00201"
),
builds = listOf(
DeviceBuildProfile("15", "BP1A.250105.002", "BP1A.250105.002", "12345679", "abfarm-release-rbe-65-00201", "blazer-1.0-12345679")
),
capabilities = defaultCapabilities.copy(phoneCount = 1, isWorldPhone = false)
), ),
"Pixel 10 Pro XL" to DeviceInfo( "Pixel 10 Pro XL" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 10 Pro XL",
model = "Pixel 10 Pro XL", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "mustang", model = "Pixel 10 Pro XL",
product = "mustang", brand = "google",
hardware = "tensor_g5", device = "mustang",
board = "mustang", product = "mustang",
bootloader = "mustang-1.0-12345680", hardware = "tensor_g5",
display = "BP1A.250105.002", board = "mustang",
host = "abfarm-release-rbe-65-00202" bootloader = "mustang-1.0-12345680",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00202"
),
builds = listOf(
DeviceBuildProfile("15", "BP1A.250105.002", "BP1A.250105.002", "12345680", "abfarm-release-rbe-65-00202", "mustang-1.0-12345680")
),
capabilities = defaultCapabilities.copy(phoneCount = 1, isWorldPhone = false)
), ),
"Pixel 10 Pro Fold" to DeviceInfo( "Pixel 10 Pro Fold" to DeviceTemplate(
manufacturer = "Google", marketingName = "Pixel 10 Pro Fold",
model = "Pixel 10 Pro Fold", deviceInfo = DeviceInfo(
brand = "google", manufacturer = "Google",
device = "rango", model = "Pixel 10 Pro Fold",
product = "rango", brand = "google",
hardware = "tensor_g5", device = "rango",
board = "rango", product = "rango",
bootloader = "rango-1.0-12345681", hardware = "tensor_g5",
display = "BP1A.250105.002", board = "rango",
host = "abfarm-release-rbe-65-00203" bootloader = "rango-1.0-12345681",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00203"
),
builds = listOf(
DeviceBuildProfile("15", "BP1A.250105.002", "BP1A.250105.002", "12345681", "abfarm-release-rbe-65-00203", "rango-1.0-12345681")
),
capabilities = singleSimCapabilities.copy(isWorldPhone = false)
), ),
"Galaxy S23 Ultra" to DeviceInfo( "Galaxy S23 Ultra" to DeviceTemplate(
manufacturer = "Samsung", marketingName = "Galaxy S23 Ultra",
model = "SM-S918B", deviceInfo = DeviceInfo(
brand = "samsung", manufacturer = "Samsung",
device = "dm3q", model = "SM-S918B",
product = "dm3qxx", brand = "samsung",
hardware = "qcom", device = "dm3q",
board = "kalama", product = "dm3qxx",
bootloader = "S918BXXU3BWJM", hardware = "qcom",
display = "UP1A.231005.007.S918BXXU3BWJM", board = "kalama",
host = "21DH7R2P" bootloader = "S918BXXU3BWJM",
display = "UP1A.231005.007.S918BXXU3BWJM",
host = "21DH7R2P"
),
builds = listOf(
DeviceBuildProfile("14", "UP1A.231005.007.S918BXXU3BWJM", "UP1A.231005.007", "S918BXXU3BWJM", "21DH7R2P", "S918BXXU3BWJM"),
DeviceBuildProfile("15", "AP3A.240905.015.S918BXXU4CXA1", "AP3A.240905.015", "S918BXXU4CXA1", "21DH7R2P", "S918BXXU4CXA1")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"Galaxy S24 Ultra" to DeviceInfo( "Galaxy S24 Ultra" to DeviceTemplate(
manufacturer = "Samsung", marketingName = "Galaxy S24 Ultra",
model = "SM-S928B", deviceInfo = DeviceInfo(
brand = "samsung", manufacturer = "Samsung",
device = "e9q", model = "SM-S928B",
product = "e9qxx", brand = "samsung",
hardware = "qcom", device = "e9q",
board = "pineapple", product = "e9qxx",
bootloader = "S928BXXU1AXB5", hardware = "qcom",
display = "UP1A.231005.007.S928BXXU1AXB5", board = "pineapple",
host = "21DH7R2P" bootloader = "S928BXXU1AXB5",
display = "UP1A.231005.007.S928BXXU1AXB5",
host = "21DH7R2P"
),
builds = listOf(
DeviceBuildProfile("14", "UP1A.231005.007.S928BXXU1AXB5", "UP1A.231005.007", "S928BXXU1AXB5", "21DH7R2P", "S928BXXU1AXB5"),
DeviceBuildProfile("15", "AP3A.240905.015.S928BXXU2BYD6", "AP3A.240905.015", "S928BXXU2BYD6", "21DH7R2P", "S928BXXU2BYD6")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"Galaxy S25 Ultra" to DeviceInfo( "Galaxy S25 Ultra" to DeviceTemplate(
manufacturer = "Samsung", marketingName = "Galaxy S25 Ultra",
model = "SM-S938B", deviceInfo = DeviceInfo(
brand = "samsung", manufacturer = "Samsung",
device = "e3q", model = "SM-S938B",
product = "e3qxx", brand = "samsung",
hardware = "qcom", device = "e3q",
board = "s5e9945", product = "e3qxx",
bootloader = "S938BXXU1AXL2", hardware = "qcom",
display = "UP1A.231005.007.S938BXXU1AXL2", board = "s5e9945",
host = "21DH7R2P" bootloader = "S938BXXU1AXL2",
display = "UP1A.231005.007.S938BXXU1AXL2",
host = "21DH7R2P"
),
builds = listOf(
DeviceBuildProfile("15", "AP3A.241005.019.S938BXXU1AXL2", "AP3A.241005.019", "S938BXXU1AXL2", "21DH7R2P", "S938BXXU1AXL2")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"OnePlus 15" to DeviceInfo( "OnePlus 15" to DeviceTemplate(
manufacturer = "OnePlus", marketingName = "OnePlus 15",
model = "CPH2651", deviceInfo = DeviceInfo(
brand = "OnePlus", manufacturer = "OnePlus",
device = "OP5929L1", model = "CPH2651",
product = "OP5929L1_EEA", brand = "OnePlus",
hardware = "qcom", device = "OP5929L1",
board = "taro", product = "OP5929L1_EEA",
bootloader = "unknown", hardware = "qcom",
display = "CPH2651_15.0.0.503(EX01)", board = "taro",
host = "ubuntu-build" bootloader = "unknown",
display = "CPH2651_15.0.0.503(EX01)",
host = "ubuntu-build"
),
builds = listOf(
DeviceBuildProfile("15", "CPH2651_15.0.0.503(EX01)", "CPH2651_15.0.0.503(EX01)", "15.0.0.503", "ubuntu-build"),
DeviceBuildProfile("15", "CPH2651_15.0.0.601(EX01)", "CPH2651_15.0.0.601(EX01)", "15.0.0.601", "ubuntu-build")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"OnePlus Open" to DeviceInfo( "OnePlus Open" to DeviceTemplate(
manufacturer = "OnePlus", marketingName = "OnePlus Open",
model = "CPH2551", deviceInfo = DeviceInfo(
brand = "OnePlus", manufacturer = "OnePlus",
device = "OP594DL1", model = "CPH2551",
product = "OP594DL1_EEA", brand = "OnePlus",
hardware = "qcom", device = "OP594DL1",
board = "taro", product = "OP594DL1_EEA",
bootloader = "unknown", hardware = "qcom",
display = "CPH2551_14.0.0.600(EX01)", board = "taro",
host = "ubuntu-build" bootloader = "unknown",
display = "CPH2551_14.0.0.600(EX01)",
host = "ubuntu-build"
),
builds = listOf(
DeviceBuildProfile("14", "CPH2551_14.0.0.600(EX01)", "CPH2551_14.0.0.600(EX01)", "14.0.0.600", "ubuntu-build"),
DeviceBuildProfile("15", "CPH2551_15.0.0.305(EX01)", "CPH2551_15.0.0.305(EX01)", "15.0.0.305", "ubuntu-build")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"Xiaomi 15 Ultra" to DeviceInfo( "Xiaomi 15 Ultra" to DeviceTemplate(
manufacturer = "Xiaomi", marketingName = "Xiaomi 15 Ultra",
model = "25010PN30G", deviceInfo = DeviceInfo(
brand = "Xiaomi", manufacturer = "Xiaomi",
device = "xuanyuan", model = "25010PN30G",
product = "xuanyuan_global", brand = "Xiaomi",
hardware = "qcom", device = "xuanyuan",
board = "taro", product = "xuanyuan_global",
bootloader = "unknown", hardware = "qcom",
display = "VK.15.0.3.0.VNGMIXM", board = "taro",
host = "c3-miui-ota-bd164.bj" bootloader = "unknown",
display = "VK.15.0.3.0.VNGMIXM",
host = "c3-miui-ota-bd164.bj"
),
builds = listOf(
DeviceBuildProfile("15", "VK.15.0.3.0.VNGMIXM", "VK.15.0.3.0.VNGMIXM", "15.0.3.0", "c3-miui-ota-bd164.bj"),
DeviceBuildProfile("15", "VK.15.0.6.0.VNGMIXM", "VK.15.0.6.0.VNGMIXM", "15.0.6.0", "c3-miui-ota-bd164.bj")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"OPPO Find X9 Pro" to DeviceInfo( "OPPO Find X9 Pro" to DeviceTemplate(
manufacturer = "OPPO", marketingName = "OPPO Find X9 Pro",
model = "PHY110", deviceInfo = DeviceInfo(
brand = "OPPO", manufacturer = "OPPO",
device = "OP595DL1", model = "PHY110",
product = "OP595DL1_EEA", brand = "OPPO",
hardware = "mt6989", device = "OP595DL1",
board = "k6989v1_64", product = "OP595DL1_EEA",
bootloader = "unknown", hardware = "mt6989",
display = "PHY110_15.0.0.100(EX01)", board = "k6989v1_64",
host = "ubuntu-build-server" bootloader = "unknown",
display = "PHY110_15.0.0.100(EX01)",
host = "ubuntu-build-server"
),
builds = listOf(
DeviceBuildProfile("15", "PHY110_15.0.0.100(EX01)", "PHY110_15.0.0.100(EX01)", "15.0.0.100", "ubuntu-build-server"),
DeviceBuildProfile("15", "PHY110_15.0.0.202(EX01)", "PHY110_15.0.0.202(EX01)", "15.0.0.202", "ubuntu-build-server")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"vivo X100 Pro" to DeviceInfo( "vivo X100 Pro" to DeviceTemplate(
manufacturer = "vivo", marketingName = "vivo X100 Pro",
model = "V2309A", deviceInfo = DeviceInfo(
brand = "vivo", manufacturer = "vivo",
device = "V2309A", model = "V2309A",
product = "PD2309", brand = "vivo",
hardware = "mt6989", device = "V2309A",
board = "k6989v1_64", product = "PD2309",
bootloader = "unknown", hardware = "mt6989",
display = "OP557L.PD2309.14.0.0.100", board = "k6989v1_64",
host = "compiler-server" bootloader = "unknown",
display = "PD2309F_EX_A_14.0.13.2.W30",
host = "compiler-server"
),
builds = listOf(
DeviceBuildProfile("14", "PD2309F_EX_A_14.0.13.2.W30", "PD2309F_EX_A_14.0.13.2.W30", "14.0.13.2", "compiler-server"),
DeviceBuildProfile("15", "PD2309F_EX_A_15.0.8.5.W30", "PD2309F_EX_A_15.0.8.5.W30", "15.0.8.5", "compiler-server")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
), ),
"realme GT 6" to DeviceInfo( "realme GT 6" to DeviceTemplate(
manufacturer = "realme", marketingName = "realme GT 6",
model = "RMX3851", deviceInfo = DeviceInfo(
brand = "realme", manufacturer = "realme",
device = "RMX3851", model = "RMX3851",
product = "RMX3851_11_A.13", brand = "realme",
hardware = "qcom", device = "RMX3851",
board = "taro", product = "RMX3851_11_A.13",
bootloader = "unknown", hardware = "qcom",
display = "RMX3851_14.0.0.700(EX01)", board = "taro",
host = "ubuntu-server" bootloader = "unknown",
display = "RMX3851_14.0.0.700(EX01)",
host = "ubuntu-server"
),
builds = listOf(
DeviceBuildProfile("14", "RMX3851_14.0.0.700(EX01)", "RMX3851_14.0.0.700(EX01)", "14.0.0.700", "ubuntu-server"),
DeviceBuildProfile("15", "RMX3851_15.0.0.205(EX01)", "RMX3851_15.0.0.205(EX01)", "15.0.0.205", "ubuntu-server")
),
capabilities = defaultCapabilities.copy(phoneCount = 2)
) )
) )
fun getAvailableDevices(): List<String> = devices.keys.toList() fun getAvailableDevices(): List<String> = devices.keys.toList()
fun getDeviceInfo(modelName: String): DeviceInfo? { fun getDeviceInfo(modelName: String): DeviceInfo? {
return devices[modelName]?.deviceInfo
}
fun getDeviceTemplate(modelName: String): DeviceTemplate? {
return devices[modelName] return devices[modelName]
} }
@@ -211,6 +376,10 @@ object DeviceSpoofer {
return "${deviceInfo.brand}/${deviceInfo.product}/${deviceInfo.device}:$buildVersion/$id/$incremental:user/release-keys" return "${deviceInfo.brand}/${deviceInfo.product}/${deviceInfo.device}:$buildVersion/$id/$incremental:user/release-keys"
} }
fun generateFingerprint(deviceInfo: DeviceInfo, buildProfile: DeviceBuildProfile): String {
return "${deviceInfo.brand}/${deviceInfo.product}/${deviceInfo.device}:${buildProfile.androidRelease}/${buildProfile.buildId}/${buildProfile.incremental}:user/release-keys"
}
fun generateAndroidId(): String { fun generateAndroidId(): String {
val random = SecureRandom() val random = SecureRandom()
val bytes = ByteArray(8) val bytes = ByteArray(8)

View File

@@ -77,7 +77,6 @@ class MediaFilePicker : Feature("Media File Picker") {
private var bypassSplitOnce = false private var bypassSplitOnce = false
private var sendSingleItemHandler: ((Any) -> Boolean)? = null private var sendSingleItemHandler: ((Any) -> Boolean)? = null
private var cleanupItemHandler: ((String) -> Unit)? = null private var cleanupItemHandler: ((String) -> Unit)? = null
fun hasQueuedSplitItems(): Boolean = queuedSplitItems.isNotEmpty() fun hasQueuedSplitItems(): Boolean = queuedSplitItems.isNotEmpty()
fun hasPendingSplitCleanup(): Boolean = queuedSplitItemIds.isNotEmpty() fun hasPendingSplitCleanup(): Boolean = queuedSplitItemIds.isNotEmpty()
fun hasOriginalUnsplitItem(): Boolean = originalUnsplitItem != null fun hasOriginalUnsplitItem(): Boolean = originalUnsplitItem != null

View File

@@ -0,0 +1,573 @@
package me.eternal.purrfectsnap.core.features.impl.experiments
import android.content.Context
import me.eternal.purrfectsnap.common.logger.AbstractLogger
import org.json.JSONArray
import org.json.JSONObject
import java.net.InetAddress
import java.security.SecureRandom
import java.util.Locale
import java.util.TimeZone
import java.util.UUID
data class RandomizedDeviceProfile(
val schemaVersion: Int,
val profileId: String,
val deviceInfo: DeviceInfo,
val androidRelease: String,
val buildIncremental: String,
val buildDisplayId: String,
val buildFingerprint: String,
val buildHost: String,
val buildTime: Long,
val supportedAbis: List<String>,
val supported32BitAbis: List<String>,
val supported64BitAbis: List<String>,
val androidId: String,
val gsfId: String,
val advertisingId: String,
val wifiMacAddress: String,
val bluetoothMacAddress: String,
val ipAddress: String,
val wifiSsid: String,
val wifiRssi: Int,
val localeTag: String,
val countryIso: String,
val timeZoneId: String,
val timeZoneDisplayName: String,
val networkType: Int,
val networkOperator: String,
val networkOperatorName: String,
val networkCountryIso: String,
val simCountryIso: String,
val simOperator: String,
val simOperatorName: String,
val simState: Int,
val hasIccCard: Boolean,
val phoneCount: Int,
val isHearingAidCompatibilitySupported: Boolean,
val isTtySupported: Boolean,
val isWorldPhone: Boolean,
val isNetworkRoaming: Boolean,
val isSmsCapable: Boolean,
val isVoiceCapable: Boolean,
val phoneType: Int,
val phoneTypeString: String,
val mmsUaProfUrl: String,
val mmsUserAgent: String,
val dnsServers: List<String>,
val dnsSearchDomains: String,
val privateDnsServerName: String,
val privateDnsActive: Boolean,
val hasCaptivePortal: Boolean,
val secureStringSettings: Map<String, String>,
val secureIntSettings: Map<String, Int>,
val systemStringSettings: Map<String, String>,
val systemIntSettings: Map<String, Int>,
val globalStringSettings: Map<String, String>,
val globalIntSettings: Map<String, Int>
) {
fun locale(): Locale = Locale.forLanguageTag(localeTag)
fun timeZone(): TimeZone = TimeZone.getTimeZone(timeZoneId)
fun toJson(): JSONObject = JSONObject().apply {
put("schemaVersion", schemaVersion)
put("profileId", profileId)
put("deviceInfo", JSONObject().apply {
put("manufacturer", deviceInfo.manufacturer)
put("model", deviceInfo.model)
put("brand", deviceInfo.brand)
put("device", deviceInfo.device)
put("product", deviceInfo.product)
put("hardware", deviceInfo.hardware)
put("board", deviceInfo.board)
put("bootloader", deviceInfo.bootloader)
put("display", deviceInfo.display)
put("host", deviceInfo.host)
})
put("androidRelease", androidRelease)
put("buildIncremental", buildIncremental)
put("buildDisplayId", buildDisplayId)
put("buildFingerprint", buildFingerprint)
put("buildHost", buildHost)
put("buildTime", buildTime)
put("supportedAbis", JSONArray(supportedAbis))
put("supported32BitAbis", JSONArray(supported32BitAbis))
put("supported64BitAbis", JSONArray(supported64BitAbis))
put("androidId", androidId)
put("gsfId", gsfId)
put("advertisingId", advertisingId)
put("wifiMacAddress", wifiMacAddress)
put("bluetoothMacAddress", bluetoothMacAddress)
put("ipAddress", ipAddress)
put("wifiSsid", wifiSsid)
put("wifiRssi", wifiRssi)
put("localeTag", localeTag)
put("countryIso", countryIso)
put("timeZoneId", timeZoneId)
put("timeZoneDisplayName", timeZoneDisplayName)
put("networkType", networkType)
put("networkOperator", networkOperator)
put("networkOperatorName", networkOperatorName)
put("networkCountryIso", networkCountryIso)
put("simCountryIso", simCountryIso)
put("simOperator", simOperator)
put("simOperatorName", simOperatorName)
put("simState", simState)
put("hasIccCard", hasIccCard)
put("phoneCount", phoneCount)
put("isHearingAidCompatibilitySupported", isHearingAidCompatibilitySupported)
put("isTtySupported", isTtySupported)
put("isWorldPhone", isWorldPhone)
put("isNetworkRoaming", isNetworkRoaming)
put("isSmsCapable", isSmsCapable)
put("isVoiceCapable", isVoiceCapable)
put("phoneType", phoneType)
put("phoneTypeString", phoneTypeString)
put("mmsUaProfUrl", mmsUaProfUrl)
put("mmsUserAgent", mmsUserAgent)
put("dnsServers", JSONArray(dnsServers))
put("dnsSearchDomains", dnsSearchDomains)
put("privateDnsServerName", privateDnsServerName)
put("privateDnsActive", privateDnsActive)
put("hasCaptivePortal", hasCaptivePortal)
put("secureStringSettings", JSONObject(secureStringSettings))
put("secureIntSettings", JSONObject(secureIntSettings))
put("systemStringSettings", JSONObject(systemStringSettings))
put("systemIntSettings", JSONObject(systemIntSettings))
put("globalStringSettings", JSONObject(globalStringSettings))
put("globalIntSettings", JSONObject(globalIntSettings))
}
companion object {
fun fromJson(json: String): RandomizedDeviceProfile {
val root = JSONObject(json)
val deviceInfoJson = root.getJSONObject("deviceInfo")
return RandomizedDeviceProfile(
schemaVersion = root.getInt("schemaVersion"),
profileId = root.getString("profileId"),
deviceInfo = DeviceInfo(
manufacturer = deviceInfoJson.getString("manufacturer"),
model = deviceInfoJson.getString("model"),
brand = deviceInfoJson.getString("brand"),
device = deviceInfoJson.getString("device"),
product = deviceInfoJson.getString("product"),
hardware = deviceInfoJson.getString("hardware"),
board = deviceInfoJson.getString("board"),
bootloader = deviceInfoJson.getString("bootloader"),
display = deviceInfoJson.getString("display"),
host = deviceInfoJson.getString("host")
),
androidRelease = root.getString("androidRelease"),
buildIncremental = root.getString("buildIncremental"),
buildDisplayId = root.getString("buildDisplayId"),
buildFingerprint = root.getString("buildFingerprint"),
buildHost = root.getString("buildHost"),
buildTime = root.getLong("buildTime"),
supportedAbis = jsonArrayToStringList(root.getJSONArray("supportedAbis")),
supported32BitAbis = jsonArrayToStringList(root.getJSONArray("supported32BitAbis")),
supported64BitAbis = jsonArrayToStringList(root.getJSONArray("supported64BitAbis")),
androidId = root.getString("androidId"),
gsfId = root.getString("gsfId"),
advertisingId = root.getString("advertisingId"),
wifiMacAddress = root.getString("wifiMacAddress"),
bluetoothMacAddress = root.getString("bluetoothMacAddress"),
ipAddress = root.optString("ipAddress").ifBlank { defaultFallbackIpAddress() },
wifiSsid = root.getString("wifiSsid"),
wifiRssi = root.getInt("wifiRssi"),
localeTag = root.getString("localeTag"),
countryIso = root.getString("countryIso"),
timeZoneId = root.getString("timeZoneId"),
timeZoneDisplayName = root.getString("timeZoneDisplayName"),
networkType = root.getInt("networkType"),
networkOperator = root.getString("networkOperator"),
networkOperatorName = root.getString("networkOperatorName"),
networkCountryIso = root.getString("networkCountryIso"),
simCountryIso = root.getString("simCountryIso"),
simOperator = root.getString("simOperator"),
simOperatorName = root.getString("simOperatorName"),
simState = root.getInt("simState"),
hasIccCard = root.getBoolean("hasIccCard"),
phoneCount = root.getInt("phoneCount"),
isHearingAidCompatibilitySupported = root.getBoolean("isHearingAidCompatibilitySupported"),
isTtySupported = root.getBoolean("isTtySupported"),
isWorldPhone = root.getBoolean("isWorldPhone"),
isNetworkRoaming = root.getBoolean("isNetworkRoaming"),
isSmsCapable = root.getBoolean("isSmsCapable"),
isVoiceCapable = root.getBoolean("isVoiceCapable"),
phoneType = root.getInt("phoneType"),
phoneTypeString = root.getString("phoneTypeString"),
mmsUaProfUrl = root.getString("mmsUaProfUrl"),
mmsUserAgent = root.getString("mmsUserAgent"),
dnsServers = jsonArrayToStringList(root.getJSONArray("dnsServers")),
dnsSearchDomains = root.getString("dnsSearchDomains"),
privateDnsServerName = root.getString("privateDnsServerName"),
privateDnsActive = root.getBoolean("privateDnsActive"),
hasCaptivePortal = root.getBoolean("hasCaptivePortal"),
secureStringSettings = jsonObjectToStringMap(root.getJSONObject("secureStringSettings")),
secureIntSettings = jsonObjectToIntMap(root.getJSONObject("secureIntSettings")),
systemStringSettings = jsonObjectToStringMap(root.getJSONObject("systemStringSettings")),
systemIntSettings = jsonObjectToIntMap(root.getJSONObject("systemIntSettings")),
globalStringSettings = jsonObjectToStringMap(root.getJSONObject("globalStringSettings")),
globalIntSettings = jsonObjectToIntMap(root.getJSONObject("globalIntSettings"))
)
}
private fun jsonArrayToStringList(array: JSONArray): List<String> = buildList {
for (index in 0 until array.length()) {
add(array.getString(index))
}
}
private fun jsonObjectToStringMap(jsonObject: JSONObject): Map<String, String> {
return jsonObject.keys().asSequence().associateWith { jsonObject.getString(it) }
}
private fun jsonObjectToIntMap(jsonObject: JSONObject): Map<String, Int> {
return jsonObject.keys().asSequence().associateWith { jsonObject.getInt(it) }
}
private fun defaultFallbackIpAddress(): String = "23.42.18.101"
}
}
object RandomizedDeviceProfileStore {
private const val prefsName = "purrfectsnap_spoof"
private const val schemaVersion = 5
private const val profileKey = "randomized_device_profile"
private val random = SecureRandom()
fun getOrCreate(context: Context, logger: AbstractLogger, generationToken: String?): RandomizedDeviceProfile {
val prefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
val requestedToken = generationToken.orEmpty()
prefs.getString(profileKey, null)?.let { raw ->
runCatching {
RandomizedDeviceProfile.fromJson(raw)
}.onSuccess { profile ->
val storedToken = prefs.getString("${profileKey}_token", "") ?: ""
if (profile.schemaVersion == schemaVersion && storedToken == requestedToken) {
logger.info("Loaded randomized device profile ${profile.profileId} (${profile.deviceInfo.manufacturer} ${profile.deviceInfo.model})")
return profile
}
}.onFailure {
logger.warn("Failed to parse saved randomized device profile, regenerating: ${it.message}")
}
}
val previousProfile = prefs.getString(profileKey, null)?.let { raw ->
runCatching { RandomizedDeviceProfile.fromJson(raw) }.getOrNull()
}
val profile = generateProfile(previousProfile)
prefs.edit()
.putString(profileKey, profile.toJson().toString())
.putString("${profileKey}_token", requestedToken)
.putString("android_id", profile.androidId)
.putString("advertising_id", profile.advertisingId)
.putString("bluetooth_address", profile.bluetoothMacAddress)
.putString("gsf_id", profile.gsfId)
.putString("random_device", profile.deviceInfo.model)
.putString("device_fingerprint", profile.buildFingerprint)
.apply()
logger.info(
"Generated randomized device profile ${profile.profileId}: " +
"${profile.deviceInfo.manufacturer} ${profile.deviceInfo.model}, " +
"androidId=${profile.androidId}, ip=${profile.ipAddress}, locale=${profile.localeTag}, tz=${profile.timeZoneId}, " +
"carrier=${profile.simOperatorName}"
)
return profile
}
private fun generateProfile(previousProfile: RandomizedDeviceProfile?): RandomizedDeviceProfile {
val eligibleDevices = DeviceSpoofer.getAvailableDevices().filter {
DeviceSpoofer.getDeviceTemplate(it)?.capabilities?.let { capabilities ->
capabilities.isSmsCapable && capabilities.isVoiceCapable && capabilities.isWorldPhone
} == true
}.ifEmpty { DeviceSpoofer.getAvailableDevices() }
val deviceCandidates = eligibleDevices.filterNot {
previousProfile != null &&
DeviceSpoofer.getDeviceTemplate(it)?.deviceInfo?.model == previousProfile.deviceInfo.model
}.ifEmpty { eligibleDevices }
val deviceName = pick(deviceCandidates)
val deviceTemplate = DeviceSpoofer.getDeviceTemplate(deviceName) ?: error("Missing device template for $deviceName")
val regionCandidates = regionProfiles.filterNot {
previousProfile != null &&
it.localeTag == previousProfile.localeTag &&
it.simOperatorName == previousProfile.simOperatorName
}.ifEmpty { regionProfiles }
val region = pick(regionCandidates)
val buildCandidates = deviceTemplate.builds.filter { it.androidRelease in region.androidReleaseOptions }
.ifEmpty { deviceTemplate.builds }
val buildProfile = pick(buildCandidates)
val deviceInfo = deviceTemplate.deviceInfo.copy(
display = buildProfile.display,
host = buildProfile.host,
bootloader = buildProfile.bootloader ?: deviceTemplate.deviceInfo.bootloader
)
val androidRelease = buildProfile.androidRelease
val buildIncremental = buildProfile.incremental
val buildDisplayId = buildProfile.display
val buildFingerprint = DeviceSpoofer.generateFingerprint(deviceInfo, buildProfile)
val buildHost = buildProfile.host
val buildTime = System.currentTimeMillis() - randomLong(45L, 220L) * 24L * 60L * 60L * 1000L
val wifiMac = randomMacAddress()
val bluetoothMac = randomMacAddress()
val ipAddress = region.randomPublicIpAddress()
val locale = Locale.forLanguageTag(region.localeTag)
val timeZone = TimeZone.getTimeZone(region.timeZoneId)
val capabilities = deviceTemplate.capabilities
val secureStringSettings = mapOf(
"accessibility_enabled" to "0",
"speak_password" to "0",
"allowed_geolocation_origins" to "",
"install_non_market_apps" to "0",
"device_provisioned" to "1",
"enabled_notification_listeners" to ""
)
val secureIntSettings = mapOf(
"input_method_selector_visibility" to 0,
"accessibility_display_inversion_enabled" to 0,
"enabled_accessibility_services" to 0,
"skip_first_use_hints" to 0,
"tts_default_synth" to 0
)
val systemStringSettings = mapOf(
"dtmf_tone_type" to "normal",
"mode_ringer_streams_affected" to "166",
"mute_streams_affected" to "46",
"show_password" to "1",
"user_rotation" to "0"
)
val systemIntSettings = mapOf(
"bluetooth_discoverability" to 0,
"bluetooth_discoverability_timeout" to 120,
"date_format" to 0,
"end_button_behavior" to 2
)
val globalStringSettings = mapOf(
"adb_enabled" to "0",
"auto_time" to "1",
"auto_time_zone" to "1",
"development_settings_enabled" to "0",
"stay_on_while_plugged_in" to "0",
"usb_mass_storage_enabled" to "0",
"wifi_networks_available_notification_on" to "0",
"data_roaming" to "1"
)
val globalIntSettings = mapOf(
"always_finish_activities" to 0,
"animator_duration_scale" to 1,
"http_proxy" to 0,
"network_preference" to 1,
"transition_animation_scale" to 1,
"use_google_mail" to 1,
"wait_for_debugger" to 0
)
return RandomizedDeviceProfile(
schemaVersion = schemaVersion,
profileId = UUID.randomUUID().toString().substring(0, 8),
deviceInfo = deviceInfo,
androidRelease = androidRelease,
buildIncremental = buildIncremental,
buildDisplayId = buildDisplayId,
buildFingerprint = buildFingerprint,
buildHost = buildHost,
buildTime = buildTime,
supportedAbis = capabilities.supportedAbis,
supported32BitAbis = capabilities.supported32BitAbis,
supported64BitAbis = capabilities.supported64BitAbis,
androidId = randomHex(16),
gsfId = randomHex(16),
advertisingId = UUID.randomUUID().toString(),
wifiMacAddress = wifiMac,
bluetoothMacAddress = bluetoothMac,
ipAddress = ipAddress,
wifiSsid = region.randomWifiSsid(),
wifiRssi = random.nextInt(-72, -36),
localeTag = locale.toLanguageTag(),
countryIso = region.countryIso,
timeZoneId = region.timeZoneId,
timeZoneDisplayName = timeZone.getDisplayName(false, TimeZone.SHORT, locale),
networkType = 13,
networkOperator = region.networkOperator,
networkOperatorName = region.networkOperatorName,
networkCountryIso = region.countryIso.lowercase(Locale.US),
simCountryIso = region.countryIso.lowercase(Locale.US),
simOperator = region.simOperator,
simOperatorName = region.simOperatorName,
simState = 5,
hasIccCard = true,
phoneCount = capabilities.phoneCount,
isHearingAidCompatibilitySupported = capabilities.isHearingAidCompatibilitySupported,
isTtySupported = capabilities.isTtySupported,
isWorldPhone = capabilities.isWorldPhone,
isNetworkRoaming = false,
isSmsCapable = capabilities.isSmsCapable,
isVoiceCapable = capabilities.isVoiceCapable,
phoneType = capabilities.phoneType,
phoneTypeString = capabilities.phoneTypeString,
mmsUaProfUrl = "",
mmsUserAgent = region.mmsUserAgent(deviceTemplate.marketingName, androidRelease),
dnsServers = region.dnsServers.sortedBy { random.nextInt() }.take(2),
dnsSearchDomains = region.dnsSearchDomains,
privateDnsServerName = region.privateDnsServerName,
privateDnsActive = true,
hasCaptivePortal = false,
secureStringSettings = secureStringSettings,
secureIntSettings = secureIntSettings,
systemStringSettings = systemStringSettings,
systemIntSettings = systemIntSettings,
globalStringSettings = globalStringSettings,
globalIntSettings = globalIntSettings
)
}
private fun randomHex(length: Int): String {
val chars = CharArray(length)
val alphabet = "0123456789abcdef"
for (index in chars.indices) {
chars[index] = alphabet[random.nextInt(alphabet.length)]
}
return String(chars)
}
private fun randomDigits(length: Int): String {
val chars = CharArray(length)
for (index in chars.indices) {
chars[index] = ('0'.code + random.nextInt(10)).toChar()
}
return String(chars)
}
private fun randomMacAddress(): String {
val bytes = ByteArray(6)
random.nextBytes(bytes)
bytes[0] = (bytes[0].toInt() and 0xFE or 0x02).toByte()
return bytes.joinToString(":") { "%02x".format(it.toInt() and 0xFF) }
}
private fun randomPublicIpv4(prefixes: List<Int>? = null): String {
val firstOctet = prefixes?.takeIf { it.isNotEmpty() }?.let { pick(it) } ?: run {
generateSequence { random.nextInt(1, 224) }
.first { candidate ->
candidate != 10 &&
candidate != 127 &&
candidate != 169 &&
candidate != 172 &&
candidate != 192
}
}
val secondOctet = random.nextInt(1, 255)
val thirdOctet = random.nextInt(1, 255)
val fourthOctet = random.nextInt(2, 255)
val candidate = "$firstOctet.$secondOctet.$thirdOctet.$fourthOctet"
return runCatching { InetAddress.getByName(candidate).hostAddress }.getOrDefault(candidate)
}
private fun randomLong(minInclusive: Long, maxExclusive: Long): Long {
require(maxExclusive > minInclusive)
val bound = maxExclusive - minInclusive
var bits: Long
var candidate: Long
do {
bits = random.nextLong() ushr 1
candidate = bits % bound
} while (bits - candidate + (bound - 1) < 0L)
return minInclusive + candidate
}
private fun <T> pick(values: List<T>): T = values[random.nextInt(values.size)]
private data class RegionProfile(
val localeTag: String,
val countryIso: String,
val timeZoneId: String,
val networkOperator: String,
val networkOperatorName: String,
val simOperator: String,
val simOperatorName: String,
val dnsServers: List<String>,
val dnsSearchDomains: String,
val privateDnsServerName: String,
val timeFormat: String,
val androidReleaseOptions: List<String>,
val wifiPrefixes: List<String>,
val ipPrefixes: List<Int>
) {
fun randomWifiSsid(): String = "${pick(wifiPrefixes)}-${randomDigits(4)}"
fun randomPublicIpAddress(): String = randomPublicIpv4(ipPrefixes)
fun mmsUserAgent(model: String, androidRelease: String): String {
return "$model/$androidRelease"
}
}
private val regionProfiles = listOf(
RegionProfile(
localeTag = "en-US",
countryIso = "US",
timeZoneId = "America/New_York",
networkOperator = "310260",
networkOperatorName = "T-Mobile",
simOperator = "310260",
simOperatorName = "T-Mobile",
dnsServers = listOf("8.8.8.8", "8.8.4.4", "1.1.1.1"),
dnsSearchDomains = "hsd1.ny.comcast.net",
privateDnsServerName = "dns.google",
timeFormat = "12",
androidReleaseOptions = listOf("14", "15"),
wifiPrefixes = listOf("TP-Link", "NETGEAR", "XFINITY", "HomeWiFi"),
ipPrefixes = listOf(23, 24, 45, 47, 66, 67, 68, 69, 72, 73, 98, 104, 107, 108, 162, 184, 198, 199)
),
RegionProfile(
localeTag = "en-GB",
countryIso = "GB",
timeZoneId = "Europe/London",
networkOperator = "23430",
networkOperatorName = "EE",
simOperator = "23430",
simOperatorName = "EE",
dnsServers = listOf("1.1.1.1", "1.0.0.1", "8.8.8.8"),
dnsSearchDomains = "bb.sky.com",
privateDnsServerName = "one.one.one.one",
timeFormat = "24",
androidReleaseOptions = listOf("14", "15"),
wifiPrefixes = listOf("Sky", "BT-Hub", "VirginMedia", "Linksys"),
ipPrefixes = listOf(51, 62, 77, 81, 86, 87, 88, 89, 90, 91, 92, 109, 141, 176, 185, 188)
),
RegionProfile(
localeTag = "de-DE",
countryIso = "DE",
timeZoneId = "Europe/Berlin",
networkOperator = "26202",
networkOperatorName = "Vodafone DE",
simOperator = "26202",
simOperatorName = "Vodafone DE",
dnsServers = listOf("9.9.9.9", "149.112.112.112", "1.1.1.1"),
dnsSearchDomains = "fritz.box",
privateDnsServerName = "dns.quad9.net",
timeFormat = "24",
androidReleaseOptions = listOf("14", "15"),
wifiPrefixes = listOf("FRITZBox", "Vodafone", "Telekom", "WLAN"),
ipPrefixes = listOf(2, 5, 31, 37, 46, 79, 80, 84, 85, 87, 91, 93, 95, 109, 134, 176, 178, 188)
),
RegionProfile(
localeTag = "en-IN",
countryIso = "IN",
timeZoneId = "Asia/Kolkata",
networkOperator = "405874",
networkOperatorName = "Jio",
simOperator = "405874",
simOperatorName = "Jio",
dnsServers = listOf("1.1.1.1", "8.8.8.8", "9.9.9.9"),
dnsSearchDomains = "airtelbroadband.in",
privateDnsServerName = "dns.google",
timeFormat = "12",
androidReleaseOptions = listOf("14", "15"),
wifiPrefixes = listOf("JioFiber", "Airtel", "ACTFibernet", "HomeNet"),
ipPrefixes = listOf(14, 27, 42, 49, 59, 61, 101, 103, 106, 117, 122, 125, 157, 182)
)
)
}

View File

@@ -60,6 +60,14 @@ class Messaging : Feature("Messaging") {
currentConversationId()?.let { stealthMode.canUseRule(it) } == true currentConversationId()?.let { stealthMode.canUseRule(it) } == true
} }
private fun shouldSpoofViewingGalleryPresence(stealthMode: StealthMode): Boolean {
return shouldHideBitmojiPresence(stealthMode) || context.config.messaging.spoofViewingGalleryPresence.get()
}
private fun shouldSpoofReplyCameraPresence(stealthMode: StealthMode): Boolean {
return shouldHideBitmojiPresence(stealthMode) || context.config.messaging.spoofReplyCameraPresence.get()
}
private fun shouldHideTyping(stealthMode: StealthMode, hideTypingIndicator: HideTypingIndicator): Boolean { private fun shouldHideTyping(stealthMode: StealthMode, hideTypingIndicator: HideTypingIndicator): Boolean {
return context.config.messaging.hideTypingNotifications.get() || return context.config.messaging.hideTypingNotifications.get() ||
currentConversationId()?.let { stealthMode.canUseRule(it) || hideTypingIndicator.canUseRule(it) } == true currentConversationId()?.let { stealthMode.canUseRule(it) || hideTypingIndicator.canUseRule(it) } == true
@@ -156,6 +164,8 @@ class Messaging : Feature("Messaging") {
classReference.getAsClass()?.let { wrapperClass -> classReference.getAsClass()?.let { wrapperClass ->
val bitmojiMethodNames = mutableSetOf<String>() val bitmojiMethodNames = mutableSetOf<String>()
val viewingGalleryMethodNames = mutableSetOf<String>()
val replyCameraMethodNames = mutableSetOf<String>()
val typingMethodNames = mutableSetOf<String>() val typingMethodNames = mutableSetOf<String>()
val peekingMethodNames = mutableSetOf<String>() val peekingMethodNames = mutableSetOf<String>()
@@ -165,14 +175,24 @@ class Messaging : Feature("Messaging") {
if (parameterTypes.any { parameterType -> if (parameterTypes.any { parameterType ->
listOf( listOf(
"PlatformChatVisibleAction", "PlatformChatVisibleAction",
"PlatformChatHiddenAction", "PlatformChatHiddenAction"
"PlatformViewingChatMediaAction",
"PlatformUsingReplyCameraAction"
).any { parameterType.name.contains(it) } ).any { parameterType.name.contains(it) }
}) { }) {
bitmojiMethodNames.add(method.name) bitmojiMethodNames.add(method.name)
} }
if (parameterTypes.any { parameterType ->
parameterType.name.contains("PlatformViewingChatMediaAction")
}) {
viewingGalleryMethodNames.add(method.name)
}
if (parameterTypes.any { parameterType ->
parameterType.name.contains("PlatformUsingReplyCameraAction")
}) {
replyCameraMethodNames.add(method.name)
}
if (parameterTypes.any { parameterType -> if (parameterTypes.any { parameterType ->
parameterType.name.contains("PlatformTypingAction") parameterType.name.contains("PlatformTypingAction")
}) { }) {
@@ -194,6 +214,22 @@ class Messaging : Feature("Messaging") {
} }
} }
viewingGalleryMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldSpoofViewingGalleryPresence(stealthMode)
}) {
it.setResult(null)
}
}
replyCameraMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldSpoofReplyCameraPresence(stealthMode)
}) {
it.setResult(null)
}
}
typingMethodNames.forEach { methodName -> typingMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, { wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldHideTyping(stealthMode, hideTypingIndicator) shouldHideTyping(stealthMode, hideTypingIndicator)
@@ -214,8 +250,8 @@ class Messaging : Feature("Messaging") {
val instance = param.thisObject<Any>() val instance = param.thisObject<Any>()
clearField(instance, "PlatformChatVisibleAction", shouldHideBitmojiPresence(stealthMode)) clearField(instance, "PlatformChatVisibleAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformChatHiddenAction", shouldHideBitmojiPresence(stealthMode)) clearField(instance, "PlatformChatHiddenAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformViewingChatMediaAction", shouldHideBitmojiPresence(stealthMode)) clearField(instance, "PlatformViewingChatMediaAction", shouldSpoofViewingGalleryPresence(stealthMode))
clearField(instance, "PlatformUsingReplyCameraAction", shouldHideBitmojiPresence(stealthMode)) clearField(instance, "PlatformUsingReplyCameraAction", shouldSpoofReplyCameraPresence(stealthMode))
clearField(instance, "PlatformTypingAction", shouldHideTyping(stealthMode, hideTypingIndicator)) clearField(instance, "PlatformTypingAction", shouldHideTyping(stealthMode, hideTypingIndicator))
clearField(instance, "PlatformStartPeekingAction", shouldHidePeek(stealthMode)) clearField(instance, "PlatformStartPeekingAction", shouldHidePeek(stealthMode))
} }

View File

@@ -25,6 +25,12 @@ import java.text.DateFormat
import java.util.Date import java.util.Date
class FriendTracker : Feature("Friend Tracker") { class FriendTracker : Feature("Friend Tracker") {
companion object {
private const val PRESENCE_PEEKING_BIT = 8
private const val PRESENCE_REPLY_CAMERA_BIT = 9
private const val PRESENCE_CHAT_MEDIA_BIT = 10
}
private val conversationPresenceState = mutableMapOf<String, MutableMap<String, FriendPresenceState?>>() // conversationId -> (userId -> state) private val conversationPresenceState = mutableMapOf<String, MutableMap<String, FriendPresenceState?>>() // conversationId -> (userId -> state)
private val tracker by lazyBridge { context.bridgeClient.getTracker() } private val tracker by lazyBridge { context.bridgeClient.getTracker() }
private val translation by lazy { context.translation.getCategory("friend_tracker_notifications") } private val translation by lazy { context.translation.getCategory("friend_tracker_notifications") }
@@ -37,6 +43,8 @@ class FriendTracker : Feature("Friend Tracker") {
)) ))
} } } }
private val conversationEntries = mutableMapOf<Pair<String, String>, Long>() private val conversationEntries = mutableMapOf<Pair<String, String>, Long>()
private val galleryEntries = mutableMapOf<Pair<String, String>, Long>()
private val replyCameraEntries = mutableMapOf<Pair<String, String>, Long>()
private val peekingStateListeners = mutableListOf<(String, String, Boolean) -> Unit>() private val peekingStateListeners = mutableListOf<(String, String, Boolean) -> Unit>()
fun addOnPeekingStateChangedListener(listener: (conversationId: String, userId: String, peeking: Boolean) -> Unit) { fun addOnPeekingStateChangedListener(listener: (conversationId: String, userId: String, peeking: Boolean) -> Unit) {
@@ -104,7 +112,12 @@ class FriendTracker : Feature("Friend Tracker") {
context.log.verbose("dispatching $action for $eventType in $conversationName") context.log.verbose("dispatching $action for $eventType in $conversationName")
val iCanSeeYouDetails = if (eventType == TrackerEventType.I_CAN_SEE_YOU) buildICanSeeYouDetails(extras) else "" val iCanSeeYouDetails = when (eventType) {
TrackerEventType.I_CAN_SEE_YOU,
TrackerEventType.I_CAN_SEE_YOU_2,
TrackerEventType.I_CAN_SEE_YOU_3 -> buildICanSeeYouDetails(extras)
else -> ""
}
val notificationText = translation[eventType.key] val notificationText = translation[eventType.key]
.replace("{friend}", authorName) .replace("{friend}", authorName)
.replace("{conversation}", conversationName) .replace("{conversation}", conversationName)
@@ -133,7 +146,7 @@ class FriendTracker : Feature("Friend Tracker") {
} }
} }
private fun buildICanSeeYouExtras(entry: Long?, exit: Long?, duration: Long?) = listOf( private fun buildTimedActivityExtras(entry: Long?, exit: Long?, duration: Long?) = listOf(
entry ?: -1, entry ?: -1,
exit ?: -1, exit ?: -1,
duration ?: -1 duration ?: -1
@@ -189,10 +202,22 @@ class FriendTracker : Feature("Friend Tracker") {
(currentState == null || oldState?.bitmojiPresent == false) && oldState?.bitmojiPresent == true -> TrackerEventType.CONVERSATION_EXIT (currentState == null || oldState?.bitmojiPresent == false) && oldState?.bitmojiPresent == true -> TrackerEventType.CONVERSATION_EXIT
oldState?.typing == false && currentState?.typing == true -> if (currentState.speaking) TrackerEventType.STARTED_SPEAKING else TrackerEventType.STARTED_TYPING oldState?.typing == false && currentState?.typing == true -> if (currentState.speaking) TrackerEventType.STARTED_SPEAKING else TrackerEventType.STARTED_TYPING
oldState?.typing == true && (currentState == null || !currentState.typing) -> if (oldState.speaking) TrackerEventType.STOPPED_SPEAKING else TrackerEventType.STOPPED_TYPING oldState?.typing == true && (currentState == null || !currentState.typing) -> if (oldState.speaking) TrackerEventType.STOPPED_SPEAKING else TrackerEventType.STOPPED_TYPING
(oldState == null || !oldState.peeking) && currentState?.peeking == true -> TrackerEventType.STARTED_PEEKING (oldState == null || !oldState.usingReplyCamera) && currentState?.usingReplyCamera == true -> TrackerEventType.STARTED_USING_REPLY_CAMERA
oldState?.peeking == true && (currentState == null || !currentState.peeking) -> TrackerEventType.STOPPED_PEEKING oldState?.usingReplyCamera == true && (currentState == null || !currentState.usingReplyCamera) -> TrackerEventType.STOPPED_USING_REPLY_CAMERA
(oldState == null || !oldState.viewingChatMedia) && currentState?.viewingChatMedia == true -> TrackerEventType.STARTED_VIEWING_CHAT_MEDIA
oldState?.viewingChatMedia == true && (currentState == null || !currentState.viewingChatMedia) -> TrackerEventType.STOPPED_VIEWING_CHAT_MEDIA
(oldState == null || !oldState.peeking) &&
currentState?.peeking == true &&
currentState.usingReplyCamera != true &&
oldState?.usingReplyCamera != true -> TrackerEventType.STARTED_PEEKING
oldState?.peeking == true &&
(currentState == null || !currentState.peeking) &&
currentState?.usingReplyCamera != true &&
oldState.usingReplyCamera != true -> TrackerEventType.STOPPED_PEEKING
else -> null else -> null
} ?: return }
eventType ?: return
when (eventType) { when (eventType) {
TrackerEventType.CONVERSATION_ENTER -> { TrackerEventType.CONVERSATION_ENTER -> {
@@ -206,7 +231,35 @@ class FriendTracker : Feature("Friend Tracker") {
TrackerEventType.I_CAN_SEE_YOU, TrackerEventType.I_CAN_SEE_YOU,
conversationId, conversationId,
userId, userId,
buildICanSeeYouExtras(entry, exit, entry?.let { exit - it }) buildTimedActivityExtras(entry, exit, entry?.let { exit - it })
)
}
TrackerEventType.STARTED_VIEWING_CHAT_MEDIA -> {
galleryEntries[conversationId to userId] = System.currentTimeMillis()
}
TrackerEventType.STOPPED_VIEWING_CHAT_MEDIA -> {
val key = conversationId to userId
val exit = System.currentTimeMillis()
val entry = galleryEntries.remove(key)
dispatchEvents(
TrackerEventType.I_CAN_SEE_YOU_2,
conversationId,
userId,
buildTimedActivityExtras(entry, exit, entry?.let { exit - it })
)
}
TrackerEventType.STARTED_USING_REPLY_CAMERA -> {
replyCameraEntries[conversationId to userId] = System.currentTimeMillis()
}
TrackerEventType.STOPPED_USING_REPLY_CAMERA -> {
val key = conversationId to userId
val exit = System.currentTimeMillis()
val entry = replyCameraEntries.remove(key)
dispatchEvents(
TrackerEventType.I_CAN_SEE_YOU_3,
conversationId,
userId,
buildTimedActivityExtras(entry, exit, entry?.let { exit - it })
) )
} }
else -> {} else -> {}
@@ -266,14 +319,20 @@ class FriendTracker : Feature("Friend Tracker") {
userIds.add(participantUserId) userIds.add(participantUserId)
if (participantUserId == context.database.myUserId) return@eachBuffer if (participantUserId == context.database.myUserId) return@eachBuffer
val stateMap = getVarInt(2, 1)?.toString(2)?.padStart(16, '0')?.reversed()?.map { it == '1' } ?: return@eachBuffer val stateMap = getVarInt(2, 1)?.toString(2)?.padStart(16, '0')?.reversed()?.map { it == '1' } ?: return@eachBuffer
val usingReplyCamera = stateMap.getOrElse(PRESENCE_REPLY_CAMERA_BIT) { false }
val viewingChatMedia = stateMap.getOrElse(PRESENCE_CHAT_MEDIA_BIT) { false }
val peeking = stateMap.getOrElse(PRESENCE_PEEKING_BIT) { false }
presenceMap[participantUserId] = FriendPresenceState( presenceMap[participantUserId] = FriendPresenceState(
bitmojiPresent = stateMap[0], bitmojiPresent = stateMap[0],
typing = stateMap[4], typing = stateMap[4],
wasTyping = stateMap[5], wasTyping = stateMap[5],
speaking = stateMap[6] && stateMap[4], speaking = stateMap[6] && stateMap[4],
// Snapchat appears to have shifted the peeking flag by one bit on newer builds. // Snapchat moved peeking by one bit on newer builds and added
peeking = stateMap.getOrElse(8) { false } || stateMap.getOrElse(9) { false } // dedicated chat-presence flags for reply camera and chat media viewing.
peeking = peeking,
usingReplyCamera = usingReplyCamera,
viewingChatMedia = viewingChatMedia
) )
} }

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a nativeAbis=arm64-v8a
APP_VERSION_NAME=1.6.1 APP_VERSION_NAME=1.6.2
APP_VERSION_CODE=312 APP_VERSION_CODE=314
debug_build_hash=18fe2a814d0e2eb5 debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256= psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c