v1.6.2
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 (?, ?, ?, ?, ?, ?)",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user