Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01b6c2f9a | ||
|
|
98f8d6ebea | ||
|
|
015226cb40 | ||
|
|
ad06b0dffb | ||
|
|
4b4d64e8eb | ||
|
|
3eec22c615 | ||
|
|
9a2e6065dc | ||
|
|
8e00c29a59 | ||
|
|
9ec9517ec5 | ||
|
|
e822fc20b4 | ||
|
|
f3c794fc47 | ||
|
|
6e6d311c23 | ||
|
|
a82db6a863 | ||
|
|
9781008b2e | ||
|
|
c71e06095d | ||
|
|
1fdaf6d55d | ||
|
|
8a9258b318 |
@@ -1 +1 @@
|
||||
- Test
|
||||
- All users are recommended to use the new Performance Mode feature! Go to the features tab and then select global and select performance mode and set it to Max. Then force stop and reopen Snapchat and you will feel the difference i.e. Snapchat will feel a lot faster.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -504,7 +504,10 @@ class DownloadProcessor (
|
||||
for (i in 0 until baseUrlNodeList.length) {
|
||||
val baseUrlNode = baseUrlNodeList.item(i)
|
||||
val baseUrl = baseUrlNode.textContent
|
||||
baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl"
|
||||
// FIX: Only add prefix if it's not already a full URL
|
||||
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
||||
baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl"
|
||||
}
|
||||
}
|
||||
|
||||
val dashOptions = downloadRequest.dashOptions!!
|
||||
|
||||
@@ -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 (?, ?, ?, ?, ?, ?)",
|
||||
|
||||
@@ -86,7 +86,7 @@ class AnnouncementCheckWorker(
|
||||
val pendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val builder = NotificationCompat.Builder(appContext, channelId)
|
||||
.setSmallIcon(R.drawable.launcher_icon_monochrome)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
|
||||
@@ -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,8 @@ 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.core.features.impl.experiments.RandomizedDeviceProfile
|
||||
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 +87,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 +170,93 @@ 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" ||
|
||||
propertyName == "backup_profile_action" ||
|
||||
propertyName == "restore_profile_action"
|
||||
}
|
||||
|
||||
internal fun backupRandomizedProfile(onConfigChanged: () -> Unit) {
|
||||
val profileSnapshot = getRandomizedProfileSnapshot()
|
||||
if (profileSnapshot.startsWith("No generated profile")) {
|
||||
context.shortToast(
|
||||
context.translation["manager.dialogs.randomize_device_profile.empty"]
|
||||
?: "No generated profile is available yet. Enable the feature in Snapchat first."
|
||||
)
|
||||
return
|
||||
}
|
||||
activityLauncher {
|
||||
saveFile("randomized-device-profile.json", "application/json") { uri ->
|
||||
runCatching {
|
||||
context.androidContext.contentResolver.openOutputStream(uri.toUri())?.bufferedWriter()?.use {
|
||||
it.write(profileSnapshot)
|
||||
} ?: error("Failed to open backup destination")
|
||||
onConfigChanged()
|
||||
context.shortToast("Randomized profile backup saved")
|
||||
}.onFailure {
|
||||
context.log.error("Failed to back up randomized profile", it)
|
||||
context.shortToast("Failed to back up randomized profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun restoreRandomizedProfile(onConfigChanged: () -> Unit) {
|
||||
activityLauncher {
|
||||
openFile("application/json") { uri ->
|
||||
runCatching {
|
||||
val importedJson = context.androidContext.contentResolver.openInputStream(uri.toUri())
|
||||
?.bufferedReader()
|
||||
?.use { it.readText() }
|
||||
?.trim()
|
||||
?: error("Failed to read randomized profile backup")
|
||||
val profile = RandomizedDeviceProfile.fromJson(importedJson)
|
||||
val generationToken = UUID.randomUUID().toString()
|
||||
context.androidContext.getSharedPreferences("purrfectsnap_spoof", 0)
|
||||
.edit()
|
||||
.putString("randomized_device_profile", profile.toJson().toString())
|
||||
.putString("randomized_device_profile_token", generationToken)
|
||||
.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()
|
||||
|
||||
val randomizeConfig = context.config.root.experimental.spoof.randomizeDeviceProfile
|
||||
randomizeConfig.profileGenerationToken.set(generationToken)
|
||||
randomizeConfig.currentProfileSnapshot.set(profile.toJson().toString(2))
|
||||
context.config.writeConfig()
|
||||
onConfigChanged()
|
||||
context.shortToast("Randomized profile restored. Restart Snapchat to apply it.")
|
||||
}.onFailure {
|
||||
context.log.error("Failed to restore randomized profile", it)
|
||||
context.shortToast("Failed to restore randomized profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToMainRoot() {
|
||||
routes.navController.navigate(routeInfo.id, NavOptions.Builder()
|
||||
.setPopUpTo(routes.navController.graph.findStartDestination().id, false)
|
||||
@@ -330,9 +425,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 +451,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 +678,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 +726,51 @@ 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"
|
||||
"backup_profile_action" -> context.translation[property.key.propertyName()] ?: "Backup Profile"
|
||||
"restore_profile_action" -> context.translation[property.key.propertyName()] ?: "Restore 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 if (property.name == "view_current_profile_action") {
|
||||
showCurrentRandomProfileDialog = true
|
||||
} else if (property.name == "backup_profile_action") {
|
||||
backupRandomizedProfile(onConfigChanged)
|
||||
} else if (property.name == "restore_profile_action") {
|
||||
restoreRandomizedProfile(onConfigChanged)
|
||||
}
|
||||
},
|
||||
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 -> {
|
||||
@@ -573,8 +778,12 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
||||
val isMessageListProperty = property.key.name.endsWith("_messages")
|
||||
val isSleepWindowProperty = property.key.name.contains("sleep_window")
|
||||
|
||||
if (isMessageListProperty) {
|
||||
alertDialogs.MessageListPropertyDialog(property) { showDialog = false }
|
||||
} else if (isSleepWindowProperty) {
|
||||
alertDialogs.AutoOpenScheduleDialog(property as PropertyPair<String>) { showDialog = false }
|
||||
} else {
|
||||
alertDialogs.KeyboardInputDialog(property) { showDialog = false }
|
||||
}
|
||||
@@ -660,12 +869,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 +954,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 +973,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 +991,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 +1091,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 +1645,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 +1719,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 +1879,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,6 +11,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -27,6 +28,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -80,6 +82,146 @@ import me.eternal.purrfectsnap.ui.util.Dialog as StandardDialog
|
||||
class AlertDialogs(
|
||||
private val translation: LocaleWrapper,
|
||||
){
|
||||
@Composable
|
||||
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
|
||||
val currentValue = property.value.getNullable()?.toString() ?: "[]"
|
||||
val propertyName = translation[property.key.propertyName()]
|
||||
|
||||
MessageListManagerDialog(
|
||||
title = propertyName ?: "",
|
||||
messageListJson = currentValue,
|
||||
onSave = { newValue: String ->
|
||||
property.value.setAny(newValue)
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AutoOpenScheduleDialog(
|
||||
property: PropertyPair<String>,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val windowParts = (property.value.get() as String).split("-")
|
||||
val startTime = windowParts.getOrNull(0)?.split(":") ?: listOf("23", "00")
|
||||
val endTime = windowParts.getOrNull(1)?.split(":") ?: listOf("07", "00")
|
||||
|
||||
var isEditingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val startState = rememberTimePickerState(
|
||||
initialHour = startTime.getOrNull(0)?.toIntOrNull() ?: 23,
|
||||
initialMinute = startTime.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
is24Hour = true
|
||||
)
|
||||
val endState = rememberTimePickerState(
|
||||
initialHour = endTime.getOrNull(0)?.toIntOrNull() ?: 7,
|
||||
initialMinute = endTime.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
is24Hour = true
|
||||
)
|
||||
|
||||
DefaultDialogCard {
|
||||
Column(
|
||||
modifier = Modifier.padding(18.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = translation["auto_open_snaps.auto_open_schedule.title"] ?: "Auto Open Scheduler",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.White.copy(alpha = 0.05f))
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
val activeColor = PurrfectPalette.glowPrimary.copy(alpha = 0.25f)
|
||||
val inactiveColor = Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (!isEditingEnd) activeColor else inactiveColor)
|
||||
.clickable { isEditingEnd = false }
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${translation["auto_open_snaps.auto_open_schedule.start"] ?: "Start"}: ${String.format("%02d:%02d", startState.hour, startState.minute)}",
|
||||
color = if (!isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontWeight = if (!isEditingEnd) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (isEditingEnd) activeColor else inactiveColor)
|
||||
.clickable { isEditingEnd = true }
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${translation["auto_open_snaps.auto_open_schedule.end"] ?: "End"}: ${String.format("%02d:%02d", endState.hour, endState.minute)}",
|
||||
color = if (isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontWeight = if (isEditingEnd) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TimePicker(
|
||||
state = if (isEditingEnd) endState else startState,
|
||||
colors = TimePickerDefaults.colors(
|
||||
clockDialColor = Color.White.copy(alpha = 0.05f),
|
||||
clockDialSelectedContentColor = Color.White,
|
||||
clockDialUnselectedContentColor = Color.White.copy(alpha = 0.7f),
|
||||
selectorColor = PurrfectPalette.glowPrimary,
|
||||
periodSelectorBorderColor = PurrfectPalette.glowPrimary,
|
||||
periodSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||
periodSelectorUnselectedContainerColor = Color.Transparent,
|
||||
periodSelectorSelectedContentColor = Color.White,
|
||||
periodSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f),
|
||||
timeSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||
timeSelectorUnselectedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
timeSelectorSelectedContentColor = Color.White,
|
||||
timeSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = translation["button.negative"], color = Color.White)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val startStr = String.format("%02d:%02d", startState.hour, startState.minute)
|
||||
val endStr = String.format("%02d:%02d", endState.hour, endState.minute)
|
||||
property.value.setAny("$startStr-$endStr")
|
||||
onDismiss()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(text = translation["button.positive"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -1260,23 +1402,7 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
|
||||
val currentValue = property.value.getNullable()?.toString() ?: "[]"
|
||||
val propertyName = translation[property.key.propertyName()]
|
||||
|
||||
MessageListManagerDialog(
|
||||
title = propertyName,
|
||||
messageListJson = currentValue,
|
||||
onSave = { newValue ->
|
||||
property.value.setAny(newValue)
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageListManagerDialog(
|
||||
@@ -1313,7 +1439,6 @@ class AlertDialogs(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Message list
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1329,7 +1454,7 @@ class AlertDialogs(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = translation["auto_reply_messages.dialog.no_messages"],
|
||||
text = translation["bulk_messaging_action.no_messages_found"] ?: "No messages",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -1371,24 +1496,14 @@ class AlertDialogs(
|
||||
showAddDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Edit",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Icon(Icons.Default.Edit, contentDescription = translation["common.edit"] ?: "Edit", tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
messageList = messageList.toMutableList().apply {
|
||||
removeAt(index)
|
||||
}
|
||||
messageList = messageList.toMutableList().apply { removeAt(index) }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Icon(Icons.Default.Delete, contentDescription = translation["common.delete"] ?: "Delete", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1398,7 +1513,6 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
|
||||
// Add button
|
||||
Button(
|
||||
onClick = {
|
||||
editingIndex = -1
|
||||
@@ -1408,20 +1522,13 @@ class AlertDialogs(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = translation["auto_reply_messages.dialog.add_message"])
|
||||
Text(text = translation["common.add"] ?: "Add Message")
|
||||
}
|
||||
|
||||
// Dialog buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1430,17 +1537,14 @@ class AlertDialogs(
|
||||
) {
|
||||
Button(
|
||||
onClick = { onDismiss() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Text(text = translation["button.cancel"])
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val gson = com.google.gson.Gson()
|
||||
val jsonString = gson.toJson(messageList)
|
||||
onSave(jsonString)
|
||||
onSave(gson.toJson(messageList))
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
@@ -1450,7 +1554,6 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
|
||||
// Add/Edit message dialog
|
||||
if (showAddDialog) {
|
||||
StandardDialog(
|
||||
onDismissRequest = { showAddDialog = false },
|
||||
@@ -1460,7 +1563,7 @@ class AlertDialogs(
|
||||
) {
|
||||
DefaultDialogCard {
|
||||
Text(
|
||||
text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["auto_reply_messages.dialog.edit_message"],
|
||||
text = if (editingIndex == -1) translation["common.add"] ?: "Add Message" else translation["common.edit"] ?: "Edit Message",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
@@ -1472,13 +1575,13 @@ class AlertDialogs(
|
||||
TextField(
|
||||
value = editingText,
|
||||
onValueChange = { editingText = it },
|
||||
label = { Text(translation["auto_reply_messages.dialog.message_label"]) },
|
||||
label = { Text(translation["common.message"] ?: "Message") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
minLines = 2,
|
||||
maxLines = 4,
|
||||
placeholder = { Text(translation["auto_reply_messages.dialog.message_placeholder"]) }
|
||||
placeholder = { Text(translation["common.type_message"] ?: "Type message...") }
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -1489,32 +1592,22 @@ class AlertDialogs(
|
||||
) {
|
||||
Button(
|
||||
onClick = { showAddDialog = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Text(text = translation["button.cancel"])
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (editingText.isNotBlank()) {
|
||||
if (editingIndex == -1) {
|
||||
// Add new message
|
||||
messageList = messageList.toMutableList().apply {
|
||||
add(editingText)
|
||||
}
|
||||
} else {
|
||||
// Edit existing message
|
||||
messageList = messageList.toMutableList().apply {
|
||||
set(editingIndex, editingText)
|
||||
}
|
||||
messageList = messageList.toMutableList().apply {
|
||||
if (editingIndex == -1) add(editingText) else set(editingIndex, editingText)
|
||||
}
|
||||
}
|
||||
showAddDialog = false
|
||||
},
|
||||
enabled = editingText.isNotBlank()
|
||||
) {
|
||||
Text(text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["button.save"])
|
||||
Text(text = if (editingIndex == -1) translation["common.add"] ?: "Add" else translation["button.save"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1522,3 +1615,4 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.6").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("322").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.
|
||||
|
||||
@@ -1,3 +1,134 @@
|
||||
## v1.6.6
|
||||
- Fix: Persistent auto open snap disabled notification(tq to Kaladin)
|
||||
- Fix: Crash issues for some devices
|
||||
- Fix: upload quality for snaps sent through send override
|
||||
- Fix: Refactored the notification card for Auto Open to remove the looping progress bar while the engine is in Monitoring stage(tq to Kaladin)
|
||||
- New: Implemented a "Pre Fetch snaps" toggle, when enabled this will pre-fetch the snaps in the auto open queue and load them for the engine to open them efficiently and thus reducing the "Failed to Open..." status(tq to Kaladin)
|
||||
- Fix: The Auto Open engine now only fires the status update if the status text, processed count, or queue structural state actually changes. This reduces system wake-ups during idle periods which helps in battery drain management(tq to Kaladin)
|
||||
- Fix: Disk Optimization: Previous versions wrote the auto open queue to the disk every 25 snaps, this update modified this to a 3-minute windowed save. This reduces Disk I/O by ~75%, keeping the device from overheating excessively during Auto Open processes(tq to Kaladin)
|
||||
|
||||
## v1.6.5
|
||||
- New: Performance mode will now be set to max by default!
|
||||
- Fix: Several optimizations to the performance mode feature which will make your snapchat experience more smooth!
|
||||
- Fix: Crash issues for some devices after enabling spoof
|
||||
- Fix: Notification Icon for the Announcements(tq to Kaladin)
|
||||
- Fix: Auto Open Engine not processing, stuck on monitor/retry/failed loop causing overheating of the devices(tq to Kaladin)
|
||||
- Fix: Refactor the Notification card for the Auto Open to remove dynamic progress bar to add the native progress bar(tq to Kaladin)
|
||||
- Fix: Auto Open Statistics have been refactored to now show the dynamic queue status(tq to Kaladin)
|
||||
- New: Auto Open Thermal Protection/Throttle toggle, when turned on the processing will be doubled down to reduce the temps of the device to maintain a study temp of 40 C and below(tq to Kaladin)
|
||||
|
||||
## v1.6.4
|
||||
- New: Performance mode feature!(Smooth & Max)
|
||||
- Fix: Failed to init feature Device Spoofer for some devices
|
||||
- Fix: Increase the default queue of auto open snaps
|
||||
- Fix: user_conversation error spam
|
||||
|
||||
## v1.6.3
|
||||
- Fix: Blank Global Settings page
|
||||
- New: Backup & Restore option for Randomized Device Profile feature!
|
||||
|
||||
## v1.6.2
|
||||
- New: Randomized Device Profile Feature!
|
||||
- Show Activation Overlay
|
||||
- Build Properties
|
||||
- Device Identity
|
||||
- Manufacturer + Model
|
||||
- Brand + Product
|
||||
- Hardware + Board
|
||||
- ABI Lists
|
||||
- Combined ABIs
|
||||
- Split ABIs
|
||||
- System Properties
|
||||
- Build
|
||||
- Locale
|
||||
- Telephony
|
||||
- Build Version
|
||||
- Fingerprint
|
||||
- Display
|
||||
- Host
|
||||
- Bootloader
|
||||
- Build Time
|
||||
- Locale Options
|
||||
- Locale
|
||||
- Language
|
||||
- Region
|
||||
- Time
|
||||
- Time Zone ID
|
||||
- Time Zone Display Name
|
||||
- Auto Time
|
||||
- Auto Time Zone
|
||||
- Telephony Options
|
||||
- MMS
|
||||
- User Agent
|
||||
- Network Identity
|
||||
- Network Type
|
||||
- Operator Numeric
|
||||
- Operator Name
|
||||
- Country ISO
|
||||
- SIM Identity
|
||||
- Country ISO
|
||||
- Operator Numeric
|
||||
- Operator Name
|
||||
- SIM State
|
||||
- Has ICC Card
|
||||
- Phone Capabilities
|
||||
- Phone Count
|
||||
- Hearing Aid Support
|
||||
- TTY Support
|
||||
- World Phone
|
||||
- Roaming
|
||||
- SMS + Voice Capability
|
||||
- Phone Type
|
||||
- Settings Options
|
||||
- Secure Settings
|
||||
- Base
|
||||
- TTS
|
||||
- System Settings
|
||||
- Base
|
||||
- Bluetooth
|
||||
- Global Settings
|
||||
- Base
|
||||
- Network Options
|
||||
- Wi-Fi
|
||||
- SSID
|
||||
- RSSI
|
||||
- DNS
|
||||
- Servers
|
||||
- Search Domains
|
||||
- Private DNS
|
||||
- Captive Portal
|
||||
- Capability
|
||||
- Identifier Options
|
||||
- Android ID
|
||||
- String Value
|
||||
- Long Value
|
||||
- Advertising ID
|
||||
- Settings Value
|
||||
- Play Services
|
||||
- Hardware Addresses
|
||||
- Wi-Fi MAC
|
||||
- Bluetooth MAC
|
||||
- Persistent App Language
|
||||
- Randomize IP Address
|
||||
- Generate Fresh Profile
|
||||
- View Current Profile
|
||||
- New: Spoof Viewing Gallery Presence
|
||||
- New: Spoof Reply Camera Presence
|
||||
- New: I Can See you 2 Friend Tracker Rule
|
||||
- New: I Can See you 3 Friend Tracker Rule
|
||||
- New: Disable "Can't Login?" overlay feature
|
||||
- Fix: Social Tab showing Failed to fetch Data for some accounts
|
||||
- Fix: Pick a Location Dialog Crash
|
||||
- Fix: Auto Open Notification duplication when snapchat is killed or force closed(tq to Kaladin)
|
||||
- Fix: Fixed a logic error where AutoOpenSnaps would ignore per-conversation rules(tq to Kaladin)
|
||||
- Fix: Auto Open engine stuck in "Monitoring" even when there are snaps in the queue(tq to Kaladin)
|
||||
- Fix: Auto Open session toggles not working as expected(tq to Kaladin)
|
||||
- Fix: Auto Open engine is now optimmized to work with the lower end devices to reduce the overheating and excessive lag(tq to Kaladin)
|
||||
- New: Auto Open Scheduler, a new toggle and a new clock picker to schedule time period for your auto open engine to process(tq to Kaladin)
|
||||
- New: Auto Open Notification session statistics toggles, now you can choose which stats to show in the notification(tq to Kaladin)
|
||||
- New: Updated Auto Open notification stats which now includes a tiered "Processing Speed" and "Estimated Finish" stats(tq to Kaladin)
|
||||
- Fix: Video Downloader is saving the video files from spotlight and stories with extension as ".dat file"(tq to Kaladin)
|
||||
|
||||
## v1.6.1
|
||||
- Fix: Custom Frame Rate
|
||||
- Fix: Conversation Sound Style
|
||||
|
||||
@@ -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,40 @@
|
||||
"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"
|
||||
},
|
||||
"backup_profile_action": {
|
||||
"name": "Backup Profile",
|
||||
"description": "Export the full randomized profile as a backup file"
|
||||
},
|
||||
"restore_profile_action": {
|
||||
"name": "Restore Profile",
|
||||
"description": "Import and restore a previously backed up randomized profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"description": "Override the Android ID sent to Snapchat",
|
||||
@@ -2348,7 +2403,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 +3023,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 +3039,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 +3250,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 +3266,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 +3421,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 +3476,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 +3492,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 +3554,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.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"activity": {
|
||||
"wrong_apk_title": "Wrong APK installed",
|
||||
@@ -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"
|
||||
@@ -1627,6 +1648,7 @@
|
||||
"status_monitoring": "Monitoring",
|
||||
"status_active": "Active",
|
||||
"status_paused": "Paused",
|
||||
"thermal_status_title": "Thermal Cooling (Throttled)",
|
||||
"processed_count": "Opened",
|
||||
"queue_size": "Queue",
|
||||
"action_reset": "Reset Statistics",
|
||||
@@ -1661,10 +1683,28 @@
|
||||
"name": "Auto Open Compact Notification",
|
||||
"description": "Use a smaller, single-line notification for status updates"
|
||||
},
|
||||
"show_lifetime_stats": {
|
||||
"name": "Show Lifetime Statistics",
|
||||
"description": "Include the total number of snaps opened since installation in the notification"
|
||||
},
|
||||
"show_queue_preview": {
|
||||
"name": "Show Queue Preview",
|
||||
"description": "Show a list of the most recent snaps waiting in the queue (Expanded only)"
|
||||
},
|
||||
"thermal_protection": {
|
||||
"name": "Thermal Protection",
|
||||
"description": "Automatically throttles the engine and increases delays if the device temperature exceeds 40°C to prevent overheating"
|
||||
},
|
||||
"only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" },
|
||||
"pre_fetch_snaps": { "name": "Pre-fetch Snaps", "description": "Fetches snap media into Snapchat's internal cache once the snap arrives. Note: Turning it on increases background data usage." },
|
||||
"content_type_snap": "Snap",
|
||||
"only_when_idle": {
|
||||
"name": "Auto Open only when Idle",
|
||||
"description": "Only process queue when the device is not in active use"
|
||||
"name": "Auto Open Schedule",
|
||||
"description": "Configure a specific time window where the engine will throttle its speed."
|
||||
},
|
||||
"sleep_window": {
|
||||
"name": "Auto Open Scheduler",
|
||||
"description": "Define the start and end times for scheduled throttled processing."
|
||||
},
|
||||
"pause_during_gaming": { "name": "Pause Auto Open During Gaming", "description": "Automatically slow down processing when a resource intensive app or a game is in the foreground" },
|
||||
"safe_processing": { "name": "Auto Open with stealth pace", "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." }
|
||||
@@ -1872,6 +1912,16 @@
|
||||
"name": "Disable Metrics",
|
||||
"description": "Blocks sending specific analytic data to Snapchat"
|
||||
},
|
||||
"performance_mode": {
|
||||
"name": "Performance Mode",
|
||||
"description": "Applies an app-wide speed profile for navigation, preview loading, background work, and camera responsiveness",
|
||||
"properties": {
|
||||
"performance_profile": {
|
||||
"name": "Performance Profile",
|
||||
"description": "Select how aggressively PurrfectSnap pushes app-wide performance tuning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_story_sections": {
|
||||
"name": "Disable Story Sections",
|
||||
"description": "Removes sections from the Stories page\nMay require a refresh to work properly"
|
||||
@@ -2119,6 +2169,426 @@
|
||||
"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_settings": {
|
||||
"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_settings": {
|
||||
"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_settings": {
|
||||
"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"
|
||||
},
|
||||
"backup_profile_action": {
|
||||
"name": "Backup Profile",
|
||||
"description": "Export the full randomized profile as a backup file"
|
||||
},
|
||||
"restore_profile_action": {
|
||||
"name": "Restore Profile",
|
||||
"description": "Import and restore a previously backed up randomized profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"description": "Override the Android ID sent to Snapchat",
|
||||
@@ -2165,7 +2635,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_optimization": { "name": "Network Optimization", "description": "Optimizes network socket buffers for higher throughput" }, "better_transcript": {
|
||||
"network_optimization": { "name": "Improved Network Connectivity", "description": "Optimizes network socket buffers for maximum stability and high-speed upload/download performance" }, "better_transcript": {
|
||||
"name": "Better Transcript",
|
||||
"description": "Improves the voice note transcript",
|
||||
"properties": {
|
||||
@@ -2471,6 +2941,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",
|
||||
@@ -2558,7 +3031,8 @@
|
||||
"NOTE": "Audio Note",
|
||||
"SNAP": "Snap",
|
||||
"SAVEABLE_SNAP": "Saveable Snap",
|
||||
"null": "Snapchat Default"
|
||||
"null": "Snapchat Default",
|
||||
"multiple_media_toast": "You can only send one media at a time"
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
"hide_caption_text": "Hide Caption Text",
|
||||
@@ -2605,6 +3079,11 @@
|
||||
"custom_image_upload_format": {
|
||||
"null": "Automatic"
|
||||
},
|
||||
"performance_profile": {
|
||||
"smooth": "Smooth",
|
||||
"max": "Max",
|
||||
"null": "Disabled"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
@@ -3085,6 +3564,7 @@
|
||||
"positive": "Yes",
|
||||
"negative": "No",
|
||||
"cancel": "Cancel",
|
||||
"copy": "Copy",
|
||||
"save": "Save",
|
||||
"open": "Open",
|
||||
"download": "Download",
|
||||
@@ -3102,6 +3582,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 +3598,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": {
|
||||
@@ -3257,6 +3743,11 @@
|
||||
"forced_logout_toast": "Removed account due to forced logout"
|
||||
},
|
||||
"auto_open_snaps": { "title": "Auto Open Snaps", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", "priority_title": "Auto Open Snaps (Priority)",
|
||||
"auto_open_schedule": {
|
||||
"title": "Auto Open Scheduler",
|
||||
"start": "Start",
|
||||
"end": "End"
|
||||
},
|
||||
"error_title": "Auto Open Snaps (Errors)",
|
||||
"channel_description": "Notifications for auto-opening snaps queue status",
|
||||
"priority_channel_description": "High priority notifications for auto-opening snaps",
|
||||
@@ -3276,7 +3767,18 @@
|
||||
"paused_message": "Processing paused. Queue preserved ({count} snaps)",
|
||||
"status_paused": "Paused",
|
||||
"status_monitoring": "Monitoring",
|
||||
"thermal_status_title": "Thermal Cooling (Throttled)",
|
||||
"status_active": "Active",
|
||||
"status_failed": "Failed to open {sender}",
|
||||
"status_retrying": "Retrying in background...",
|
||||
"processing_speed_full": "Full Speed",
|
||||
"processing_speed": "Processing Speed",
|
||||
"speed_throttled": "Throttled",
|
||||
"estimated_time": "Estimated Time",
|
||||
"notification_statistics": "STATISTICS",
|
||||
"notification_total_opened": "Lifetime Opened",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_no_snaps_queue": "Monitoring snaps in background...",
|
||||
"queue_cleared": "Queue cleared and statistics reset",
|
||||
"queue_cleared_title": "Queue cleared",
|
||||
"queue_cleared_reset": "Queue Cleared & Reset",
|
||||
@@ -3350,6 +3852,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 +3868,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",
|
||||
@@ -3378,7 +3886,6 @@
|
||||
"material3_strings": {
|
||||
"date_range_picker_start_headline": "From",
|
||||
"date_range_picker_end_headline": "To",
|
||||
"date_range_picker_title": "Select date range",
|
||||
"date_picker_switch_to_calendar_mode": "Calendar",
|
||||
"date_picker_switch_to_input_mode": "Input",
|
||||
"date_range_picker_scroll_to_previous_month": "Previous month",
|
||||
@@ -3522,9 +4029,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 +4087,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 +4103,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 +4165,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.",
|
||||
@@ -3686,17 +4204,25 @@
|
||||
"include_saved_locations_description": "Export your saved location coordinates",
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"add": "Add",
|
||||
"ok": "OK",
|
||||
"quit": "Quit",
|
||||
"done": "Done",
|
||||
"back": "Back",
|
||||
"message": "Message",
|
||||
"type_message": "Type message...",
|
||||
"unknown": "Unknown",
|
||||
"unknown_error": "Unknown error",
|
||||
"not_available": "N/A",
|
||||
"added": "Added",
|
||||
"no_friends_found": "No friends found",
|
||||
"no_messages": "No messages",
|
||||
"message": "Message",
|
||||
"type_message": "Type message...",
|
||||
"exporting_memories": "Exporting memories... ({failed} failed)"
|
||||
},
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
@@ -3843,10 +4369,3 @@
|
||||
"tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?",
|
||||
"tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history."
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -38,9 +38,18 @@ class Global : ConfigContainer() {
|
||||
val customUploadImageFormat = unique("custom_image_upload_format", "jpeg", "png", "webp") { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE) }
|
||||
}
|
||||
|
||||
inner class PerformanceModeConfig : ConfigContainer() {
|
||||
val profile = unique("performance_profile", "smooth", "max") {
|
||||
requireRestart()
|
||||
}
|
||||
}
|
||||
|
||||
val betterLocation = container("better_location", BetterLocationConfig())
|
||||
val snapchatPlus = unique("snapchat_plus", "not_subscribed", "basic", "ad_free") { requireRestart() }
|
||||
val mediaUploadQualityConfig = container("media_upload_quality", MediaUploadQualityConfig())
|
||||
val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }.apply {
|
||||
profile.set("max")
|
||||
}
|
||||
val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "erase_message", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
|
||||
val disableMetrics = boolean("disable_metrics") { requireRestart() }
|
||||
val disableStorySections = multiple("disable_story_sections", "friends", "suggested_stories", "following", "discover") { requireRestart(); requireCleanCache() }
|
||||
|
||||
@@ -166,7 +166,7 @@ class MessagingTweaks : ConfigContainer() {
|
||||
val maxDelayMs = integer("max_delay_ms", defaultValue = 100) {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(0) != null && it.toInt() > minDelay.get() }
|
||||
}
|
||||
val queueSize = integer("queue_size", defaultValue = 10) {
|
||||
val queueSize = integer("queue_size", defaultValue = 1000) {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1) != null }
|
||||
}
|
||||
val retryAttempts = integer("retry_attempts", defaultValue = 5) {
|
||||
@@ -176,12 +176,20 @@ class MessagingTweaks : ConfigContainer() {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null }
|
||||
}
|
||||
val compactNotification = boolean("compact_notification", false)
|
||||
val showLifetimeStats = boolean("show_lifetime_stats", false)
|
||||
val showQueuePreview = boolean("show_queue_preview", true)
|
||||
val thermalProtection = boolean("thermal_protection", false)
|
||||
|
||||
// Resource Intelligence: Smart triggers for battery and data safety
|
||||
val onlyOnWifi = boolean("only_on_wifi", false)
|
||||
val onlyWhenIdle = boolean("only_when_idle", false)
|
||||
val preFetchSnaps = boolean("pre_fetch_snaps", false)
|
||||
val pauseDuringGaming = boolean("pause_during_gaming", false)
|
||||
val safeProcessing = boolean("safe_processing", true)
|
||||
val onlyWhenIdle = boolean("only_when_idle", false)
|
||||
val sleepWindow = string("sleep_window", defaultValue = "23:00-07:00") {
|
||||
addFlags(ConfigFlag.NO_DISABLE_KEY)
|
||||
inputCheck = { it.matches(Regex("^([01]\\d|2[0-3]):([0-5]\\d)-([01]\\d|2[0-3]):([0-5]\\d)$")) }
|
||||
}
|
||||
}
|
||||
|
||||
class AutoDeleteSentMessagesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
@@ -207,6 +215,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() }
|
||||
@@ -324,4 +334,3 @@ class MessagingTweaks : ConfigContainer() {
|
||||
|
||||
val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,195 @@ 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 secureSettings = container("secure_settings", RandomizedSecureSettingsConfig().apply { globalState = true })
|
||||
val systemSettings = container("system_settings", RandomizedSystemSettingsConfig().apply { globalState = true })
|
||||
val globalSettings = container("global_settings", 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 backupProfileAction = string("backup_profile_action")
|
||||
val restoreProfileAction = string("restore_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 +201,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 +224,7 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
|
||||
"realme GT 6"
|
||||
) {
|
||||
requireRestart()
|
||||
addFlags(ConfigFlag.HIDDEN)
|
||||
customOptionTranslationPath = "features.options.device_model"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ enum class FileType(
|
||||
JPG("jpg", "image/jpg",false, true, false),
|
||||
ZIP("zip", "application/zip", false, false, false),
|
||||
WEBP("webp", "image/webp", false, true, false),
|
||||
HEIC("heic", "image/heic", false, true, false),
|
||||
HEIF("heif", "image/heif", false, true, false),
|
||||
MPD("mpd", "text/xml", false, false, false),
|
||||
UNKNOWN("dat", "application/octet-stream", false, false, false);
|
||||
|
||||
@@ -64,6 +66,11 @@ enum class FileType(
|
||||
}
|
||||
|
||||
val majorBrand = String(array, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
|
||||
|
||||
// Explicitly exclude known IMAGE-only brands to prevent false positives (HEIC/HEIF)
|
||||
val imageBrands = setOf("heic", "heix", "hevc", "hevx", "mif1", "msf1")
|
||||
if (majorBrand in imageBrands) return false
|
||||
|
||||
return majorBrand in setOf(
|
||||
"mp41",
|
||||
"mp42",
|
||||
@@ -75,14 +82,14 @@ enum class FileType(
|
||||
"iso6",
|
||||
"avc1",
|
||||
"dash",
|
||||
"mif1",
|
||||
"cmfc",
|
||||
"msnv",
|
||||
"3gp4",
|
||||
"3gp5",
|
||||
"3gp6",
|
||||
"3g2a",
|
||||
"3g2b"
|
||||
)
|
||||
) || majorBrand.isNotEmpty() // FALLBACK: If it has the ftyp box and isn't a known image brand, it's a video
|
||||
}
|
||||
|
||||
fun fromFile(file: File): FileType {
|
||||
@@ -97,8 +104,24 @@ enum class FileType(
|
||||
val headerBytes = ByteArray(16)
|
||||
System.arraycopy(array, 0, headerBytes, 0, 16)
|
||||
val hex = bytesToHex(headerBytes)
|
||||
return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value
|
||||
?: if (looksLikeIsoBmffVideo(headerBytes)) MP4 else UNKNOWN
|
||||
|
||||
// 1. Check strict signatures
|
||||
fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value?.let { return it }
|
||||
|
||||
// 2. Check ISO BMFF container type
|
||||
val majorBrand = if (headerBytes.size >= 12 &&
|
||||
headerBytes[4] == 'f'.code.toByte() && headerBytes[5] == 't'.code.toByte() &&
|
||||
headerBytes[6] == 'y'.code.toByte() && headerBytes[7] == 'p'.code.toByte()) {
|
||||
String(headerBytes, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
|
||||
} else null
|
||||
|
||||
if (majorBrand != null) {
|
||||
if (majorBrand in setOf("heic", "heix")) return HEIC
|
||||
if (majorBrand in setOf("mif1", "msf1")) return HEIF
|
||||
if (looksLikeIsoBmffVideo(headerBytes)) return MP4
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
fun fromInputStream(inputStream: InputStream): FileType {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -47,6 +47,14 @@ class DatabaseAccess(
|
||||
} == true
|
||||
}
|
||||
|
||||
private val hasArroyoUserConversationTable by lazy {
|
||||
useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||
safeRawQuery("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'user_conversation'")?.use { query ->
|
||||
query.moveToFirst() && query.getStringOrNull("name") == "user_conversation"
|
||||
}
|
||||
} == true
|
||||
}
|
||||
|
||||
private fun useDatabase(database: DatabaseType, writeMode: Boolean = false): SQLiteDatabase? {
|
||||
// only cache read-only databases
|
||||
if (!writeMode && openedDatabases.containsKey(database) && openedDatabases[database]?.isOpen == true) {
|
||||
@@ -148,6 +156,10 @@ class DatabaseAccess(
|
||||
}?.toMutableMap() ?: mutableMapOf()
|
||||
}
|
||||
|
||||
if (!hasArroyoUserConversationTable) {
|
||||
return@lazy mutableMapOf()
|
||||
}
|
||||
|
||||
(useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||
safeRawQuery(
|
||||
"SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?",
|
||||
@@ -354,7 +366,7 @@ class DatabaseAccess(
|
||||
}
|
||||
|
||||
fun getConversationType(conversationId: String): Int? {
|
||||
if (hasArroyoConversationTable) {
|
||||
if (hasArroyoConversationTable || !hasArroyoUserConversationTable) {
|
||||
return getFeedEntryByConversationId(conversationId)?.conversationType
|
||||
}
|
||||
|
||||
@@ -372,7 +384,9 @@ class DatabaseAccess(
|
||||
}
|
||||
|
||||
fun getDMConversationId(userId: String): String? {
|
||||
if (hasArroyoConversationTable) {
|
||||
friendDMsCache[userId]?.let { return it }
|
||||
|
||||
if (hasArroyoConversationTable || !hasArroyoUserConversationTable) {
|
||||
return friendDMsCache[userId]
|
||||
}
|
||||
|
||||
@@ -408,6 +422,10 @@ class DatabaseAccess(
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasArroyoUserConversationTable) {
|
||||
return getFeedEntryByConversationId(conversationId)?.participants
|
||||
}
|
||||
|
||||
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||
safeRawQuery(
|
||||
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ?",
|
||||
|
||||
@@ -99,6 +99,7 @@ class FeatureManager(
|
||||
MeoPasscodeBypass(),
|
||||
AppLock(),
|
||||
CameraTweaks(),
|
||||
PerformanceMode(),
|
||||
InfiniteStoryBoost(),
|
||||
PinConversations(),
|
||||
DeviceSpooferHook(),
|
||||
|
||||
@@ -2,9 +2,11 @@ package me.eternal.purrfectsnap.core.features
|
||||
|
||||
import me.eternal.purrfectsnap.common.data.MessagingRuleType
|
||||
import me.eternal.purrfectsnap.common.data.RuleState
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) {
|
||||
private val listeners = mutableListOf<(String, Boolean) -> Unit>()
|
||||
private val ruleCache = ConcurrentHashMap<String, Boolean>()
|
||||
|
||||
fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) {
|
||||
listeners.add(listener)
|
||||
@@ -13,18 +15,22 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp
|
||||
open fun getRuleState() = context.config.rules.getRuleState(ruleType)
|
||||
|
||||
fun setState(conversationId: String, state: Boolean) {
|
||||
val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId
|
||||
context.bridgeClient.setRule(
|
||||
context.database.getDMOtherParticipant(conversationId) ?: conversationId,
|
||||
targetId,
|
||||
ruleType,
|
||||
state
|
||||
)
|
||||
ruleCache[targetId] = state
|
||||
listeners.forEach { it(conversationId, state) }
|
||||
}
|
||||
|
||||
fun getState(conversationId: String) =
|
||||
context.bridgeClient.getRules(
|
||||
context.database.getDMOtherParticipant(conversationId) ?: conversationId
|
||||
).contains(ruleType) && getRuleState() != null
|
||||
fun getState(conversationId: String): Boolean {
|
||||
val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId
|
||||
return ruleCache.getOrPut(targetId) {
|
||||
context.bridgeClient.getRules(targetId).contains(ruleType)
|
||||
} && getRuleState() != null
|
||||
}
|
||||
|
||||
fun canUseRule(conversationId: String): Boolean {
|
||||
if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) {
|
||||
|
||||
@@ -37,18 +37,39 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
}.getOrNull()
|
||||
|
||||
val propertyOverrides = mutableMapOf<String, ConfigFilter>()
|
||||
val loggedOverrides = mutableSetOf<String>()
|
||||
|
||||
fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) {
|
||||
propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment)
|
||||
}
|
||||
|
||||
fun logPerformanceOverride(key: String, value: Any?) {
|
||||
if (!key.contains("PRELOAD") &&
|
||||
!key.contains("PERFORMANCE") &&
|
||||
!key.contains("WARM") &&
|
||||
!key.contains("PREFETCH") &&
|
||||
!key.contains("LATENCY") &&
|
||||
!key.contains("ANALYTICS") &&
|
||||
!key.contains("THREAD_PRIORITY") &&
|
||||
!key.contains("HD_MODE") &&
|
||||
!key.contains("LENS") &&
|
||||
!key.contains("THUMBNAIL")
|
||||
) {
|
||||
return
|
||||
}
|
||||
synchronized(loggedOverrides) {
|
||||
if (!loggedOverrides.add(key)) return
|
||||
}
|
||||
context.log.info("Performance override applied: $key=$value", "PerformanceMode")
|
||||
}
|
||||
|
||||
overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() },
|
||||
{ true })
|
||||
overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() },
|
||||
overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null },
|
||||
{ true }, isAppExperiment = true)
|
||||
|
||||
run {
|
||||
val isForceQuality = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() }
|
||||
val isForceQuality = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null }
|
||||
val level7Value = { _: ConfigKeyInfo -> 700 }
|
||||
arrayOf(
|
||||
"MY_STORY_UPLOAD_QUALITY_LEVEL",
|
||||
@@ -72,10 +93,14 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
isForceQuality, { true })
|
||||
overrideProperty("MEDIA_QUALITY_LEVEL_DOWNGRADING_PERCENTAGE",
|
||||
isForceQuality, { 0.0f })
|
||||
overrideProperty("CHAT_MEDIA_IMPORT_TRANSCODED_QUALITY",
|
||||
isForceQuality, { 4 })
|
||||
overrideProperty("POSTED_STORY_IMPORT_TRANSCODED_QUALITY",
|
||||
isForceQuality, { 4 })
|
||||
}
|
||||
|
||||
run {
|
||||
val isDisableCompression = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.disableImageCompression.get() }
|
||||
val isDisableCompression = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.disableImageCompression.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null }
|
||||
overrideProperty("LIBJPEG_IMAGE_ENCODING_QUALITY", isDisableCompression, { 100 })
|
||||
overrideProperty("LIBJPEG_IMAGE_ENCODING_QUALITY_V2", isDisableCompression, { 100 })
|
||||
}
|
||||
@@ -84,6 +109,44 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
{ true })
|
||||
overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() },
|
||||
{ true })
|
||||
overrideProperty("PREVIEW_PRELOAD_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ true })
|
||||
overrideProperty("BUFFERED_VIDEO_RECORDING_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ true })
|
||||
overrideProperty("CAMERA_THREAD_PRIORITY", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ true })
|
||||
overrideProperty("HD_MODE_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() == "max" },
|
||||
{ true })
|
||||
|
||||
arrayOf(
|
||||
"FEATURE_PRELOADER",
|
||||
"USER_STORY_PRELOAD",
|
||||
"STARTUP_LENS_ACTIVATOR",
|
||||
"LENSES_PREVIEW_ACTIVATOR",
|
||||
"THUMBNAIL_PRESENTER_ACTIVATOR",
|
||||
"SINGLE_SEGMENT_THUMBNAIL_ACTIVATOR",
|
||||
"SERVER_PREFETCH",
|
||||
"SERVER_PREFETCH_WITH_COF",
|
||||
"DISCOVER_FEED_PERFORMANCE",
|
||||
"DISCOVER_FEED_STORY_PREFETCH",
|
||||
"DISCOVER_FEED_THUMBNAILS",
|
||||
"LOGIN_PRELOAD",
|
||||
"PREFETCH_REPO_SUBSCRIBE_ON_CPU",
|
||||
"COMPUTE_FEED_CACHE_WITH_TTL",
|
||||
"COMPUTE_FEED_NETWORK_WITH_CACHE",
|
||||
"OPERA_WARMUP",
|
||||
"REFACTORED_WITH_WARMUP_LENS",
|
||||
"SHOW_PREFETCH",
|
||||
).forEach { key ->
|
||||
overrideProperty(key, { context.config.global.performanceMode.profile.getNullable() != null }, { true })
|
||||
}
|
||||
|
||||
overrideProperty("LOAD_LATENCY_TRACKER_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ false })
|
||||
overrideProperty("ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ false })
|
||||
overrideProperty("LOCK_SCREEN_ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
|
||||
{ false })
|
||||
overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() },
|
||||
{ true })
|
||||
|
||||
@@ -115,7 +178,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
|
||||
propertyOverrides[propertyKey.name]?.let { (filter, value) ->
|
||||
if (!filter(propertyKey)) return@let
|
||||
param.setResult(value(propertyKey))
|
||||
value(propertyKey).also {
|
||||
logPerformanceOverride(propertyKey.name ?: return@also, it)
|
||||
param.setResult(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +201,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
propertyOverrides[key]?.let { (filter, value) ->
|
||||
val keyInfo = getConfigKeyInfo(enumData) ?: return@let
|
||||
if (!filter(keyInfo)) return@let
|
||||
setValue(value(keyInfo))
|
||||
value(keyInfo).also {
|
||||
logPerformanceOverride(key, it)
|
||||
setValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +220,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
}
|
||||
propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) ->
|
||||
if (isAppExperiment != true || !filter(keyInfo)) return@let
|
||||
param.setResult(value(keyInfo))
|
||||
value(keyInfo).also {
|
||||
logPerformanceOverride(keyInfo.name ?: return@also, it)
|
||||
param.setResult(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +246,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
}
|
||||
|
||||
val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook
|
||||
propertyOverride.isAppExperiment.takeIf { propertyOverride.filter(keyInfo) }?.let { param.setResult(it) }
|
||||
propertyOverride.isAppExperiment.takeIf { propertyOverride.filter(keyInfo) }?.let {
|
||||
logPerformanceOverride(keyInfo.name ?: return@let, it)
|
||||
param.setResult(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,578 @@
|
||||
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 = randomInt(-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 { randomInt(1, 224) }
|
||||
.first { candidate ->
|
||||
candidate != 10 &&
|
||||
candidate != 127 &&
|
||||
candidate != 169 &&
|
||||
candidate != 172 &&
|
||||
candidate != 192
|
||||
}
|
||||
}
|
||||
val secondOctet = randomInt(1, 255)
|
||||
val thirdOctet = randomInt(1, 255)
|
||||
val fourthOctet = randomInt(2, 255)
|
||||
val candidate = "$firstOctet.$secondOctet.$thirdOctet.$fourthOctet"
|
||||
return runCatching { InetAddress.getByName(candidate).hostAddress }.getOrDefault(candidate)
|
||||
}
|
||||
|
||||
private fun randomInt(minInclusive: Int, maxExclusive: Int): Int {
|
||||
require(maxExclusive > minInclusive)
|
||||
return minInclusive + random.nextInt(maxExclusive - minInclusive)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import me.eternal.purrfectsnap.core.features.Feature
|
||||
|
||||
class DisableMetrics : Feature("DisableMetrics") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableMetrics.get()) return
|
||||
if (!context.config.global.disableMetrics.get() && context.config.global.performanceMode.profile.getNullable() == null) return
|
||||
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { param ->
|
||||
val url = param.url
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.tweaks
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.hardware.camera2.CaptureRequest
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Build
|
||||
import android.transition.Transition
|
||||
import android.os.HandlerThread
|
||||
import android.os.Process
|
||||
import android.util.Base64
|
||||
import android.util.Range
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.Animation
|
||||
import android.widget.OverScroller
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import java.io.File
|
||||
import java.lang.Thread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
|
||||
import okhttp3.Dispatcher
|
||||
|
||||
class PerformanceMode : Feature("Performance Mode") {
|
||||
private data class SnapshotCell(
|
||||
val type: Int,
|
||||
val stringValue: String? = null,
|
||||
val longValue: Long? = null,
|
||||
val doubleValue: Double? = null,
|
||||
val blobValue: String? = null,
|
||||
)
|
||||
|
||||
private data class CursorSnapshot(
|
||||
val columns: List<String>,
|
||||
val rows: List<List<SnapshotCell>>,
|
||||
)
|
||||
|
||||
override fun init() {
|
||||
val profile = context.config.global.performanceMode.profile.getNullable() ?: return
|
||||
val isMaxProfile = profile == "max"
|
||||
val threadPriority = if (isMaxProfile) {
|
||||
Process.THREAD_PRIORITY_DISPLAY
|
||||
} else {
|
||||
Process.THREAD_PRIORITY_MORE_FAVORABLE
|
||||
}
|
||||
val minimumFrameRate = if (isMaxProfile) 60 else 45
|
||||
val durationScale = if (isMaxProfile) 0.35f else 0.55f
|
||||
val recyclerViewCacheSize = if (isMaxProfile) 64 else 32
|
||||
val maxRequests = if (isMaxProfile) 192 else 96
|
||||
val maxRequestsPerHost = if (isMaxProfile) 32 else 16
|
||||
val minimumCoreThreads = if (isMaxProfile) 16 else 8
|
||||
val prefetchItemCount = if (isMaxProfile) 24 else 12
|
||||
val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L
|
||||
val maxScrollDurationMs = if (isMaxProfile) 120 else 180
|
||||
val preferredRefreshRate = if (isMaxProfile) 120f else 90f
|
||||
|
||||
context.log.info(
|
||||
"Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate",
|
||||
"PerformanceMode"
|
||||
)
|
||||
|
||||
runCatching {
|
||||
ValueAnimator.setFrameDelay(0L)
|
||||
context.log.info("Applied ValueAnimator frame delay override: 0ms", "PerformanceMode")
|
||||
}
|
||||
|
||||
fun firstHitLogger(name: String): (String) -> Unit {
|
||||
val didLog = AtomicBoolean(false)
|
||||
return { details ->
|
||||
if (didLog.compareAndSet(false, true)) {
|
||||
context.log.info("First hit: $name | $details", "PerformanceMode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val handlerThreadConstructorLog = firstHitLogger("HandlerThread.constructor")
|
||||
val handlerThreadStartLog = firstHitLogger("HandlerThread.start")
|
||||
val threadStartLog = firstHitLogger("Thread.start")
|
||||
val executorLog = firstHitLogger("ThreadPoolExecutor.constructor")
|
||||
val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor")
|
||||
val animatorLog = firstHitLogger("ValueAnimator.getDurationScale")
|
||||
val animatorDurationLog = firstHitLogger("ValueAnimator.setDuration")
|
||||
val viewAnimatorDurationLog = firstHitLogger("ViewPropertyAnimator.setDuration")
|
||||
val transitionDurationLog = firstHitLogger("Transition.setDuration")
|
||||
val animationDurationLog = firstHitLogger("Animation.setDuration")
|
||||
val recyclerCtorLog = firstHitLogger("RecyclerView.constructor")
|
||||
val recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter")
|
||||
val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager")
|
||||
val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase")
|
||||
val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase")
|
||||
val mediaRecorderLog = firstHitLogger("MediaRecorder.setVideoFrameRate")
|
||||
val captureRequestLog = firstHitLogger("CaptureRequest.Builder.set")
|
||||
val sustainedModeLog = firstHitLogger("Window.setSustainedPerformanceMode")
|
||||
val refreshRateLog = firstHitLogger("Activity.preferredRefreshRate")
|
||||
val overScrollerLog = firstHitLogger("OverScroller.startScroll")
|
||||
val chatFeedCacheServeLog = firstHitLogger("ChatFeed.cacheServe")
|
||||
val chatFeedCacheRefreshLog = firstHitLogger("ChatFeed.cacheRefresh")
|
||||
val mapDialogLog = firstHitLogger("Dialog.show")
|
||||
val mapViewLog = firstHitLogger("MapView.constructor")
|
||||
val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock")
|
||||
val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration")
|
||||
val mapThreadLog = firstHitLogger("SnapMap.mapThread")
|
||||
val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps")
|
||||
|
||||
fun isPerformanceSensitiveThread(name: String?): Boolean {
|
||||
val normalizedName = name?.lowercase() ?: return false
|
||||
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any {
|
||||
normalizedName.contains(it)
|
||||
}
|
||||
}
|
||||
|
||||
val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() }
|
||||
val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json")
|
||||
|
||||
fun isChatFeedQuery(sql: String): Boolean {
|
||||
val normalized = sql.uppercase()
|
||||
if (!normalized.startsWith("SELECT")) return false
|
||||
val hitsFriendsFeedView = sql.contains("FriendsFeedView")
|
||||
val hitsFeedEntry = sql.contains("feed_entry") && (sql.contains("last_updated_timestamp") || sql.contains("displayInteractionType") || sql.contains("streak_count"))
|
||||
return (hitsFriendsFeedView || hitsFeedEntry) &&
|
||||
!normalized.contains("COUNT(") &&
|
||||
!normalized.contains("SELECT 0") &&
|
||||
!normalized.contains("WHERE KEY = ?") &&
|
||||
!normalized.contains("WHERE CLIENT_CONVERSATION_ID = ?")
|
||||
}
|
||||
|
||||
fun cursorCell(cursor: Cursor, index: Int): SnapshotCell {
|
||||
return when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_NULL -> SnapshotCell(Cursor.FIELD_TYPE_NULL)
|
||||
Cursor.FIELD_TYPE_INTEGER -> SnapshotCell(Cursor.FIELD_TYPE_INTEGER, longValue = cursor.getLong(index))
|
||||
Cursor.FIELD_TYPE_FLOAT -> SnapshotCell(Cursor.FIELD_TYPE_FLOAT, doubleValue = cursor.getDouble(index))
|
||||
Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
|
||||
Cursor.FIELD_TYPE_BLOB -> SnapshotCell(
|
||||
Cursor.FIELD_TYPE_BLOB,
|
||||
blobValue = Base64.encodeToString(cursor.getBlob(index), Base64.NO_WRAP)
|
||||
)
|
||||
else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
|
||||
}
|
||||
}
|
||||
|
||||
fun snapshotFromCursor(cursor: Cursor): CursorSnapshot {
|
||||
val columns = cursor.columnNames.toList()
|
||||
val rows = mutableListOf<List<SnapshotCell>>()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
rows += columns.indices.map { index -> cursorCell(cursor, index) }
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
return CursorSnapshot(columns, rows)
|
||||
}
|
||||
|
||||
fun snapshotToMatrixCursor(snapshot: CursorSnapshot): MatrixCursor {
|
||||
return MatrixCursor(snapshot.columns.toTypedArray(), snapshot.rows.size).also { matrixCursor ->
|
||||
snapshot.rows.forEach { row ->
|
||||
matrixCursor.addRow(row.map { cell ->
|
||||
when (cell.type) {
|
||||
Cursor.FIELD_TYPE_NULL -> null
|
||||
Cursor.FIELD_TYPE_INTEGER -> cell.longValue
|
||||
Cursor.FIELD_TYPE_FLOAT -> cell.doubleValue
|
||||
Cursor.FIELD_TYPE_BLOB -> cell.blobValue?.let { Base64.decode(it, Base64.NO_WRAP) }
|
||||
else -> cell.stringValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readSnapshot(file: File): CursorSnapshot? {
|
||||
return runCatching {
|
||||
if (!file.exists()) return null
|
||||
context.gson.fromJson(file.readText(Charsets.UTF_8), CursorSnapshot::class.java)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun writeSnapshot(file: File, snapshot: CursorSnapshot) {
|
||||
runCatching {
|
||||
file.writeText(context.gson.toJson(snapshot), Charsets.UTF_8)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to persist friend list snapshot", it, "PerformanceMode")
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
|
||||
if (!isMaxProfile) return@subscribe
|
||||
val url = event.url
|
||||
if (url.contains("ami/friends")) {
|
||||
if (chatFeedSnapshotFile.exists()) {
|
||||
chatFeedSnapshotFile.delete()
|
||||
context.log.info("Invalidated chat feed snapshot after friends mutation sync", "PerformanceMode")
|
||||
}
|
||||
}
|
||||
if (url.contains("messaging") || url.contains("conversation") || url.contains("feed")) {
|
||||
if (chatFeedSnapshotFile.exists()) {
|
||||
chatFeedSnapshotFile.delete()
|
||||
context.log.info("Invalidated chat feed snapshot after messaging/feed network activity", "PerformanceMode")
|
||||
}
|
||||
}
|
||||
if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) {
|
||||
event.canceled = true
|
||||
mapboxNetworkBlockLog("url=$url")
|
||||
}
|
||||
}
|
||||
|
||||
HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param ->
|
||||
if (param.args().size < 2) return@hookConstructor
|
||||
val threadName = param.argNullable<String>(0)
|
||||
if (!isPerformanceSensitiveThread(threadName)) return@hookConstructor
|
||||
param.setArg(1, threadPriority)
|
||||
handlerThreadConstructorLog("name=$threadName priority=$threadPriority")
|
||||
}
|
||||
|
||||
HandlerThread::class.java.hook("start", HookStage.AFTER) { param ->
|
||||
val thread = param.nullableThisObject<Any>() as? HandlerThread ?: return@hook
|
||||
if (!isPerformanceSensitiveThread(thread.name)) return@hook
|
||||
runCatching {
|
||||
val tid = thread.threadId
|
||||
if (tid > 0) {
|
||||
Process.setThreadPriority(tid, threadPriority)
|
||||
}
|
||||
}
|
||||
handlerThreadStartLog("name=${thread.name} tid=${thread.threadId} priority=$threadPriority")
|
||||
}
|
||||
|
||||
Thread::class.java.hook("start", HookStage.AFTER) { param ->
|
||||
val thread = param.thisObject<Thread>()
|
||||
if (!isPerformanceSensitiveThread(thread.name)) return@hook
|
||||
runCatching {
|
||||
thread.priority = Thread.MAX_PRIORITY
|
||||
}
|
||||
threadStartLog("name=${thread.name} priority=${thread.priority}")
|
||||
if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) {
|
||||
mapThreadLog("name=${thread.name} priority=${thread.priority}")
|
||||
}
|
||||
}
|
||||
|
||||
ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
val executor = param.thisObject<ThreadPoolExecutor>()
|
||||
runCatching {
|
||||
val targetCorePoolSize = executor.maximumPoolSize.coerceAtLeast(1).coerceAtMost(minimumCoreThreads.coerceAtLeast(executor.corePoolSize))
|
||||
if (executor.corePoolSize < targetCorePoolSize) {
|
||||
executor.corePoolSize = targetCorePoolSize
|
||||
}
|
||||
executor.allowCoreThreadTimeOut(false)
|
||||
executor.prestartAllCoreThreads()
|
||||
executorLog("core=${executor.corePoolSize} max=${executor.maximumPoolSize} active=${executor.activeCount}")
|
||||
}
|
||||
}
|
||||
|
||||
Dispatcher::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
val dispatcher = param.thisObject<Dispatcher>()
|
||||
runCatching {
|
||||
dispatcher.maxRequests = maxRequests
|
||||
dispatcher.maxRequestsPerHost = maxRequestsPerHost
|
||||
dispatcherLog("maxRequests=${dispatcher.maxRequests} maxRequestsPerHost=${dispatcher.maxRequestsPerHost}")
|
||||
}
|
||||
}
|
||||
|
||||
ValueAnimator::class.java.hook("getDurationScale", HookStage.AFTER) { param ->
|
||||
param.setResult(durationScale)
|
||||
animatorLog("durationScale=$durationScale")
|
||||
}
|
||||
|
||||
ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
val original = param.arg<Long>(0)
|
||||
val updated = original.coerceAtMost(maxAnimationDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
animatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
val thisObject = param.nullableThisObject<Any>()?.javaClass?.name ?: ""
|
||||
if (thisObject.contains("map", ignoreCase = true)) {
|
||||
mapCameraAnimLog("owner=$thisObject requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
val original = param.arg<Long>(0)
|
||||
val updated = original.coerceAtMost(maxAnimationDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
viewAnimatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
Transition::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
val original = param.arg<Long>(0)
|
||||
val updated = original.coerceAtMost(maxAnimationDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
transitionDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
Animation::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
val original = param.arg<Long>(0)
|
||||
val updated = original.coerceAtMost(maxAnimationDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
animationDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
|
||||
recyclerView.overScrollMode = View.OVER_SCROLL_NEVER
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
if (isMaxProfile) {
|
||||
recyclerView.itemAnimator = null
|
||||
}
|
||||
recyclerCtorLog("cache=$recyclerViewCacheSize max=$isMaxProfile class=${recyclerView::class.java.name}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
if (isMaxProfile) {
|
||||
recyclerView.itemAnimator = null
|
||||
}
|
||||
recyclerAdapterLog("cache=$recyclerViewCacheSize adapter=${param.argNullable<Any>(0)?.javaClass?.name}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hook("setLayoutManager", HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
val layoutManager = param.argNullable<Any>(0)
|
||||
when (layoutManager) {
|
||||
is LinearLayoutManager -> {
|
||||
layoutManager.isItemPrefetchEnabled = true
|
||||
layoutManager.initialPrefetchItemCount = prefetchItemCount
|
||||
}
|
||||
is StaggeredGridLayoutManager -> {
|
||||
layoutManager.isItemPrefetchEnabled = true
|
||||
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
}
|
||||
}
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount")
|
||||
}
|
||||
|
||||
fun SQLiteDatabase.applyPerformancePragmas() {
|
||||
runCatching { execSQL("PRAGMA synchronous = NORMAL") }
|
||||
runCatching { execSQL("PRAGMA temp_store = MEMORY") }
|
||||
runCatching { execSQL("PRAGMA cache_size = -32768") }
|
||||
runCatching { execSQL("PRAGMA mmap_size = 268435456") }
|
||||
runCatching { execSQL("PRAGMA journal_size_limit = 1048576") }
|
||||
runCatching { execSQL("PRAGMA optimize") }
|
||||
}
|
||||
|
||||
SQLiteDatabase::class.java.hook("openDatabase", HookStage.AFTER) { param ->
|
||||
(param.getResult() as? SQLiteDatabase)?.also {
|
||||
it.applyPerformancePragmas()
|
||||
sqliteOpenLog("path=${param.argNullable<Any>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
SQLiteDatabase::class.java.hook("openOrCreateDatabase", HookStage.AFTER) { param ->
|
||||
(param.getResult() as? SQLiteDatabase)?.also {
|
||||
it.applyPerformancePragmas()
|
||||
sqliteCreateLog("path=${param.argNullable<Any>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param ->
|
||||
val currentRate = param.arg<Int>(0)
|
||||
if (currentRate < minimumFrameRate) {
|
||||
param.setArg(0, minimumFrameRate)
|
||||
}
|
||||
mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}")
|
||||
}
|
||||
|
||||
OverScroller::class.java.hook("startScroll", HookStage.BEFORE) { param ->
|
||||
if (param.args().size >= 5) {
|
||||
val original = param.arg<Int>(4)
|
||||
val updated = original.coerceAtMost(maxScrollDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(4, updated)
|
||||
}
|
||||
overScrollerLog("requested=$original applied=${param.arg<Int>(4)}")
|
||||
}
|
||||
}
|
||||
|
||||
OverScroller::class.java.hook("fling", HookStage.BEFORE) { param ->
|
||||
if (param.args().size >= 10) {
|
||||
val overX = param.arg<Int>(8)
|
||||
val overY = param.arg<Int>(9)
|
||||
if (overX != 0) param.setArg(8, 0)
|
||||
if (overY != 0) param.setArg(9, 0)
|
||||
}
|
||||
}
|
||||
|
||||
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
|
||||
val key = param.arg<CaptureRequest.Key<*>>(0)
|
||||
captureRequestLog("key=${key.name} value=${param.argNullable<Any>(1)}")
|
||||
}
|
||||
|
||||
fun applyActivityPerformanceTuning(activity: Activity) {
|
||||
runCatching {
|
||||
activity.window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
val display = activity.display
|
||||
val targetRefreshRate = display?.supportedModes?.maxByOrNull { it.refreshRate }?.refreshRate
|
||||
?.coerceAtLeast(preferredRefreshRate) ?: preferredRefreshRate
|
||||
activity.window.attributes = activity.window.attributes.apply {
|
||||
this.preferredRefreshRate = targetRefreshRate
|
||||
}
|
||||
refreshRateLog("activity=${activity::class.java.name} refreshRate=$targetRefreshRate")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isMaxProfile) {
|
||||
runCatching {
|
||||
activity.window.setSustainedPerformanceMode(true)
|
||||
sustainedModeLog("activity=${activity::class.java.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNextActivityCreate {
|
||||
applyActivityPerformanceTuning(it)
|
||||
}
|
||||
|
||||
Dialog::class.java.hook("show", HookStage.AFTER) { param ->
|
||||
val dialog = param.nullableThisObject<Any>() as? Dialog ?: return@hook
|
||||
val window = dialog.window ?: return@hook
|
||||
runCatching {
|
||||
window.setWindowAnimations(0)
|
||||
window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
window.attributes = window.attributes.apply {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||
}
|
||||
if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) {
|
||||
mapDialogLog("class=${dialog::class.java.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param ->
|
||||
val mapView = param.nullableThisObject<Any>() as? View ?: return@hookConstructor
|
||||
mapView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
mapView.overScrollMode = View.OVER_SCROLL_NEVER
|
||||
mapViewLog("class=${mapView::class.java.name}")
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
findClass("com.mapbox.mapboxsdk.maps.renderer.MapRenderer").hook("setMaximumFps", HookStage.BEFORE) { param ->
|
||||
val requested = param.arg<Int>(0)
|
||||
val applied = requested.coerceAtLeast(120)
|
||||
if (applied != requested) {
|
||||
param.setArg(0, applied)
|
||||
}
|
||||
mapRendererFpsLog("requested=$requested applied=${param.arg<Int>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param ->
|
||||
if (!isMaxProfile) return@hook
|
||||
val sql = param.argNullable<String>(1) ?: return@hook
|
||||
if (!isChatFeedQuery(sql)) return@hook
|
||||
readSnapshot(chatFeedSnapshotFile)?.let { snapshot ->
|
||||
param.setResult(snapshotToMatrixCursor(snapshot))
|
||||
chatFeedCacheServeLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}")
|
||||
}
|
||||
}
|
||||
|
||||
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.AFTER) { param ->
|
||||
if (!isMaxProfile) return@hook
|
||||
val sql = param.argNullable<String>(1) ?: return@hook
|
||||
if (!isChatFeedQuery(sql)) return@hook
|
||||
val cursor = param.getResult() as? Cursor ?: return@hook
|
||||
val snapshot = snapshotFromCursor(cursor)
|
||||
writeSnapshot(chatFeedSnapshotFile, snapshot)
|
||||
param.setResult(snapshotToMatrixCursor(snapshot))
|
||||
runCatching { cursor.close() }
|
||||
chatFeedCacheRefreshLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}")
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.6
|
||||
APP_VERSION_CODE=322
|
||||
debug_build_hash=18fe2a814d0e2eb5
|
||||
psIntegrityPinnedSha256=
|
||||
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c
|
||||
|
||||
Reference in New Issue
Block a user