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.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.core.app.CoreComponentFactory
@@ -30,12 +32,14 @@ import androidx.work.WorkManager
import me.eternal.purrfectsnap.bridge.BridgeService
import me.eternal.purrfectsnap.common.BuildConfig
import me.eternal.purrfectsnap.common.Constants
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.action.EnumAction
import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerWrapper
import me.eternal.purrfectsnap.common.bridge.wrapper.MappingsWrapper
import me.eternal.purrfectsnap.common.config.ModConfig
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.getPurgeTime
import me.eternal.purrfectsnap.e2ee.E2EEImplementation
@@ -275,6 +279,67 @@ class RemoteSideContext(
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() {
val workManager = WorkManager.getInstance(androidContext)
val constraints = Constraints.Builder()

View File

@@ -93,24 +93,6 @@ fun AppDatabase.replaceMessagingData(
executeAsync {
database.beginTransaction()
try {
val friendIds = friends.map { it.userId }.toSet()
val groupIds = groups.map { it.conversationId }.toSet()
getFriends().forEach { friend ->
if (friend.userId !in friendIds) {
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(friend.userId))
database.execSQL("DELETE FROM streaks WHERE id = ?", arrayOf(friend.userId))
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(friend.userId))
}
}
getGroups().forEach { group ->
if (group.conversationId !in groupIds) {
database.execSQL("DELETE FROM groups WHERE conversationId = ?", arrayOf(group.conversationId))
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(group.conversationId))
}
}
friends.forEach { friend ->
database.execSQL(
"INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)",

View File

@@ -9,6 +9,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.lazy.LazyColumn
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -72,6 +77,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import me.eternal.purrfectsnap.common.ui.TopBarActionButton
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.ManagerTheme
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 org.json.JSONArray
import org.json.JSONObject
import java.util.UUID
import kotlin.math.max
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() {
routes.navController.navigate(routeInfo.id, NavOptions.Builder()
.setPopUpTo(routes.navController.graph.findStartDestination().id, false)
@@ -330,9 +359,17 @@ class FeaturesRootSection : Routes.Route() {
}
@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 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 }
@@ -348,7 +385,64 @@ class FeaturesRootSection : Routes.Route() {
}
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)) {
registerDialogOnClickCallback()
@@ -518,12 +612,12 @@ class FeaturesRootSection : Routes.Route() {
val hapticFeedback = LocalHapticFeedback.current
Switch(
checked = state,
onCheckedChange = {
onCheckedChange = { requestedState ->
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
state = state.not()
propertyValue.setAny(state)
state = requestedState
propertyValue.setAny(requestedState)
persistConfig()
},
colors = purrfectSwitchColors()
@@ -566,6 +660,45 @@ class FeaturesRootSection : Routes.Route() {
}
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 = {
when (dataType) {
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
@@ -660,12 +793,35 @@ class FeaturesRootSection : Routes.Route() {
val hapticFeedback = LocalHapticFeedback.current
Switch(
checked = state,
onCheckedChange = {
onCheckedChange = { requestedState ->
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
state = state.not()
container.globalState = state
if (isRandomizedProfileContainer && requestedState) {
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()
},
colors = purrfectSwitchColors()
@@ -722,7 +878,12 @@ class FeaturesRootSection : Routes.Route() {
}
@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" }
var clickCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
val noticeColorMap = remember {
@@ -736,6 +897,7 @@ class FeaturesRootSection : Routes.Route() {
val versionCheck = remember { property.key.params.versionCheck }
val versionCheckPair = remember(property) { versionCheck?.checkVersion(context.installationSummary.snapchatInfo?.versionCode ?: return@remember null)}
val isComponentDisabled = remember { versionCheckPair != null && versionCheck?.isDisabled == true }
val isInteractionEnabled = !isComponentDisabled
val cardShape = RoundedCornerShape(22.dp)
val interactionSource = remember { MutableInteractionSource() }
@@ -753,8 +915,9 @@ class FeaturesRootSection : Routes.Route() {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 7.dp)
.graphicsLayer { if (isComponentDisabled) alpha = 0.5f }
.graphicsLayer { if (!isInteractionEnabled) alpha = 0.5f }
.clickable(
enabled = isInteractionEnabled,
interactionSource = interactionSource,
indication = null
) {
@@ -852,7 +1015,7 @@ class FeaturesRootSection : Routes.Route() {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
PropertyAction(property, registerClickCallback = { callback ->
PropertyAction(property, onConfigChanged = onConfigChanged, registerClickCallback = { callback ->
if (property.key.propertyTranslationPath().startsWith("rules.properties")) {
clickCallback = {
routes.manageRuleFeature.navigate {
@@ -1406,6 +1569,7 @@ class FeaturesRootSection : Routes.Route() {
) {
val density = LocalDensity.current
var controlsHeight by remember { mutableStateOf(100.dp) }
var configRefreshNonce by rememberSaveable { mutableStateOf(0) }
val listState = rememberLazyListState()
@@ -1479,7 +1643,12 @@ class FeaturesRootSection : Routes.Route() {
upsertHistory(liveSearchQuery, sharedSearchHistory)
}
} else null
PropertyCard(item, onOpen = onOpen)
PropertyCard(
property = item,
configRefreshNonce = configRefreshNonce,
onConfigChanged = { configRefreshNonce++ },
onOpen = onOpen
)
}
}
item { Spacer(modifier = Modifier.height(12.dp)) }
@@ -1634,9 +1803,14 @@ class FeaturesRootSection : Routes.Route() {
onBack: (() -> Unit)? = null,
) {
PropertiesView(
properties = remember {
properties = remember(configContainer.globalState) {
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,

View File

@@ -24,11 +24,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.*
import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.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.util.coil.BitmojiImage
@@ -219,39 +219,70 @@ class AddFriendDialog(
var hasFetchError by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
val updateSnapshot: (List<MessagingFriendInfo>, List<MessagingGroupInfo>) -> Unit = { friends, groups ->
coroutineScope.launch {
cachedFriends = friends.run {
if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.userId) }
} else friends
fun applySnapshot(
friends: List<MessagingFriendInfo>,
groups: List<MessagingGroupInfo>
) {
cachedFriends = friends.run {
if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.userId) }
} else {
this
}
cachedGroups = groups.run {
if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.conversationId) }
} else groups
}
cachedGroups = groups.run {
if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.conversationId) }
} else {
this
}
}
if (friends.isNotEmpty() || groups.isNotEmpty()) {
timeoutJob?.cancel()
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) {
context.bridgeService?.requestEphemeralSocialSnapshot(updateSnapshot)
} else {
context.database.receiveMessagingDataCallback = updateSnapshot
}
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}.also {
runCatching {
context.androidContext.sendBroadcast(it)
}.onFailure {
context.log.error("Failed to send broadcast", it)
hasFetchError = true
context.requestSocialSnapshotRefresh()
coroutineScope.launch(Dispatchers.IO) {
repeat(25) {
delay(1000)
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 {
withContext(Dispatchers.IO) {
delay(20000)
hasFetchError = true
delay(25000)
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.launch
import me.eternal.purrfectsnap.R
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.data.SocialScope
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper
import me.eternal.purrfectsnap.storage.*
import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.ManagerTheme
@@ -63,13 +61,7 @@ class SocialRootSection : Routes.Route() {
}
internal fun requestLatestSnapshot() {
runCatching {
context.androidContext.sendBroadcast(
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}
)
}.onFailure {
context.log.error("Failed to request latest social snapshot", it)
}
context.requestSocialSnapshotRefresh()
}
@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_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_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) }
LaunchedEffect(Unit) {
context.database.receiveMessagingDataCallback = { friends, groups ->
friendList = friends
groupList = groups
}
updateScopeLists()
}
DisposableEffect(Unit) {
onDispose {
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
val filteredFriends = remember(friendList, normalizedQuery) {
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_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_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.View.OnAttachStateChangeListener
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.addCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -47,6 +50,8 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID
@@ -197,6 +202,21 @@ private fun InlineDialog(
val screenWidthDp = with(density) { displayMetrics.widthPixels.toDp() }
val screenHeightDp = with(density) { displayMetrics.heightPixels.toDp() }
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) }
LaunchedEffect(Unit) {
@@ -212,43 +232,45 @@ private fun InlineDialog(
),
onDismissRequest = onDismissRequest
) {
Box(
modifier = Modifier
.width(screenWidthDp)
.height(screenHeightDp)
.then(
if (dismissOnClickOutside) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onDismissRequest
)
} else {
Modifier
}
)
.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)
)
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides backDispatcherOwner) {
Box(
modifier = Modifier
.width(screenWidthDp)
.height(screenHeightDp)
.then(
if (dismissOnClickOutside) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onDismissRequest
)
} else {
Modifier
}
)
.semantics { dialog() },
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Box(
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = {}
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)
)
) {
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:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.1").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("312").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.2").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("314").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
// Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -532,6 +532,18 @@
"title": "Export Sensitive Data?",
"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": {
"title": "Choose content types to process",
"select_all_button": "Select All"
@@ -898,7 +910,8 @@
"notices": {
"unstable": "\u26a0 Unstable",
"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": {
"downloader": {
@@ -1174,6 +1187,14 @@
"name": "Hide Bitmoji Presence",
"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": {
"name": "Hide Typing Notifications",
"description": "Prevents anyone from knowing you're typing a message"
@@ -2013,6 +2034,32 @@
"name": "Force Wi-Fi Transport Flag",
"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": {
"name": "Spoof Device ID",
"description": "Override the Android ID sent to Snapchat",
@@ -2348,7 +2395,10 @@
"custom_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_mention": "By Mention",
"added_by_group_chat": "By Group Chat",
@@ -2965,6 +3015,10 @@
"stopped_speaking": "Stopped Speaking",
"started_peeking": "Started 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_deleted": "Message Deleted",
"message_saved": "Message Saved",
@@ -2977,7 +3031,9 @@
"snap_replayed_twice": "Snap Replayed Twice",
"snap_screenshot": "Snap Screenshot",
"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",
"tracker_actions": {
@@ -3186,6 +3242,10 @@
"stopped_speaking": "{friend} stopped speaking in {conversation}",
"started_peeking": "{friend} started 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_deleted": "{friend} deleted 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_screenshot": "{friend} took a screenshot 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": {
"notification_channel_name": "Friend Mutation Observer",
@@ -3351,6 +3413,10 @@
"stopped_speaking": "Stopped speaking",
"started_peeking": "Started 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_deleted": "Deleted message",
"message_saved": "Saved message",
@@ -3402,6 +3468,10 @@
"stopped_speaking": "stopped speaking",
"started_peeking": "started 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_deleted": "deleted a message",
"message_saved": "saved a message",
@@ -3414,7 +3484,9 @@
"snap_replayed_twice": "replayed a snap twice",
"snap_screenshot": "took a screenshot",
"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_auto_mapper_label": "Disable Auto Mapper",
"disable_bypass_indicator_label": "Disable Bypass Indicator",
"disable_cant_login_button_label": "Disable Can't Login Button",
"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.",

View File

@@ -570,6 +570,19 @@
"title": "Export Sensitive Data?",
"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": {
"title": "Choose content types to process",
"select_all_button": "Select All"
@@ -1238,6 +1251,14 @@
"name": "Hide Bitmoji Presence",
"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": {
"name": "Hide Typing Notifications",
"description": "Prevents anyone from knowing you're typing a message"
@@ -2119,6 +2140,418 @@
"name": "Force Wi-Fi Transport Flag",
"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": {
"name": "Spoof Device ID",
"description": "Override the Android ID sent to Snapchat",
@@ -2471,6 +2904,9 @@
"custom_android_id": {
"null": "Use real Android ID"
},
"persistent_app_language": {
"system_default": "System Default"
},
"add_friend_source_spoof": {
"added_by_username": "By Username",
"added_by_mention": "By Mention",
@@ -3085,6 +3521,7 @@
"positive": "Yes",
"negative": "No",
"cancel": "Cancel",
"copy": "Copy",
"save": "Save",
"open": "Open",
"download": "Download",
@@ -3102,6 +3539,10 @@
"stopped_speaking": "Stopped Speaking",
"started_peeking": "Started 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_deleted": "Message Deleted",
"message_saved": "Message Saved",
@@ -3114,7 +3555,9 @@
"snap_replayed_twice": "Snap Replayed Twice",
"snap_screenshot": "Snap Screenshot",
"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",
"tracker_actions": {
@@ -3350,6 +3793,10 @@
"stopped_speaking": "{friend} stopped speaking in {conversation}",
"started_peeking": "{friend} started 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_deleted": "{friend} deleted 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_screenshot": "{friend} took a screenshot 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": {
"notification_channel_name": "Friend Mutation Observer",
@@ -3522,9 +3971,13 @@
"started_typing": "Started typing",
"stopped_typing": "Stopped typing",
"started_speaking": "Started speaking",
"stopped_speaking": "Stopped speaking",
"stopped_speaking": "Stopped speaking",
"started_peeking": "Started 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_deleted": "Deleted message",
"message_saved": "Saved message",
@@ -3576,6 +4029,10 @@
"stopped_speaking": "stopped speaking",
"started_peeking": "started 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_deleted": "deleted a message",
"message_saved": "saved a message",
@@ -3588,7 +4045,9 @@
"snap_replayed_twice": "replayed a snap twice",
"snap_screenshot": "took a screenshot",
"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_auto_mapper_label": "Disable Auto Mapper",
"disable_bypass_indicator_label": "Disable Bypass Indicator",
"disable_cant_login_button_label": "Disable Can't Login Button",
"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.",

View File

@@ -63,7 +63,7 @@ class Experimental : ConfigContainer() {
}
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 mediaFilePicker = boolean("media_file_picker") { 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 hidePeekAPeek = boolean("hide_peek_a_peek")
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 unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
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
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() {
val spoofAndroidId = boolean("spoof_android_id") { requireRestart() }
val spoofAndroidId = boolean("spoof_android_id") { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
val customAndroidId = string("custom_android_id") {
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' }) }
}
}
@@ -16,8 +199,9 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
val removeVpnTransportFlag = boolean("remove_vpn_transport_flag") { requireRestart() }
val removeMockLocationFlag = boolean("remove_mock_location_flag") { requireRestart() }
val forceWifiTransportFlag = boolean("force_wifi_transport_flag") { requireRestart() }
val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart() }
val spoofDevice = boolean("spoof_device") { requireRestart() }
val randomizeDeviceProfile = container("randomize_device_profile", RandomizedDeviceProfileConfig()) { 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",
"none",
"random",
@@ -38,6 +222,7 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
"realme GT 6"
) {
requireRestart()
addFlags(ConfigFlag.HIDDEN)
customOptionTranslationPath = "features.options.device_model"
}
}

View File

@@ -9,7 +9,9 @@ data class FriendPresenceState(
val typing: Boolean,
val wasTyping: Boolean,
val speaking: Boolean,
val peeking: Boolean
val peeking: Boolean,
val usingReplyCamera: Boolean,
val viewingChatMedia: Boolean
)
open class SessionEvent(
@@ -44,6 +46,8 @@ enum class SessionEventType(
SNAP_SCREENSHOT("snap_screenshot"),
SNAP_SCREEN_RECORD("snap_screen_record"),
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(
@@ -58,6 +62,10 @@ enum class TrackerEventType(
STOPPED_SPEAKING("stopped_speaking"),
STARTED_PEEKING("started_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
MESSAGE_READ("message_read"),
@@ -73,6 +81,8 @@ enum class TrackerEventType(
SNAP_SCREENSHOT("snap_screenshot"),
SNAP_SCREEN_RECORD("snap_screen_record"),
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 = {
var showDialog by remember { mutableStateOf(false) }
var isLoginScreen by remember { mutableStateOf(false) }
val disableHelpButton = context.bridgeClient.getDebugProp("disable_cant_login_button", "false") == "true"
LaunchedEffect(Unit) {
while (true) {
@@ -329,13 +330,13 @@ class SecurityFeatures(
}
}
if (isLoginScreen) {
if (isLoginScreen && !disableHelpButton) {
LoginSignupHelpButton(
onClick = { showDialog = true }
)
}
if (isLoginScreen && showDialog) {
if (isLoginScreen && !disableHelpButton && showDialog) {
LoginSignupHelpDialog(
onDismiss = { showDialog = false }
)
@@ -433,6 +434,9 @@ class SecurityFeatures(
context.features.addActivityCreateListener { activity ->
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 {
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.util.toSerialized
import me.eternal.purrfectsnap.core.ModContext
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@@ -234,10 +235,48 @@ class BridgeClient(
fun passGroupsAndFriends(groups: List<MessagingGroupInfo>, friends: List<MessagingFriendInfo>) =
safeServiceCall {
service.passGroupsAndFriends(
groups.mapNotNull { it.toSerialized() },
friends.mapNotNull { it.toSerialized() }
val serializedGroups = groups.mapNotNull { it.toSerialized() }
val serializedFriends = 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 {

View File

@@ -15,193 +15,358 @@ data class DeviceInfo(
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 {
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(
"Pixel 8 Pro" to DeviceInfo(
manufacturer = "Google",
model = "Pixel 8 Pro",
brand = "google",
device = "husky",
product = "husky",
hardware = "husky",
board = "husky",
bootloader = "husky-1.0-11003666",
display = "UQ1A.231205.015",
host = "abfarm-release-rbe-64-00163"
"Pixel 8 Pro" to DeviceTemplate(
marketingName = "Pixel 8 Pro",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 8 Pro",
brand = "google",
device = "husky",
product = "husky",
hardware = "husky",
board = "husky",
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(
manufacturer = "Google",
model = "Pixel 9 Pro XL",
brand = "google",
device = "komodo",
product = "komodo",
hardware = "komodo",
board = "komodo",
bootloader = "komodo-1.0-12110753",
display = "AP3A.241105.008",
host = "abfarm-release-rbe-64-00163"
"Pixel 9 Pro XL" to DeviceTemplate(
marketingName = "Pixel 9 Pro XL",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 9 Pro XL",
brand = "google",
device = "komodo",
product = "komodo",
hardware = "komodo",
board = "komodo",
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(
manufacturer = "Google",
model = "Pixel 10",
brand = "google",
device = "frankel",
product = "frankel",
hardware = "tensor_g5",
board = "frankel",
bootloader = "frankel-1.0-12345678",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00200"
"Pixel 10" to DeviceTemplate(
marketingName = "Pixel 10",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 10",
brand = "google",
device = "frankel",
product = "frankel",
hardware = "tensor_g5",
board = "frankel",
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(
manufacturer = "Google",
model = "Pixel 10 Pro",
brand = "google",
device = "blazer",
product = "blazer",
hardware = "tensor_g5",
board = "blazer",
bootloader = "blazer-1.0-12345679",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00201"
"Pixel 10 Pro" to DeviceTemplate(
marketingName = "Pixel 10 Pro",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 10 Pro",
brand = "google",
device = "blazer",
product = "blazer",
hardware = "tensor_g5",
board = "blazer",
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(
manufacturer = "Google",
model = "Pixel 10 Pro XL",
brand = "google",
device = "mustang",
product = "mustang",
hardware = "tensor_g5",
board = "mustang",
bootloader = "mustang-1.0-12345680",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00202"
"Pixel 10 Pro XL" to DeviceTemplate(
marketingName = "Pixel 10 Pro XL",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 10 Pro XL",
brand = "google",
device = "mustang",
product = "mustang",
hardware = "tensor_g5",
board = "mustang",
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(
manufacturer = "Google",
model = "Pixel 10 Pro Fold",
brand = "google",
device = "rango",
product = "rango",
hardware = "tensor_g5",
board = "rango",
bootloader = "rango-1.0-12345681",
display = "BP1A.250105.002",
host = "abfarm-release-rbe-65-00203"
"Pixel 10 Pro Fold" to DeviceTemplate(
marketingName = "Pixel 10 Pro Fold",
deviceInfo = DeviceInfo(
manufacturer = "Google",
model = "Pixel 10 Pro Fold",
brand = "google",
device = "rango",
product = "rango",
hardware = "tensor_g5",
board = "rango",
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(
manufacturer = "Samsung",
model = "SM-S918B",
brand = "samsung",
device = "dm3q",
product = "dm3qxx",
hardware = "qcom",
board = "kalama",
bootloader = "S918BXXU3BWJM",
display = "UP1A.231005.007.S918BXXU3BWJM",
host = "21DH7R2P"
"Galaxy S23 Ultra" to DeviceTemplate(
marketingName = "Galaxy S23 Ultra",
deviceInfo = DeviceInfo(
manufacturer = "Samsung",
model = "SM-S918B",
brand = "samsung",
device = "dm3q",
product = "dm3qxx",
hardware = "qcom",
board = "kalama",
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(
manufacturer = "Samsung",
model = "SM-S928B",
brand = "samsung",
device = "e9q",
product = "e9qxx",
hardware = "qcom",
board = "pineapple",
bootloader = "S928BXXU1AXB5",
display = "UP1A.231005.007.S928BXXU1AXB5",
host = "21DH7R2P"
"Galaxy S24 Ultra" to DeviceTemplate(
marketingName = "Galaxy S24 Ultra",
deviceInfo = DeviceInfo(
manufacturer = "Samsung",
model = "SM-S928B",
brand = "samsung",
device = "e9q",
product = "e9qxx",
hardware = "qcom",
board = "pineapple",
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(
manufacturer = "Samsung",
model = "SM-S938B",
brand = "samsung",
device = "e3q",
product = "e3qxx",
hardware = "qcom",
board = "s5e9945",
bootloader = "S938BXXU1AXL2",
display = "UP1A.231005.007.S938BXXU1AXL2",
host = "21DH7R2P"
"Galaxy S25 Ultra" to DeviceTemplate(
marketingName = "Galaxy S25 Ultra",
deviceInfo = DeviceInfo(
manufacturer = "Samsung",
model = "SM-S938B",
brand = "samsung",
device = "e3q",
product = "e3qxx",
hardware = "qcom",
board = "s5e9945",
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(
manufacturer = "OnePlus",
model = "CPH2651",
brand = "OnePlus",
device = "OP5929L1",
product = "OP5929L1_EEA",
hardware = "qcom",
board = "taro",
bootloader = "unknown",
display = "CPH2651_15.0.0.503(EX01)",
host = "ubuntu-build"
"OnePlus 15" to DeviceTemplate(
marketingName = "OnePlus 15",
deviceInfo = DeviceInfo(
manufacturer = "OnePlus",
model = "CPH2651",
brand = "OnePlus",
device = "OP5929L1",
product = "OP5929L1_EEA",
hardware = "qcom",
board = "taro",
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(
manufacturer = "OnePlus",
model = "CPH2551",
brand = "OnePlus",
device = "OP594DL1",
product = "OP594DL1_EEA",
hardware = "qcom",
board = "taro",
bootloader = "unknown",
display = "CPH2551_14.0.0.600(EX01)",
host = "ubuntu-build"
"OnePlus Open" to DeviceTemplate(
marketingName = "OnePlus Open",
deviceInfo = DeviceInfo(
manufacturer = "OnePlus",
model = "CPH2551",
brand = "OnePlus",
device = "OP594DL1",
product = "OP594DL1_EEA",
hardware = "qcom",
board = "taro",
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(
manufacturer = "Xiaomi",
model = "25010PN30G",
brand = "Xiaomi",
device = "xuanyuan",
product = "xuanyuan_global",
hardware = "qcom",
board = "taro",
bootloader = "unknown",
display = "VK.15.0.3.0.VNGMIXM",
host = "c3-miui-ota-bd164.bj"
"Xiaomi 15 Ultra" to DeviceTemplate(
marketingName = "Xiaomi 15 Ultra",
deviceInfo = DeviceInfo(
manufacturer = "Xiaomi",
model = "25010PN30G",
brand = "Xiaomi",
device = "xuanyuan",
product = "xuanyuan_global",
hardware = "qcom",
board = "taro",
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(
manufacturer = "OPPO",
model = "PHY110",
brand = "OPPO",
device = "OP595DL1",
product = "OP595DL1_EEA",
hardware = "mt6989",
board = "k6989v1_64",
bootloader = "unknown",
display = "PHY110_15.0.0.100(EX01)",
host = "ubuntu-build-server"
"OPPO Find X9 Pro" to DeviceTemplate(
marketingName = "OPPO Find X9 Pro",
deviceInfo = DeviceInfo(
manufacturer = "OPPO",
model = "PHY110",
brand = "OPPO",
device = "OP595DL1",
product = "OP595DL1_EEA",
hardware = "mt6989",
board = "k6989v1_64",
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(
manufacturer = "vivo",
model = "V2309A",
brand = "vivo",
device = "V2309A",
product = "PD2309",
hardware = "mt6989",
board = "k6989v1_64",
bootloader = "unknown",
display = "OP557L.PD2309.14.0.0.100",
host = "compiler-server"
"vivo X100 Pro" to DeviceTemplate(
marketingName = "vivo X100 Pro",
deviceInfo = DeviceInfo(
manufacturer = "vivo",
model = "V2309A",
brand = "vivo",
device = "V2309A",
product = "PD2309",
hardware = "mt6989",
board = "k6989v1_64",
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(
manufacturer = "realme",
model = "RMX3851",
brand = "realme",
device = "RMX3851",
product = "RMX3851_11_A.13",
hardware = "qcom",
board = "taro",
bootloader = "unknown",
display = "RMX3851_14.0.0.700(EX01)",
host = "ubuntu-server"
"realme GT 6" to DeviceTemplate(
marketingName = "realme GT 6",
deviceInfo = DeviceInfo(
manufacturer = "realme",
model = "RMX3851",
brand = "realme",
device = "RMX3851",
product = "RMX3851_11_A.13",
hardware = "qcom",
board = "taro",
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 getDeviceInfo(modelName: String): DeviceInfo? {
return devices[modelName]?.deviceInfo
}
fun getDeviceTemplate(modelName: String): DeviceTemplate? {
return devices[modelName]
}
@@ -211,6 +376,10 @@ object DeviceSpoofer {
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 {
val random = SecureRandom()
val bytes = ByteArray(8)

View File

@@ -77,7 +77,6 @@ class MediaFilePicker : Feature("Media File Picker") {
private var bypassSplitOnce = false
private var sendSingleItemHandler: ((Any) -> Boolean)? = null
private var cleanupItemHandler: ((String) -> Unit)? = null
fun hasQueuedSplitItems(): Boolean = queuedSplitItems.isNotEmpty()
fun hasPendingSplitCleanup(): Boolean = queuedSplitItemIds.isNotEmpty()
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
}
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 {
return context.config.messaging.hideTypingNotifications.get() ||
currentConversationId()?.let { stealthMode.canUseRule(it) || hideTypingIndicator.canUseRule(it) } == true
@@ -156,6 +164,8 @@ class Messaging : Feature("Messaging") {
classReference.getAsClass()?.let { wrapperClass ->
val bitmojiMethodNames = mutableSetOf<String>()
val viewingGalleryMethodNames = mutableSetOf<String>()
val replyCameraMethodNames = mutableSetOf<String>()
val typingMethodNames = mutableSetOf<String>()
val peekingMethodNames = mutableSetOf<String>()
@@ -165,14 +175,24 @@ class Messaging : Feature("Messaging") {
if (parameterTypes.any { parameterType ->
listOf(
"PlatformChatVisibleAction",
"PlatformChatHiddenAction",
"PlatformViewingChatMediaAction",
"PlatformUsingReplyCameraAction"
"PlatformChatHiddenAction"
).any { parameterType.name.contains(it) }
}) {
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 ->
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 ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldHideTyping(stealthMode, hideTypingIndicator)
@@ -214,8 +250,8 @@ class Messaging : Feature("Messaging") {
val instance = param.thisObject<Any>()
clearField(instance, "PlatformChatVisibleAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformChatHiddenAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformViewingChatMediaAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformUsingReplyCameraAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformViewingChatMediaAction", shouldSpoofViewingGalleryPresence(stealthMode))
clearField(instance, "PlatformUsingReplyCameraAction", shouldSpoofReplyCameraPresence(stealthMode))
clearField(instance, "PlatformTypingAction", shouldHideTyping(stealthMode, hideTypingIndicator))
clearField(instance, "PlatformStartPeekingAction", shouldHidePeek(stealthMode))
}

View File

@@ -25,6 +25,12 @@ import java.text.DateFormat
import java.util.Date
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 tracker by lazyBridge { context.bridgeClient.getTracker() }
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 galleryEntries = mutableMapOf<Pair<String, String>, Long>()
private val replyCameraEntries = mutableMapOf<Pair<String, String>, Long>()
private val peekingStateListeners = mutableListOf<(String, String, 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")
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]
.replace("{friend}", authorName)
.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,
exit ?: -1,
duration ?: -1
@@ -189,10 +202,22 @@ class FriendTracker : Feature("Friend Tracker") {
(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 == 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?.peeking == true && (currentState == null || !currentState.peeking) -> TrackerEventType.STOPPED_PEEKING
(oldState == null || !oldState.usingReplyCamera) && currentState?.usingReplyCamera == true -> TrackerEventType.STARTED_USING_REPLY_CAMERA
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
} ?: return
}
eventType ?: return
when (eventType) {
TrackerEventType.CONVERSATION_ENTER -> {
@@ -206,7 +231,35 @@ class FriendTracker : Feature("Friend Tracker") {
TrackerEventType.I_CAN_SEE_YOU,
conversationId,
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 -> {}
@@ -266,14 +319,20 @@ class FriendTracker : Feature("Friend Tracker") {
userIds.add(participantUserId)
if (participantUserId == context.database.myUserId) 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(
bitmojiPresent = stateMap[0],
typing = stateMap[4],
wasTyping = stateMap[5],
speaking = stateMap[6] && stateMap[4],
// Snapchat appears to have shifted the peeking flag by one bit on newer builds.
peeking = stateMap.getOrElse(8) { false } || stateMap.getOrElse(9) { false }
// Snapchat moved peeking by one bit on newer builds and added
// 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
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.6.1
APP_VERSION_CODE=312
APP_VERSION_NAME=1.6.2
APP_VERSION_CODE=314
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c