Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82db6a863 | ||
|
|
9781008b2e | ||
|
|
c71e06095d | ||
|
|
1fdaf6d55d | ||
|
|
8a9258b318 | ||
|
|
6491288513 |
@@ -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 (?, ?, ?, ?, ?, ?)",
|
||||
|
||||
@@ -9,6 +9,9 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -43,8 +46,10 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -72,6 +77,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import me.eternal.purrfectsnap.common.ui.TopBarActionButton
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.ManagerTheme
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
@@ -80,6 +86,7 @@ import me.eternal.purrfectsnap.ui.util.Dialog
|
||||
import me.eternal.purrfectsnap.ui.util.DialogProperties
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -162,6 +169,28 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isRandomizedProfileEnabled(): Boolean {
|
||||
return context.config.root.experimental.spoof.randomizeDeviceProfile.globalState == true
|
||||
}
|
||||
|
||||
internal fun requestFreshRandomizedProfile() {
|
||||
val randomizeConfig = context.config.root.experimental.spoof.randomizeDeviceProfile
|
||||
randomizeConfig.profileGenerationToken.set(UUID.randomUUID().toString())
|
||||
randomizeConfig.currentProfileSnapshot.set("")
|
||||
}
|
||||
|
||||
internal fun getRandomizedProfileSnapshot(): String {
|
||||
context.config.load()
|
||||
return context.config.root.experimental.spoof.randomizeDeviceProfile.currentProfileSnapshot.getNullable()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: (context.translation["manager.dialogs.randomize_device_profile.empty"]
|
||||
?: "No generated profile is available yet. Enable the feature in Snapchat first.")
|
||||
}
|
||||
|
||||
internal fun isRandomizedProfileActionProperty(propertyName: String): Boolean {
|
||||
return propertyName == "generate_fresh_profile_action" || propertyName == "view_current_profile_action"
|
||||
}
|
||||
|
||||
fun navigateToMainRoot() {
|
||||
routes.navController.navigate(routeInfo.id, NavOptions.Builder()
|
||||
.setPopUpTo(routes.navController.graph.findStartDestination().id, false)
|
||||
@@ -330,9 +359,17 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PropertyAction(property: PropertyPair<*>, registerClickCallback: ( () -> Unit ) -> (() -> Unit)) {
|
||||
internal fun PropertyAction(
|
||||
property: PropertyPair<*>,
|
||||
onConfigChanged: () -> Unit,
|
||||
registerClickCallback: (() -> Unit) -> (() -> Unit)
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var dialogComposable by remember { mutableStateOf<@Composable () -> Unit>({}) }
|
||||
var showRandomProfileProgressDialog by remember { mutableStateOf(false) }
|
||||
var randomProfileStatus by remember { mutableStateOf("") }
|
||||
var showCurrentRandomProfileDialog by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun registerDialogOnClickCallback() = registerClickCallback { showDialog = true }
|
||||
|
||||
@@ -348,7 +385,64 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
val propertyValue = property.value
|
||||
fun persistConfig() = context.config.writeConfig()
|
||||
val randomProfileEnabled = isRandomizedProfileEnabled()
|
||||
val isRandomizedProfileContainer = property.name == "randomize_device_profile"
|
||||
fun persistConfig() {
|
||||
context.config.writeConfig()
|
||||
onConfigChanged()
|
||||
}
|
||||
|
||||
if (showRandomProfileProgressDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = {},
|
||||
title = context.translation["manager.dialogs.randomize_device_profile.title"]
|
||||
?: "Generating random device profile",
|
||||
text = randomProfileStatus,
|
||||
icon = Icons.Filled.AutoAwesome,
|
||||
confirmButtonText = "",
|
||||
onConfirm = {},
|
||||
loading = true,
|
||||
showIcon = false,
|
||||
showCloseButton = false,
|
||||
confirmEnabled = false
|
||||
)
|
||||
}
|
||||
|
||||
if (showCurrentRandomProfileDialog) {
|
||||
val profileSnapshot = getRandomizedProfileSnapshot()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showCurrentRandomProfileDialog = false },
|
||||
title = context.translation["manager.dialogs.randomize_device_profile.view_title"]
|
||||
?: "Current randomized profile",
|
||||
text = "",
|
||||
icon = Icons.Filled.Visibility,
|
||||
dismissButtonText = context.translation["button.copy"] ?: "Copy",
|
||||
onDismiss = {
|
||||
clipboardManager.setText(AnnotatedString(profileSnapshot))
|
||||
context.shortToast(
|
||||
context.translation["manager.dialogs.randomize_device_profile.copied"]
|
||||
?: "Randomized profile copied"
|
||||
)
|
||||
},
|
||||
confirmButtonText = context.translation["button.positive"],
|
||||
onConfirm = { showCurrentRandomProfileDialog = false },
|
||||
showCloseButton = false,
|
||||
customContent = {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = profileSnapshot,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 360.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
color = PurrfectPalette.textSecondary,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (property.key.params.flags.contains(ConfigFlag.USER_IMPORT)) {
|
||||
registerDialogOnClickCallback()
|
||||
@@ -518,12 +612,12 @@ class FeaturesRootSection : Routes.Route() {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = state,
|
||||
onCheckedChange = {
|
||||
onCheckedChange = { requestedState ->
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
state = state.not()
|
||||
propertyValue.setAny(state)
|
||||
state = requestedState
|
||||
propertyValue.setAny(requestedState)
|
||||
persistConfig()
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
@@ -566,6 +660,45 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
||||
if (dataType == DataProcessors.Type.STRING && isRandomizedProfileActionProperty(property.name)) {
|
||||
val actionLabel = when (property.name) {
|
||||
"generate_fresh_profile_action" -> context.translation[property.key.propertyName()] ?: "Generate Fresh Profile"
|
||||
"view_current_profile_action" -> context.translation[property.key.propertyName()] ?: "View Current Profile"
|
||||
else -> property.name
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (property.name == "generate_fresh_profile_action") {
|
||||
showRandomProfileProgressDialog = true
|
||||
coroutineScope.launch {
|
||||
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.allocating"]
|
||||
?: "Allocating a randomized device fingerprint"
|
||||
delay(260)
|
||||
requestFreshRandomizedProfile()
|
||||
persistConfig()
|
||||
randomProfileStatus = context.translation["manager.dialogs.randomize_device_profile.phase.finalizing"]
|
||||
?: "Finalizing the all-in-one profile and disabling manual overrides"
|
||||
delay(260)
|
||||
showRandomProfileProgressDialog = false
|
||||
context.shortToast(
|
||||
context.translation["manager.dialogs.randomize_device_profile.refresh_requested"]
|
||||
?: "Fresh randomized profile requested. Restart Snapchat to apply it."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showCurrentRandomProfileDialog = true
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(actionLabel, maxLines = 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dialogComposable = {
|
||||
when (dataType) {
|
||||
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
|
||||
@@ -573,8 +706,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 +797,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 +882,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 +901,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 +919,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 +1019,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 +1573,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 +1647,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 +1807,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.0").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("310").get().toInt())
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.2").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("314").get().toInt())
|
||||
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
|
||||
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
|
||||
// Include version code so each release has a different hash; use random for uniqueness within same version.
|
||||
|
||||
@@ -1,3 +1,110 @@
|
||||
## 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
|
||||
- Fix: Auto Reactions when downloading stories(Removed story thumbnail feature)
|
||||
|
||||
## v1.6.0
|
||||
- New: Conversation Sound Effects!(Sound when you send or receive a msg while in chat)
|
||||
- New: Call Metadata Notifier!
|
||||
|
||||
@@ -532,6 +532,18 @@
|
||||
"title": "Export Sensitive Data?",
|
||||
"content": "Do you want to export the config with sensitive data? (Such as location coordinates, etc.)"
|
||||
},
|
||||
"randomize_device_profile": {
|
||||
"title": "Generating random device profile",
|
||||
"done": "Randomized device profile generated",
|
||||
"view_title": "Current randomized profile",
|
||||
"empty": "No generated profile is available yet. Enable the feature in Snapchat first.",
|
||||
"refresh_requested": "Fresh randomized profile requested. Restart Snapchat to apply it.",
|
||||
"phase": {
|
||||
"allocating": "Allocating a randomized device fingerprint",
|
||||
"network": "Preparing network, locale, and telephony values",
|
||||
"finalizing": "Finalizing the all-in-one profile and disabling manual overrides"
|
||||
}
|
||||
},
|
||||
"messaging_action": {
|
||||
"title": "Choose content types to process",
|
||||
"select_all_button": "Select All"
|
||||
@@ -898,7 +910,8 @@
|
||||
"notices": {
|
||||
"unstable": "\u26a0 Unstable",
|
||||
"ban_risk": "\u26a0 This feature may cause bans",
|
||||
"internal_behavior": "\u26a0 This may break Snapchat internal behaviour"
|
||||
"internal_behavior": "\u26a0 This may break Snapchat internal behaviour",
|
||||
"randomize_device_profile_override": "Controlled by Randomized Device Profile"
|
||||
},
|
||||
"properties": {
|
||||
"downloader": {
|
||||
@@ -1174,6 +1187,14 @@
|
||||
"name": "Hide Bitmoji Presence",
|
||||
"description": "Prevents your Bitmoji from popping up while in Chat"
|
||||
},
|
||||
"spoof_viewing_gallery_presence": {
|
||||
"name": "Spoof Viewing Gallery Presence",
|
||||
"description": "Keeps your Bitmoji visible in Chat while viewing chat media"
|
||||
},
|
||||
"spoof_reply_camera_presence": {
|
||||
"name": "Spoof Reply Camera Presence",
|
||||
"description": "Keeps your Bitmoji visible in Chat while using the reply camera"
|
||||
},
|
||||
"hide_typing_notifications": {
|
||||
"name": "Hide Typing Notifications",
|
||||
"description": "Prevents anyone from knowing you're typing a message"
|
||||
@@ -2013,6 +2034,32 @@
|
||||
"name": "Force Wi-Fi Transport Flag",
|
||||
"description": "Force network transport to report Wi-Fi instead of mobile data"
|
||||
},
|
||||
"randomize_device_profile": {
|
||||
"name": "Randomized Device Profile",
|
||||
"description": "Generate and apply a full randomized device, network, locale, and settings profile in one restart-safe profile",
|
||||
"properties": {
|
||||
"show_activation_overlay": {
|
||||
"name": "Show Activation Overlay",
|
||||
"description": "Show the in-app toast when the randomized profile becomes active"
|
||||
},
|
||||
"randomize_ip_address": {
|
||||
"name": "Randomize IP Address",
|
||||
"description": "Generate and spoof a randomized IP address whenever a fresh randomized profile is created"
|
||||
},
|
||||
"persistent_app_language": {
|
||||
"name": "Persistent App Language",
|
||||
"description": "Force Snapchat to stay on a specific supported app language"
|
||||
},
|
||||
"generate_fresh_profile_action": {
|
||||
"name": "Generate Fresh Profile",
|
||||
"description": "Request a newly generated randomized profile"
|
||||
},
|
||||
"view_current_profile_action": {
|
||||
"name": "View Current Profile",
|
||||
"description": "Inspect the latest randomized profile snapshot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"description": "Override the Android ID sent to Snapchat",
|
||||
@@ -2348,7 +2395,10 @@
|
||||
"custom_android_id": {
|
||||
"null": "Use real Android ID"
|
||||
},
|
||||
"add_friend_source_spoof": {
|
||||
"persistent_app_language": {
|
||||
"system_default": "System Default"
|
||||
},
|
||||
"add_friend_source_spoof": {
|
||||
"added_by_username": "By Username",
|
||||
"added_by_mention": "By Mention",
|
||||
"added_by_group_chat": "By Group Chat",
|
||||
@@ -2965,6 +3015,10 @@
|
||||
"stopped_speaking": "Stopped Speaking",
|
||||
"started_peeking": "Started Peeking",
|
||||
"stopped_peeking": "Stopped Peeking",
|
||||
"started_using_reply_camera": "Started Using Reply Camera",
|
||||
"stopped_using_reply_camera": "Stopped Using Reply Camera",
|
||||
"started_viewing_chat_media": "Started Viewing Chat Media",
|
||||
"stopped_viewing_chat_media": "Stopped Viewing Chat Media",
|
||||
"message_read": "Message Read",
|
||||
"message_deleted": "Message Deleted",
|
||||
"message_saved": "Message Saved",
|
||||
@@ -2977,7 +3031,9 @@
|
||||
"snap_replayed_twice": "Snap Replayed Twice",
|
||||
"snap_screenshot": "Snap Screenshot",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"i_can_see_you": "I Can See You"
|
||||
"i_can_see_you": "I Can See You",
|
||||
"i_can_see_you_2": "I Can See You 2",
|
||||
"i_can_see_you_3": "I Can See You 3"
|
||||
},
|
||||
"cleared_from_feed": "Cleared from feed",
|
||||
"tracker_actions": {
|
||||
@@ -3186,6 +3242,10 @@
|
||||
"stopped_speaking": "{friend} stopped speaking in {conversation}",
|
||||
"started_peeking": "{friend} started peeking in {conversation}",
|
||||
"stopped_peeking": "{friend} stopped peeking in {conversation}",
|
||||
"started_using_reply_camera": "{friend} opened the reply camera in {conversation}",
|
||||
"stopped_using_reply_camera": "{friend} closed the reply camera in {conversation}",
|
||||
"started_viewing_chat_media": "{friend} started viewing chat media in {conversation}",
|
||||
"stopped_viewing_chat_media": "{friend} stopped viewing chat media in {conversation}",
|
||||
"message_read": "{friend} read a message in {conversation}",
|
||||
"message_deleted": "{friend} deleted a message in {conversation}",
|
||||
"message_saved": "{friend} saved a message in {conversation}",
|
||||
@@ -3198,7 +3258,9 @@
|
||||
"snap_replayed_twice": "{friend} replayed a snap twice in {conversation}",
|
||||
"snap_screenshot": "{friend} took a screenshot in {conversation}",
|
||||
"snap_screen_record": "{friend} screen recorded in {conversation}",
|
||||
"i_can_see_you": "{friend} activity in {conversation}: {details}"
|
||||
"i_can_see_you": "{friend} activity in {conversation}: {details}",
|
||||
"i_can_see_you_2": "{friend} gallery activity in {conversation}: {details}",
|
||||
"i_can_see_you_3": "{friend} reply camera activity in {conversation}: {details}"
|
||||
},
|
||||
"friend_mutation_observer": {
|
||||
"notification_channel_name": "Friend Mutation Observer",
|
||||
@@ -3351,6 +3413,10 @@
|
||||
"stopped_speaking": "Stopped speaking",
|
||||
"started_peeking": "Started peeking",
|
||||
"stopped_peeking": "Stopped peeking",
|
||||
"started_using_reply_camera": "Opened reply camera",
|
||||
"stopped_using_reply_camera": "Closed reply camera",
|
||||
"started_viewing_chat_media": "Started viewing chat media",
|
||||
"stopped_viewing_chat_media": "Stopped viewing chat media",
|
||||
"message_read": "Read message",
|
||||
"message_deleted": "Deleted message",
|
||||
"message_saved": "Saved message",
|
||||
@@ -3402,6 +3468,10 @@
|
||||
"stopped_speaking": "stopped speaking",
|
||||
"started_peeking": "started peeking",
|
||||
"stopped_peeking": "stopped peeking",
|
||||
"started_using_reply_camera": "opened the reply camera",
|
||||
"stopped_using_reply_camera": "closed the reply camera",
|
||||
"started_viewing_chat_media": "started viewing chat media",
|
||||
"stopped_viewing_chat_media": "stopped viewing chat media",
|
||||
"message_read": "read a message",
|
||||
"message_deleted": "deleted a message",
|
||||
"message_saved": "saved a message",
|
||||
@@ -3414,7 +3484,9 @@
|
||||
"snap_replayed_twice": "replayed a snap twice",
|
||||
"snap_screenshot": "took a screenshot",
|
||||
"snap_screen_record": "screen recorded",
|
||||
"i_can_see_you": "was active"
|
||||
"i_can_see_you": "was active",
|
||||
"i_can_see_you_2": "was viewing gallery",
|
||||
"i_can_see_you_3": "was using the reply camera"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3474,6 +3546,7 @@
|
||||
"disable_feature_loading_label": "Disable Feature Loading",
|
||||
"disable_auto_mapper_label": "Disable Auto Mapper",
|
||||
"disable_bypass_indicator_label": "Disable Bypass Indicator",
|
||||
"disable_cant_login_button_label": "Disable Can't Login Button",
|
||||
"friend_list": {
|
||||
"manage_title": "Manage Friend List",
|
||||
"export_description": "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.",
|
||||
|
||||
@@ -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"
|
||||
@@ -1300,10 +1321,6 @@
|
||||
"name": "Call Metadata Notifier",
|
||||
"description": "Shows a notification with captured call metadata after the call ends"
|
||||
},
|
||||
"conversation_sound_effects": {
|
||||
"name": "Conversation Sound Effects",
|
||||
"description": "Plays send and receive sounds inside an open conversation"
|
||||
},
|
||||
"conversation_sound_effects_style": {
|
||||
"name": "Conversation Sound Style",
|
||||
"description": "Choose the sound style used for in-conversation send and receive sounds"
|
||||
@@ -1665,10 +1682,26 @@
|
||||
"name": "Auto Open Compact Notification",
|
||||
"description": "Use a smaller, single-line notification for status updates"
|
||||
},
|
||||
"show_progress_bar": {
|
||||
"name": "Show Progress Bar",
|
||||
"description": "Display a visual progress bar in the status notification"
|
||||
},
|
||||
"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)"
|
||||
},
|
||||
"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" },
|
||||
"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." }
|
||||
@@ -2123,6 +2156,418 @@
|
||||
"name": "Force Wi-Fi Transport Flag",
|
||||
"description": "Force network transport to report Wi-Fi instead of mobile data"
|
||||
},
|
||||
"randomize_device_profile": {
|
||||
"name": "Randomized Device Profile",
|
||||
"description": "Generate and apply a full randomized device, network, locale, and settings profile",
|
||||
"properties": {
|
||||
"show_activation_overlay": {
|
||||
"name": "Show Activation Overlay",
|
||||
"description": "Show the in-app toast when the randomized profile becomes active"
|
||||
},
|
||||
"randomize_ip_address": {
|
||||
"name": "Randomize IP Address",
|
||||
"description": "Generate and spoof a randomized IP address whenever a fresh randomized profile is created"
|
||||
},
|
||||
"spoof_build_properties": {
|
||||
"name": "Spoof Build Properties",
|
||||
"description": "Apply randomized build fields, fingerprints, and device property values"
|
||||
},
|
||||
"build_properties": {
|
||||
"name": "Build Properties",
|
||||
"description": "Enable build property spoofing and fine-tune its subsets",
|
||||
"properties": {
|
||||
"device_identity": {
|
||||
"name": "Device Identity",
|
||||
"description": "Enable device identity spoofing and fine-tune its values",
|
||||
"properties": {
|
||||
"manufacturer_model": {
|
||||
"name": "Manufacturer And Model",
|
||||
"description": "Randomize the reported manufacturer and model"
|
||||
},
|
||||
"brand_product": {
|
||||
"name": "Brand And Product",
|
||||
"description": "Randomize the reported brand, device, and product values"
|
||||
},
|
||||
"hardware_board": {
|
||||
"name": "Hardware And Board",
|
||||
"description": "Randomize the reported hardware and board values"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build_version": {
|
||||
"name": "Build Version",
|
||||
"description": "Enable build version spoofing and fine-tune its values",
|
||||
"properties": {
|
||||
"fingerprint": {
|
||||
"name": "Fingerprint",
|
||||
"description": "Randomize the reported build fingerprint"
|
||||
},
|
||||
"display": {
|
||||
"name": "Display ID",
|
||||
"description": "Randomize the reported build display ID"
|
||||
},
|
||||
"host": {
|
||||
"name": "Host",
|
||||
"description": "Randomize the reported build host"
|
||||
},
|
||||
"bootloader": {
|
||||
"name": "Bootloader",
|
||||
"description": "Randomize the reported bootloader value"
|
||||
},
|
||||
"build_time": {
|
||||
"name": "Build Time",
|
||||
"description": "Randomize the reported build timestamp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abi_lists": {
|
||||
"name": "ABI Lists",
|
||||
"description": "Enable ABI spoofing and fine-tune its values",
|
||||
"properties": {
|
||||
"combined_abis": {
|
||||
"name": "Combined ABI List",
|
||||
"description": "Randomize the combined supported ABI list"
|
||||
},
|
||||
"split_abis": {
|
||||
"name": "32-bit And 64-bit ABI Lists",
|
||||
"description": "Randomize the split 32-bit and 64-bit ABI lists"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_properties": {
|
||||
"name": "System Properties",
|
||||
"description": "Expose randomized values through Android system property lookups",
|
||||
"properties": {
|
||||
"build": {
|
||||
"name": "Build Properties",
|
||||
"description": "Expose randomized build values through system properties"
|
||||
},
|
||||
"locale": {
|
||||
"name": "Locale Properties",
|
||||
"description": "Expose randomized locale values through system properties"
|
||||
},
|
||||
"telephony": {
|
||||
"name": "Telephony Properties",
|
||||
"description": "Expose randomized telephony values through system properties"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_locale": {
|
||||
"name": "Spoof Locale",
|
||||
"description": "Apply the randomized locale and language hooks"
|
||||
},
|
||||
"locale_options": {
|
||||
"name": "Locale Details",
|
||||
"description": "Enable locale spoofing and fine-tune its subsets",
|
||||
"properties": {
|
||||
"locale": {
|
||||
"name": "Locale",
|
||||
"description": "Enable locale spoofing and fine-tune language and region values",
|
||||
"properties": {
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"description": "Randomize the reported language value"
|
||||
},
|
||||
"region": {
|
||||
"name": "Region",
|
||||
"description": "Randomize the reported region value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"name": "Time",
|
||||
"description": "Enable time spoofing and fine-tune time-related values",
|
||||
"properties": {
|
||||
"time_zone_id": {
|
||||
"name": "Time Zone ID",
|
||||
"description": "Randomize the reported time zone ID"
|
||||
},
|
||||
"time_zone_display_name": {
|
||||
"name": "Time Zone Display Name",
|
||||
"description": "Randomize the reported time zone display name"
|
||||
},
|
||||
"auto_time": {
|
||||
"name": "Auto Time",
|
||||
"description": "Randomize the global auto-time setting"
|
||||
},
|
||||
"auto_time_zone": {
|
||||
"name": "Auto Time Zone",
|
||||
"description": "Randomize the global auto-time-zone setting"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_telephony": {
|
||||
"name": "Spoof Telephony",
|
||||
"description": "Apply randomized carrier, SIM, and phone capability values"
|
||||
},
|
||||
"telephony_options": {
|
||||
"name": "Telephony Details",
|
||||
"description": "Enable telephony spoofing and fine-tune its subsets",
|
||||
"properties": {
|
||||
"mms": {
|
||||
"name": "MMS",
|
||||
"description": "Enable MMS spoofing and fine-tune MMS values",
|
||||
"properties": {
|
||||
"user_agent": {
|
||||
"name": "User Agent",
|
||||
"description": "Randomize the MMS user agent string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_identity": {
|
||||
"name": "Network Identity",
|
||||
"description": "Enable network identity spoofing and fine-tune network values",
|
||||
"properties": {
|
||||
"network_type": {
|
||||
"name": "Network Type",
|
||||
"description": "Randomize the reported network type"
|
||||
},
|
||||
"operator_numeric": {
|
||||
"name": "Operator Numeric",
|
||||
"description": "Randomize the reported operator numeric code"
|
||||
},
|
||||
"operator_name": {
|
||||
"name": "Operator Name",
|
||||
"description": "Randomize the reported operator name"
|
||||
},
|
||||
"country_iso": {
|
||||
"name": "Country ISO",
|
||||
"description": "Randomize the reported network country ISO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sim_identity": {
|
||||
"name": "SIM Identity",
|
||||
"description": "Enable SIM identity spoofing and fine-tune SIM values",
|
||||
"properties": {
|
||||
"country_iso": {
|
||||
"name": "Country ISO",
|
||||
"description": "Randomize the reported SIM country ISO"
|
||||
},
|
||||
"operator_numeric": {
|
||||
"name": "Operator Numeric",
|
||||
"description": "Randomize the reported SIM operator numeric code"
|
||||
},
|
||||
"operator_name": {
|
||||
"name": "Operator Name",
|
||||
"description": "Randomize the reported SIM operator name"
|
||||
},
|
||||
"sim_state": {
|
||||
"name": "SIM State",
|
||||
"description": "Randomize the reported SIM state"
|
||||
},
|
||||
"has_icc_card": {
|
||||
"name": "ICC Card",
|
||||
"description": "Randomize whether a SIM card is reported as present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"phone_capabilities": {
|
||||
"name": "Phone Capabilities",
|
||||
"description": "Enable phone capability spoofing and fine-tune capability values",
|
||||
"properties": {
|
||||
"phone_count": {
|
||||
"name": "Phone Count",
|
||||
"description": "Randomize the reported phone count"
|
||||
},
|
||||
"hearing_aid": {
|
||||
"name": "Hearing Aid",
|
||||
"description": "Randomize hearing aid compatibility support"
|
||||
},
|
||||
"tty": {
|
||||
"name": "TTY",
|
||||
"description": "Randomize TTY support"
|
||||
},
|
||||
"world_phone": {
|
||||
"name": "World Phone",
|
||||
"description": "Randomize world phone support"
|
||||
},
|
||||
"roaming": {
|
||||
"name": "Roaming",
|
||||
"description": "Randomize roaming status"
|
||||
},
|
||||
"sms_voice": {
|
||||
"name": "SMS And Voice",
|
||||
"description": "Randomize SMS and voice capability support"
|
||||
},
|
||||
"phone_type": {
|
||||
"name": "Phone Type",
|
||||
"description": "Randomize the reported phone type"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_settings": {
|
||||
"name": "Spoof Settings",
|
||||
"description": "Apply the randomized Android settings overrides"
|
||||
},
|
||||
"settings_options": {
|
||||
"name": "Settings Details",
|
||||
"description": "Enable settings spoofing and fine-tune its namespaces",
|
||||
"properties": {
|
||||
"secure": {
|
||||
"name": "Secure Settings",
|
||||
"description": "Enable secure settings spoofing and fine-tune secure values",
|
||||
"properties": {
|
||||
"base": {
|
||||
"name": "Base",
|
||||
"description": "Randomize the base secure settings values"
|
||||
},
|
||||
"tts": {
|
||||
"name": "Text To Speech",
|
||||
"description": "Randomize text-to-speech secure settings values"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"name": "System Settings",
|
||||
"description": "Enable system settings spoofing and fine-tune system values",
|
||||
"properties": {
|
||||
"base": {
|
||||
"name": "Base",
|
||||
"description": "Randomize the base system settings values"
|
||||
},
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth",
|
||||
"description": "Randomize Bluetooth-related system settings values"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"name": "Global Settings",
|
||||
"description": "Enable global settings spoofing and fine-tune global values",
|
||||
"properties": {
|
||||
"base": {
|
||||
"name": "Base",
|
||||
"description": "Randomize the base global settings values"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_network": {
|
||||
"name": "Spoof Network",
|
||||
"description": "Apply randomized Wi-Fi and DNS network values"
|
||||
},
|
||||
"network_options": {
|
||||
"name": "Network Details",
|
||||
"description": "Enable network spoofing and fine-tune its subsets",
|
||||
"properties": {
|
||||
"wifi": {
|
||||
"name": "Wi-Fi Info",
|
||||
"description": "Enable Wi-Fi spoofing and fine-tune Wi-Fi values",
|
||||
"properties": {
|
||||
"ssid": {
|
||||
"name": "SSID",
|
||||
"description": "Randomize the reported Wi-Fi SSID"
|
||||
},
|
||||
"rssi": {
|
||||
"name": "Signal Strength",
|
||||
"description": "Randomize the reported Wi-Fi RSSI value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"name": "DNS",
|
||||
"description": "Enable DNS spoofing and fine-tune DNS values",
|
||||
"properties": {
|
||||
"servers": {
|
||||
"name": "Servers",
|
||||
"description": "Randomize the reported DNS server list"
|
||||
},
|
||||
"search_domains": {
|
||||
"name": "Search Domains",
|
||||
"description": "Randomize the reported DNS search domains"
|
||||
},
|
||||
"private_dns": {
|
||||
"name": "Private DNS",
|
||||
"description": "Randomize the reported private DNS values"
|
||||
}
|
||||
}
|
||||
},
|
||||
"captive_portal": {
|
||||
"name": "Captive Portal",
|
||||
"description": "Enable captive portal spoofing and fine-tune portal values",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"name": "Capability",
|
||||
"description": "Randomize the reported captive portal capability"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_identifiers": {
|
||||
"name": "Spoof Identifiers",
|
||||
"description": "Apply randomized Android ID, advertising ID, and hardware address overrides"
|
||||
},
|
||||
"identifier_options": {
|
||||
"name": "Identifier Details",
|
||||
"description": "Enable identifier spoofing and fine-tune its subsets",
|
||||
"properties": {
|
||||
"android_id": {
|
||||
"name": "Android ID",
|
||||
"description": "Enable Android ID spoofing and fine-tune Android ID values",
|
||||
"properties": {
|
||||
"string_value": {
|
||||
"name": "String Value",
|
||||
"description": "Randomize the string Android ID value"
|
||||
},
|
||||
"long_value": {
|
||||
"name": "Long Value",
|
||||
"description": "Randomize the long Android ID value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advertising_id": {
|
||||
"name": "Advertising ID",
|
||||
"description": "Enable advertising ID spoofing and fine-tune advertising ID values",
|
||||
"properties": {
|
||||
"settings_value": {
|
||||
"name": "Settings Value",
|
||||
"description": "Randomize the advertising ID returned through settings"
|
||||
},
|
||||
"play_services": {
|
||||
"name": "Play Services",
|
||||
"description": "Randomize the advertising ID returned through Play Services"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hardware_addresses": {
|
||||
"name": "Hardware Addresses",
|
||||
"description": "Enable hardware address spoofing and fine-tune address values",
|
||||
"properties": {
|
||||
"wifi_mac": {
|
||||
"name": "Wi-Fi MAC",
|
||||
"description": "Randomize the reported Wi-Fi MAC address"
|
||||
},
|
||||
"bluetooth_mac": {
|
||||
"name": "Bluetooth MAC",
|
||||
"description": "Randomize the reported Bluetooth MAC address"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"persistent_app_language": {
|
||||
"name": "Persistent App Language",
|
||||
"description": "Force Snapchat to stay on a specific supported app language"
|
||||
},
|
||||
"generate_fresh_profile_action": {
|
||||
"name": "Generate Fresh Profile",
|
||||
"description": "Request a newly generated randomized profile"
|
||||
},
|
||||
"view_current_profile_action": {
|
||||
"name": "View Current Profile",
|
||||
"description": "Inspect the latest randomized profile snapshot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"description": "Override the Android ID sent to Snapchat",
|
||||
@@ -2169,7 +2614,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": {
|
||||
@@ -2475,6 +2920,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",
|
||||
@@ -2529,6 +2977,7 @@
|
||||
"null": "Device default FPS"
|
||||
},
|
||||
"conversation_sound_effects_style": {
|
||||
"disabled": "Disabled",
|
||||
"imessage": "iMessage",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
@@ -3088,6 +3537,7 @@
|
||||
"positive": "Yes",
|
||||
"negative": "No",
|
||||
"cancel": "Cancel",
|
||||
"copy": "Copy",
|
||||
"save": "Save",
|
||||
"open": "Open",
|
||||
"download": "Download",
|
||||
@@ -3105,6 +3555,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",
|
||||
@@ -3117,7 +3571,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": {
|
||||
@@ -3260,6 +3716,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",
|
||||
@@ -3280,6 +3741,16 @@
|
||||
"status_paused": "Paused",
|
||||
"status_monitoring": "Monitoring",
|
||||
"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",
|
||||
@@ -3353,6 +3824,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}",
|
||||
@@ -3365,7 +3840,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",
|
||||
@@ -3525,9 +4002,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",
|
||||
@@ -3579,6 +4060,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",
|
||||
@@ -3591,7 +4076,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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3651,6 +4138,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.",
|
||||
@@ -3689,17 +4177,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",
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -176,12 +176,19 @@ class MessagingTweaks : ConfigContainer() {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null }
|
||||
}
|
||||
val compactNotification = boolean("compact_notification", false)
|
||||
val showProgressBar = boolean("show_progress_bar", true)
|
||||
val showLifetimeStats = boolean("show_lifetime_stats", false)
|
||||
val showQueuePreview = boolean("show_queue_preview", true)
|
||||
|
||||
// Resource Intelligence: Smart triggers for battery and data safety
|
||||
val onlyOnWifi = boolean("only_on_wifi", false)
|
||||
val onlyWhenIdle = boolean("only_when_idle", 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 +214,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() }
|
||||
@@ -224,12 +233,12 @@ class MessagingTweaks : ConfigContainer() {
|
||||
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
|
||||
val blockCalls = boolean("block_calls") { requireRestart() }
|
||||
val callMetadataNotifier = boolean("call_metadata_notifier") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
|
||||
val conversationSoundEffects = boolean("conversation_sound_effects") { requireRestart() }
|
||||
val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "imessage", "telegram", "whatsapp", "subtle") {
|
||||
val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "disabled", "imessage", "telegram", "whatsapp", "subtle") {
|
||||
requireRestart()
|
||||
customOptionTranslationPath = "conversation_sound_effects_style"
|
||||
addFlags(ConfigFlag.NO_TRANSLATE)
|
||||
}.apply { set("imessage") }
|
||||
addFlags(ConfigFlag.NO_DISABLE_KEY)
|
||||
}.apply { set("disabled") }
|
||||
val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() }
|
||||
val disableSnapModeRestrictions = boolean("disable_snap_mode_restrictions") { requireRestart() }
|
||||
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",
|
||||
@@ -324,4 +333,3 @@ class MessagingTweaks : ConfigContainer() {
|
||||
|
||||
val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,193 @@ import me.eternal.purrfectsnap.common.config.ConfigContainer
|
||||
import me.eternal.purrfectsnap.common.config.ConfigFlag
|
||||
|
||||
class Spoof : ConfigContainer(hasGlobalState = true) {
|
||||
companion object {
|
||||
val supportedSnapchatLanguages = listOf(
|
||||
"ar", "bn", "bn-BD", "bn-IN", "da", "de", "el", "en-GB", "es", "es-AR", "es-ES", "es-MX",
|
||||
"fi", "fil", "fil-PH", "fr", "gu", "gu-IN", "hi", "hi-IN", "in", "it", "ja", "kn", "kn-IN",
|
||||
"ko", "ml", "ml-IN", "mr", "mr-IN", "ms", "ms-MY", "nb", "nl", "pa", "pa-IN", "pl", "pt",
|
||||
"pt-PT", "ro", "ru", "sv", "ta", "ta-IN", "te", "te-IN", "th", "th-TH", "tr", "ur", "ur-PK",
|
||||
"vi", "vi-VN", "zh", "zh-CN", "zh-TW"
|
||||
)
|
||||
}
|
||||
|
||||
inner class RandomizedDeviceProfileConfig : ConfigContainer(hasGlobalState = true) {
|
||||
inner class RandomizedBuildVersionConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val fingerprint = boolean("fingerprint", defaultValue = true) { requireRestart() }
|
||||
val display = boolean("display", defaultValue = true) { requireRestart() }
|
||||
val host = boolean("host", defaultValue = true) { requireRestart() }
|
||||
val bootloader = boolean("bootloader", defaultValue = true) { requireRestart() }
|
||||
val buildTime = boolean("build_time", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedDeviceIdentityConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val manufacturerModel = boolean("manufacturer_model", defaultValue = true) { requireRestart() }
|
||||
val brandProduct = boolean("brand_product", defaultValue = true) { requireRestart() }
|
||||
val hardwareBoard = boolean("hardware_board", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedAbiListsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val combinedAbis = boolean("combined_abis", defaultValue = true) { requireRestart() }
|
||||
val splitAbis = boolean("split_abis", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedSystemPropertiesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val build = boolean("build", defaultValue = true) { requireRestart() }
|
||||
val locale = boolean("locale", defaultValue = true) { requireRestart() }
|
||||
val telephony = boolean("telephony", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedBuildPropertiesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val deviceIdentity = container("device_identity", RandomizedDeviceIdentityConfig().apply { globalState = true })
|
||||
val abiLists = container("abi_lists", RandomizedAbiListsConfig().apply { globalState = true })
|
||||
val systemProperties = container("system_properties", RandomizedSystemPropertiesConfig().apply { globalState = true })
|
||||
val buildVersion = container("build_version", RandomizedBuildVersionConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
inner class RandomizedLocaleValueConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val language = boolean("language", defaultValue = true) { requireRestart() }
|
||||
val region = boolean("region", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedTimeConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val timeZoneId = boolean("time_zone_id", defaultValue = false) { requireRestart() }
|
||||
val timeZoneDisplayName = boolean("time_zone_display_name", defaultValue = false) { requireRestart() }
|
||||
val autoTime = boolean("auto_time", defaultValue = false) { requireRestart() }
|
||||
val autoTimeZone = boolean("auto_time_zone", defaultValue = false) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedLocaleConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val locale = container("locale", RandomizedLocaleValueConfig().apply { globalState = true })
|
||||
val time = container("time", RandomizedTimeConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
inner class RandomizedMmsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val userAgent = boolean("user_agent", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedNetworkIdentityConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val networkType = boolean("network_type", defaultValue = true) { requireRestart() }
|
||||
val operatorNumeric = boolean("operator_numeric", defaultValue = true) { requireRestart() }
|
||||
val operatorName = boolean("operator_name", defaultValue = true) { requireRestart() }
|
||||
val countryIso = boolean("country_iso", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedSimIdentityConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val countryIso = boolean("country_iso", defaultValue = true) { requireRestart() }
|
||||
val operatorNumeric = boolean("operator_numeric", defaultValue = true) { requireRestart() }
|
||||
val operatorName = boolean("operator_name", defaultValue = true) { requireRestart() }
|
||||
val simState = boolean("sim_state", defaultValue = true) { requireRestart() }
|
||||
val hasIccCard = boolean("has_icc_card", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedPhoneCapabilitiesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val phoneCount = boolean("phone_count", defaultValue = true) { requireRestart() }
|
||||
val hearingAid = boolean("hearing_aid", defaultValue = true) { requireRestart() }
|
||||
val tty = boolean("tty", defaultValue = true) { requireRestart() }
|
||||
val worldPhone = boolean("world_phone", defaultValue = true) { requireRestart() }
|
||||
val roaming = boolean("roaming", defaultValue = true) { requireRestart() }
|
||||
val smsVoice = boolean("sms_voice", defaultValue = true) { requireRestart() }
|
||||
val phoneType = boolean("phone_type", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedTelephonyConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val mms = container("mms", RandomizedMmsConfig().apply { globalState = true })
|
||||
val networkIdentity = container("network_identity", RandomizedNetworkIdentityConfig().apply { globalState = true })
|
||||
val simIdentity = container("sim_identity", RandomizedSimIdentityConfig().apply { globalState = true })
|
||||
val phoneCapabilities = container("phone_capabilities", RandomizedPhoneCapabilitiesConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
inner class RandomizedSecureSettingsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val base = boolean("base", defaultValue = true) { requireRestart() }
|
||||
val tts = boolean("tts", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedSystemSettingsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val base = boolean("base", defaultValue = true) { requireRestart() }
|
||||
val bluetooth = boolean("bluetooth", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedGlobalSettingsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val base = boolean("base", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedSettingsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val secure = container("secure", RandomizedSecureSettingsConfig().apply { globalState = true })
|
||||
val system = container("system", RandomizedSystemSettingsConfig().apply { globalState = true })
|
||||
val global = container("global", RandomizedGlobalSettingsConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
inner class RandomizedWifiConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val ssid = boolean("ssid", defaultValue = true) { requireRestart() }
|
||||
val rssi = boolean("rssi", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedDnsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val servers = boolean("servers", defaultValue = true) { requireRestart() }
|
||||
val searchDomains = boolean("search_domains", defaultValue = true) { requireRestart() }
|
||||
val privateDns = boolean("private_dns", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedCaptivePortalConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val capability = boolean("capability", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedNetworkConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val wifi = container("wifi", RandomizedWifiConfig().apply { globalState = true })
|
||||
val dns = container("dns", RandomizedDnsConfig().apply { globalState = true })
|
||||
val captivePortal = container("captive_portal", RandomizedCaptivePortalConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
inner class RandomizedAndroidIdConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val stringValue = boolean("string_value", defaultValue = true) { requireRestart() }
|
||||
val longValue = boolean("long_value", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedAdvertisingIdConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val settingsValue = boolean("settings_value", defaultValue = true) { requireRestart() }
|
||||
val playServices = boolean("play_services", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedHardwareAddressesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val wifiMac = boolean("wifi_mac", defaultValue = true) { requireRestart() }
|
||||
val bluetoothMac = boolean("bluetooth_mac", defaultValue = true) { requireRestart() }
|
||||
}
|
||||
|
||||
inner class RandomizedIdentifiersConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val androidId = container("android_id", RandomizedAndroidIdConfig().apply { globalState = true })
|
||||
val advertisingId = container("advertising_id", RandomizedAdvertisingIdConfig().apply { globalState = true })
|
||||
val hardwareAddresses = container("hardware_addresses", RandomizedHardwareAddressesConfig().apply { globalState = true })
|
||||
}
|
||||
|
||||
val showActivationOverlay = boolean("show_activation_overlay", defaultValue = false)
|
||||
val randomizeIpAddress = boolean("randomize_ip_address", defaultValue = false) { requireRestart() }
|
||||
val buildProperties = container("build_properties", RandomizedBuildPropertiesConfig().apply { globalState = true })
|
||||
val localeOptions = container("locale_options", RandomizedLocaleConfig().apply { globalState = true })
|
||||
val telephonyOptions = container("telephony_options", RandomizedTelephonyConfig().apply { globalState = true })
|
||||
val settingsOptions = container("settings_options", RandomizedSettingsConfig().apply { globalState = true })
|
||||
val networkOptions = container("network_options", RandomizedNetworkConfig().apply { globalState = true })
|
||||
val identifierOptions = container("identifier_options", RandomizedIdentifiersConfig().apply { globalState = true })
|
||||
val persistentAppLanguage = unique("persistent_app_language", *supportedSnapchatLanguages.toTypedArray()) {
|
||||
requireRestart()
|
||||
addFlags(ConfigFlag.NO_TRANSLATE)
|
||||
disabledKey = "system_default"
|
||||
customOptionTranslationPath = "features.options.persistent_app_language"
|
||||
}
|
||||
val generateFreshProfileAction = string("generate_fresh_profile_action")
|
||||
val viewCurrentProfileAction = string("view_current_profile_action")
|
||||
val profileGenerationToken = string("profile_generation_token") {
|
||||
addFlags(ConfigFlag.HIDDEN)
|
||||
}
|
||||
val currentProfileSnapshot = string("current_profile_snapshot") {
|
||||
addFlags(ConfigFlag.HIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SpoofDeviceIdConfig : ConfigContainer() {
|
||||
val spoofAndroidId = boolean("spoof_android_id") { requireRestart() }
|
||||
val spoofAndroidId = boolean("spoof_android_id") { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
|
||||
val customAndroidId = string("custom_android_id") {
|
||||
requireRestart()
|
||||
addFlags(ConfigFlag.HIDDEN)
|
||||
inputCheck = { it.isEmpty() || (it.length == 16 && it.all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' }) }
|
||||
}
|
||||
}
|
||||
@@ -16,8 +199,9 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
|
||||
val removeVpnTransportFlag = boolean("remove_vpn_transport_flag") { requireRestart() }
|
||||
val removeMockLocationFlag = boolean("remove_mock_location_flag") { requireRestart() }
|
||||
val forceWifiTransportFlag = boolean("force_wifi_transport_flag") { requireRestart() }
|
||||
val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart() }
|
||||
val spoofDevice = boolean("spoof_device") { requireRestart() }
|
||||
val randomizeDeviceProfile = container("randomize_device_profile", RandomizedDeviceProfileConfig()) { requireRestart() }
|
||||
val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
|
||||
val spoofDevice = boolean("spoof_device") { requireRestart(); addFlags(ConfigFlag.HIDDEN) }
|
||||
val deviceModel = unique("device_model",
|
||||
"none",
|
||||
"random",
|
||||
@@ -38,6 +222,7 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
|
||||
"realme GT 6"
|
||||
) {
|
||||
requireRestart()
|
||||
addFlags(ConfigFlag.HIDDEN)
|
||||
customOptionTranslationPath = "features.options.device_model"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -30,14 +29,11 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -45,8 +41,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -125,11 +119,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
|
||||
var lastSeenMapParams: ParamMap? = null
|
||||
private set
|
||||
private val storyPreviewCache = mutableMapOf<String, MutableMap<Int, Bitmap>>()
|
||||
@Volatile
|
||||
private var pendingBatchDownloadIndices: MutableList<Int>? = null
|
||||
@Volatile
|
||||
private var batchForceAllowDuplicate: Boolean = false
|
||||
private val translations by lazy {
|
||||
context.translation.getCategory("download_processor")
|
||||
}
|
||||
@@ -267,68 +256,10 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
val tr = context.translation.getCategory("download_processor.story_snap_dialog")
|
||||
val cancelStr = context.translation["button.cancel"]
|
||||
val downloadStr = context.translation["button.download"]
|
||||
val previewCacheKey = buildString {
|
||||
append(paramMap["STORY_ID"]?.toString() ?: "story")
|
||||
append("|")
|
||||
append(paramMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString() ?: "user")
|
||||
append("|")
|
||||
append(totalCount)
|
||||
}
|
||||
context.runOnUiThread {
|
||||
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
PurrfectOverlayTheme {
|
||||
val selected = remember { mutableStateListOf<Int>().apply { add(currentIndex) } }
|
||||
val previewBitmaps = remember { mutableStateMapOf<Int, Bitmap?>() }
|
||||
val previewLoading = remember { mutableStateMapOf<Int, Boolean>() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!selected.contains(currentIndex)) selected.add(currentIndex)
|
||||
mediaInfoMap[SplitMediaAssetType.ORIGINAL]?.uri?.let { currentUri ->
|
||||
previewLoading[currentIndex] = true
|
||||
previewBitmaps[currentIndex] = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(currentUri) }
|
||||
previewLoading[currentIndex] = false
|
||||
}
|
||||
synchronized(storyPreviewCache) {
|
||||
storyPreviewCache[previewCacheKey]?.forEach { (index, bitmap) ->
|
||||
previewBitmaps[index] = bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(previewCacheKey) {
|
||||
val overlay = context.feature(OperaStoryOverlay::class)
|
||||
val cachedIndices = synchronized(storyPreviewCache) {
|
||||
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }.keys.toSet()
|
||||
}
|
||||
val indicesToScan = (0 until totalCount).filter { it != currentIndex && it !in cachedIndices }
|
||||
|
||||
try {
|
||||
for (targetIndex in indicesToScan) {
|
||||
val jumped = withContext(Dispatchers.Main) {
|
||||
overlay.requestJumpToSnap(targetIndex, totalCount)
|
||||
}
|
||||
if (!jumped) continue
|
||||
|
||||
val reached = waitForStoryIndex(targetIndex)
|
||||
if (!reached) continue
|
||||
|
||||
val uri = lastSeenMediaInfoMap?.get(SplitMediaAssetType.ORIGINAL)?.uri ?: continue
|
||||
previewLoading[targetIndex] = true
|
||||
val bitmap = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(uri) }
|
||||
previewLoading[targetIndex] = false
|
||||
if (bitmap != null) {
|
||||
previewBitmaps[targetIndex] = bitmap
|
||||
synchronized(storyPreviewCache) {
|
||||
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }[targetIndex] = bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
overlay.requestJumpToSnap(currentIndex, totalCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PurrfectGlassCard(
|
||||
title = tr["title"],
|
||||
@@ -362,35 +293,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
},
|
||||
colors = CheckboxDefaults.colors(checkedColor = PurrfectOverlayPalette.glowPrimary)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(Color.White.copy(alpha = 0.06f), RoundedCornerShape(12.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val rowBitmap = previewBitmaps[index]
|
||||
val rowLoading = previewLoading[index] == true
|
||||
when {
|
||||
rowBitmap != null -> Image(
|
||||
bitmap = rowBitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(Color.Transparent, RoundedCornerShape(12.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
rowLoading -> CircularProgressIndicator(
|
||||
color = PurrfectOverlayPalette.glowPrimary,
|
||||
modifier = Modifier.size(22.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
else -> Icon(
|
||||
imageVector = Icons.Outlined.Image,
|
||||
contentDescription = null,
|
||||
tint = PurrfectOverlayPalette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -435,10 +337,15 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (selected.isNotEmpty()) {
|
||||
startBatchDownload(selected.sorted().toMutableList(), allowDuplicate)
|
||||
alertDialog.dismiss()
|
||||
if (!selected.contains(currentIndex)) return@Button
|
||||
context.executeAsync {
|
||||
runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, allowDuplicate) }
|
||||
.onFailure {
|
||||
context.log.error("Story download failed", it)
|
||||
context.shortToast(translations["failed_generic_toast"])
|
||||
}
|
||||
}
|
||||
alertDialog.dismiss()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
@@ -457,124 +364,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForStoryIndex(targetIndex: Int, timeoutMs: Long = 3000L): Boolean {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startedAt < timeoutMs) {
|
||||
if (lastSeenMapParams?.getStorySnapIndex() == targetIndex) return true
|
||||
kotlinx.coroutines.delay(60L)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadStoryPreviewBitmap(uriString: String): Bitmap? {
|
||||
return runCatching {
|
||||
val uri = Uri.parse(uriString)
|
||||
when (uri.scheme?.lowercase()) {
|
||||
"content" -> context.androidContext.contentResolver.openInputStream(uri)?.use(BitmapFactory::decodeStream)
|
||||
"file", null -> BitmapFactory.decodeFile(uri.path)
|
||||
"http", "https" -> {
|
||||
runCatching {
|
||||
OkHttpClient().newCall(Request.Builder().url(uriString).build()).execute().use { response ->
|
||||
response.body?.byteStream()?.use { stream -> BitmapFactory.decodeStream(stream) }
|
||||
}
|
||||
}.getOrNull() ?: run {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(uriString, emptyMap())
|
||||
retriever.frameAtTime
|
||||
} finally {
|
||||
runCatching { retriever.release() }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: run {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(context.androidContext, uri)
|
||||
retriever.frameAtTime
|
||||
} finally {
|
||||
runCatching { retriever.release() }
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun startBatchDownload(indices: MutableList<Int>, allowDuplicate: Boolean) {
|
||||
if (indices.isEmpty()) return
|
||||
val paramMap = lastSeenMapParams ?: return
|
||||
val mediaInfoMap = lastSeenMediaInfoMap ?: return
|
||||
|
||||
pendingBatchDownloadIndices = indices
|
||||
batchForceAllowDuplicate = allowDuplicate
|
||||
|
||||
val currentIndex = paramMap.getStorySnapIndex() ?: 0
|
||||
val targetIndex = indices.first()
|
||||
val totalCount = paramMap.getStorySnapTotal()
|
||||
|
||||
if (currentIndex == targetIndex) {
|
||||
processNextBatchDownload(paramMap, mediaInfoMap)
|
||||
} else {
|
||||
val jumped = context.feature(OperaStoryOverlay::class).requestJumpToSnap(targetIndex, totalCount)
|
||||
if (!jumped) {
|
||||
pendingBatchDownloadIndices = null
|
||||
context.shortToast(translations["batch_download_jump_failed_toast"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadSingleSnap(paramMap: ParamMap, mediaInfoMap: Map<SplitMediaAssetType, MediaInfo>) {
|
||||
context.executeAsync {
|
||||
runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, batchForceAllowDuplicate) }
|
||||
.onFailure {
|
||||
context.log.error("Batch download failed", it)
|
||||
context.shortToast(translations["failed_generic_toast"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processNextBatchDownload(paramMap: ParamMap, mediaInfoMap: Map<SplitMediaAssetType, MediaInfo>) {
|
||||
val queue = pendingBatchDownloadIndices ?: return
|
||||
if (queue.isEmpty()) {
|
||||
flushPendingMergeAndComplete()
|
||||
return
|
||||
}
|
||||
|
||||
val currentIndex = paramMap.getStorySnapIndex() ?: -1
|
||||
if (currentIndex != queue.first()) return
|
||||
|
||||
queue.removeAt(0)
|
||||
downloadSingleSnap(paramMap, mediaInfoMap)
|
||||
|
||||
if (queue.isEmpty()) {
|
||||
flushPendingMergeAndComplete()
|
||||
} else {
|
||||
val totalCount = paramMap.getStorySnapTotal()
|
||||
context.runOnUiThread {
|
||||
fun tryJump(retryCount: Int = 0) {
|
||||
val delayMs = if (retryCount == 0) 120L else 220L
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
val jumped = runCatching {
|
||||
context.feature(OperaStoryOverlay::class).requestJumpToSnap(queue.first(), totalCount)
|
||||
}.getOrNull() == true
|
||||
if (!jumped && retryCount < 1) {
|
||||
tryJump(retryCount + 1)
|
||||
} else if (!jumped) {
|
||||
pendingBatchDownloadIndices = null
|
||||
context.shortToast(translations["batch_download_jump_failed_toast"])
|
||||
}
|
||||
}, delayMs)
|
||||
}
|
||||
tryJump()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushPendingMergeAndComplete() {
|
||||
pendingBatchDownloadIndices = null
|
||||
context.shortToast(translations["batch_download_complete_toast"])
|
||||
}
|
||||
|
||||
fun showLastOperaDebugMediaInfo() {
|
||||
if (lastSeenMapParams == null || lastSeenMediaInfoMap == null) return
|
||||
|
||||
@@ -1063,15 +852,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
lastSeenMapParams = mediaParamMap
|
||||
lastSeenMediaInfoMap = mediaInfoMap
|
||||
|
||||
if (pendingBatchDownloadIndices != null) {
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
if (pendingBatchDownloadIndices != null) {
|
||||
processNextBatchDownload(mediaParamMap, mediaInfoMap)
|
||||
}
|
||||
}, 80L)
|
||||
return@onOperaViewStateCallback
|
||||
}
|
||||
|
||||
if (!shouldAutoDownload) {
|
||||
return@onOperaViewStateCallback
|
||||
}
|
||||
|
||||
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,573 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.experiments
|
||||
|
||||
import android.content.Context
|
||||
import me.eternal.purrfectsnap.common.logger.AbstractLogger
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.net.InetAddress
|
||||
import java.security.SecureRandom
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
|
||||
data class RandomizedDeviceProfile(
|
||||
val schemaVersion: Int,
|
||||
val profileId: String,
|
||||
val deviceInfo: DeviceInfo,
|
||||
val androidRelease: String,
|
||||
val buildIncremental: String,
|
||||
val buildDisplayId: String,
|
||||
val buildFingerprint: String,
|
||||
val buildHost: String,
|
||||
val buildTime: Long,
|
||||
val supportedAbis: List<String>,
|
||||
val supported32BitAbis: List<String>,
|
||||
val supported64BitAbis: List<String>,
|
||||
val androidId: String,
|
||||
val gsfId: String,
|
||||
val advertisingId: String,
|
||||
val wifiMacAddress: String,
|
||||
val bluetoothMacAddress: String,
|
||||
val ipAddress: String,
|
||||
val wifiSsid: String,
|
||||
val wifiRssi: Int,
|
||||
val localeTag: String,
|
||||
val countryIso: String,
|
||||
val timeZoneId: String,
|
||||
val timeZoneDisplayName: String,
|
||||
val networkType: Int,
|
||||
val networkOperator: String,
|
||||
val networkOperatorName: String,
|
||||
val networkCountryIso: String,
|
||||
val simCountryIso: String,
|
||||
val simOperator: String,
|
||||
val simOperatorName: String,
|
||||
val simState: Int,
|
||||
val hasIccCard: Boolean,
|
||||
val phoneCount: Int,
|
||||
val isHearingAidCompatibilitySupported: Boolean,
|
||||
val isTtySupported: Boolean,
|
||||
val isWorldPhone: Boolean,
|
||||
val isNetworkRoaming: Boolean,
|
||||
val isSmsCapable: Boolean,
|
||||
val isVoiceCapable: Boolean,
|
||||
val phoneType: Int,
|
||||
val phoneTypeString: String,
|
||||
val mmsUaProfUrl: String,
|
||||
val mmsUserAgent: String,
|
||||
val dnsServers: List<String>,
|
||||
val dnsSearchDomains: String,
|
||||
val privateDnsServerName: String,
|
||||
val privateDnsActive: Boolean,
|
||||
val hasCaptivePortal: Boolean,
|
||||
val secureStringSettings: Map<String, String>,
|
||||
val secureIntSettings: Map<String, Int>,
|
||||
val systemStringSettings: Map<String, String>,
|
||||
val systemIntSettings: Map<String, Int>,
|
||||
val globalStringSettings: Map<String, String>,
|
||||
val globalIntSettings: Map<String, Int>
|
||||
) {
|
||||
fun locale(): Locale = Locale.forLanguageTag(localeTag)
|
||||
|
||||
fun timeZone(): TimeZone = TimeZone.getTimeZone(timeZoneId)
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put("schemaVersion", schemaVersion)
|
||||
put("profileId", profileId)
|
||||
put("deviceInfo", JSONObject().apply {
|
||||
put("manufacturer", deviceInfo.manufacturer)
|
||||
put("model", deviceInfo.model)
|
||||
put("brand", deviceInfo.brand)
|
||||
put("device", deviceInfo.device)
|
||||
put("product", deviceInfo.product)
|
||||
put("hardware", deviceInfo.hardware)
|
||||
put("board", deviceInfo.board)
|
||||
put("bootloader", deviceInfo.bootloader)
|
||||
put("display", deviceInfo.display)
|
||||
put("host", deviceInfo.host)
|
||||
})
|
||||
put("androidRelease", androidRelease)
|
||||
put("buildIncremental", buildIncremental)
|
||||
put("buildDisplayId", buildDisplayId)
|
||||
put("buildFingerprint", buildFingerprint)
|
||||
put("buildHost", buildHost)
|
||||
put("buildTime", buildTime)
|
||||
put("supportedAbis", JSONArray(supportedAbis))
|
||||
put("supported32BitAbis", JSONArray(supported32BitAbis))
|
||||
put("supported64BitAbis", JSONArray(supported64BitAbis))
|
||||
put("androidId", androidId)
|
||||
put("gsfId", gsfId)
|
||||
put("advertisingId", advertisingId)
|
||||
put("wifiMacAddress", wifiMacAddress)
|
||||
put("bluetoothMacAddress", bluetoothMacAddress)
|
||||
put("ipAddress", ipAddress)
|
||||
put("wifiSsid", wifiSsid)
|
||||
put("wifiRssi", wifiRssi)
|
||||
put("localeTag", localeTag)
|
||||
put("countryIso", countryIso)
|
||||
put("timeZoneId", timeZoneId)
|
||||
put("timeZoneDisplayName", timeZoneDisplayName)
|
||||
put("networkType", networkType)
|
||||
put("networkOperator", networkOperator)
|
||||
put("networkOperatorName", networkOperatorName)
|
||||
put("networkCountryIso", networkCountryIso)
|
||||
put("simCountryIso", simCountryIso)
|
||||
put("simOperator", simOperator)
|
||||
put("simOperatorName", simOperatorName)
|
||||
put("simState", simState)
|
||||
put("hasIccCard", hasIccCard)
|
||||
put("phoneCount", phoneCount)
|
||||
put("isHearingAidCompatibilitySupported", isHearingAidCompatibilitySupported)
|
||||
put("isTtySupported", isTtySupported)
|
||||
put("isWorldPhone", isWorldPhone)
|
||||
put("isNetworkRoaming", isNetworkRoaming)
|
||||
put("isSmsCapable", isSmsCapable)
|
||||
put("isVoiceCapable", isVoiceCapable)
|
||||
put("phoneType", phoneType)
|
||||
put("phoneTypeString", phoneTypeString)
|
||||
put("mmsUaProfUrl", mmsUaProfUrl)
|
||||
put("mmsUserAgent", mmsUserAgent)
|
||||
put("dnsServers", JSONArray(dnsServers))
|
||||
put("dnsSearchDomains", dnsSearchDomains)
|
||||
put("privateDnsServerName", privateDnsServerName)
|
||||
put("privateDnsActive", privateDnsActive)
|
||||
put("hasCaptivePortal", hasCaptivePortal)
|
||||
put("secureStringSettings", JSONObject(secureStringSettings))
|
||||
put("secureIntSettings", JSONObject(secureIntSettings))
|
||||
put("systemStringSettings", JSONObject(systemStringSettings))
|
||||
put("systemIntSettings", JSONObject(systemIntSettings))
|
||||
put("globalStringSettings", JSONObject(globalStringSettings))
|
||||
put("globalIntSettings", JSONObject(globalIntSettings))
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String): RandomizedDeviceProfile {
|
||||
val root = JSONObject(json)
|
||||
val deviceInfoJson = root.getJSONObject("deviceInfo")
|
||||
return RandomizedDeviceProfile(
|
||||
schemaVersion = root.getInt("schemaVersion"),
|
||||
profileId = root.getString("profileId"),
|
||||
deviceInfo = DeviceInfo(
|
||||
manufacturer = deviceInfoJson.getString("manufacturer"),
|
||||
model = deviceInfoJson.getString("model"),
|
||||
brand = deviceInfoJson.getString("brand"),
|
||||
device = deviceInfoJson.getString("device"),
|
||||
product = deviceInfoJson.getString("product"),
|
||||
hardware = deviceInfoJson.getString("hardware"),
|
||||
board = deviceInfoJson.getString("board"),
|
||||
bootloader = deviceInfoJson.getString("bootloader"),
|
||||
display = deviceInfoJson.getString("display"),
|
||||
host = deviceInfoJson.getString("host")
|
||||
),
|
||||
androidRelease = root.getString("androidRelease"),
|
||||
buildIncremental = root.getString("buildIncremental"),
|
||||
buildDisplayId = root.getString("buildDisplayId"),
|
||||
buildFingerprint = root.getString("buildFingerprint"),
|
||||
buildHost = root.getString("buildHost"),
|
||||
buildTime = root.getLong("buildTime"),
|
||||
supportedAbis = jsonArrayToStringList(root.getJSONArray("supportedAbis")),
|
||||
supported32BitAbis = jsonArrayToStringList(root.getJSONArray("supported32BitAbis")),
|
||||
supported64BitAbis = jsonArrayToStringList(root.getJSONArray("supported64BitAbis")),
|
||||
androidId = root.getString("androidId"),
|
||||
gsfId = root.getString("gsfId"),
|
||||
advertisingId = root.getString("advertisingId"),
|
||||
wifiMacAddress = root.getString("wifiMacAddress"),
|
||||
bluetoothMacAddress = root.getString("bluetoothMacAddress"),
|
||||
ipAddress = root.optString("ipAddress").ifBlank { defaultFallbackIpAddress() },
|
||||
wifiSsid = root.getString("wifiSsid"),
|
||||
wifiRssi = root.getInt("wifiRssi"),
|
||||
localeTag = root.getString("localeTag"),
|
||||
countryIso = root.getString("countryIso"),
|
||||
timeZoneId = root.getString("timeZoneId"),
|
||||
timeZoneDisplayName = root.getString("timeZoneDisplayName"),
|
||||
networkType = root.getInt("networkType"),
|
||||
networkOperator = root.getString("networkOperator"),
|
||||
networkOperatorName = root.getString("networkOperatorName"),
|
||||
networkCountryIso = root.getString("networkCountryIso"),
|
||||
simCountryIso = root.getString("simCountryIso"),
|
||||
simOperator = root.getString("simOperator"),
|
||||
simOperatorName = root.getString("simOperatorName"),
|
||||
simState = root.getInt("simState"),
|
||||
hasIccCard = root.getBoolean("hasIccCard"),
|
||||
phoneCount = root.getInt("phoneCount"),
|
||||
isHearingAidCompatibilitySupported = root.getBoolean("isHearingAidCompatibilitySupported"),
|
||||
isTtySupported = root.getBoolean("isTtySupported"),
|
||||
isWorldPhone = root.getBoolean("isWorldPhone"),
|
||||
isNetworkRoaming = root.getBoolean("isNetworkRoaming"),
|
||||
isSmsCapable = root.getBoolean("isSmsCapable"),
|
||||
isVoiceCapable = root.getBoolean("isVoiceCapable"),
|
||||
phoneType = root.getInt("phoneType"),
|
||||
phoneTypeString = root.getString("phoneTypeString"),
|
||||
mmsUaProfUrl = root.getString("mmsUaProfUrl"),
|
||||
mmsUserAgent = root.getString("mmsUserAgent"),
|
||||
dnsServers = jsonArrayToStringList(root.getJSONArray("dnsServers")),
|
||||
dnsSearchDomains = root.getString("dnsSearchDomains"),
|
||||
privateDnsServerName = root.getString("privateDnsServerName"),
|
||||
privateDnsActive = root.getBoolean("privateDnsActive"),
|
||||
hasCaptivePortal = root.getBoolean("hasCaptivePortal"),
|
||||
secureStringSettings = jsonObjectToStringMap(root.getJSONObject("secureStringSettings")),
|
||||
secureIntSettings = jsonObjectToIntMap(root.getJSONObject("secureIntSettings")),
|
||||
systemStringSettings = jsonObjectToStringMap(root.getJSONObject("systemStringSettings")),
|
||||
systemIntSettings = jsonObjectToIntMap(root.getJSONObject("systemIntSettings")),
|
||||
globalStringSettings = jsonObjectToStringMap(root.getJSONObject("globalStringSettings")),
|
||||
globalIntSettings = jsonObjectToIntMap(root.getJSONObject("globalIntSettings"))
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonArrayToStringList(array: JSONArray): List<String> = buildList {
|
||||
for (index in 0 until array.length()) {
|
||||
add(array.getString(index))
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonObjectToStringMap(jsonObject: JSONObject): Map<String, String> {
|
||||
return jsonObject.keys().asSequence().associateWith { jsonObject.getString(it) }
|
||||
}
|
||||
|
||||
private fun jsonObjectToIntMap(jsonObject: JSONObject): Map<String, Int> {
|
||||
return jsonObject.keys().asSequence().associateWith { jsonObject.getInt(it) }
|
||||
}
|
||||
|
||||
private fun defaultFallbackIpAddress(): String = "23.42.18.101"
|
||||
}
|
||||
}
|
||||
|
||||
object RandomizedDeviceProfileStore {
|
||||
private const val prefsName = "purrfectsnap_spoof"
|
||||
private const val schemaVersion = 5
|
||||
private const val profileKey = "randomized_device_profile"
|
||||
private val random = SecureRandom()
|
||||
|
||||
fun getOrCreate(context: Context, logger: AbstractLogger, generationToken: String?): RandomizedDeviceProfile {
|
||||
val prefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
val requestedToken = generationToken.orEmpty()
|
||||
prefs.getString(profileKey, null)?.let { raw ->
|
||||
runCatching {
|
||||
RandomizedDeviceProfile.fromJson(raw)
|
||||
}.onSuccess { profile ->
|
||||
val storedToken = prefs.getString("${profileKey}_token", "") ?: ""
|
||||
if (profile.schemaVersion == schemaVersion && storedToken == requestedToken) {
|
||||
logger.info("Loaded randomized device profile ${profile.profileId} (${profile.deviceInfo.manufacturer} ${profile.deviceInfo.model})")
|
||||
return profile
|
||||
}
|
||||
}.onFailure {
|
||||
logger.warn("Failed to parse saved randomized device profile, regenerating: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val previousProfile = prefs.getString(profileKey, null)?.let { raw ->
|
||||
runCatching { RandomizedDeviceProfile.fromJson(raw) }.getOrNull()
|
||||
}
|
||||
val profile = generateProfile(previousProfile)
|
||||
prefs.edit()
|
||||
.putString(profileKey, profile.toJson().toString())
|
||||
.putString("${profileKey}_token", requestedToken)
|
||||
.putString("android_id", profile.androidId)
|
||||
.putString("advertising_id", profile.advertisingId)
|
||||
.putString("bluetooth_address", profile.bluetoothMacAddress)
|
||||
.putString("gsf_id", profile.gsfId)
|
||||
.putString("random_device", profile.deviceInfo.model)
|
||||
.putString("device_fingerprint", profile.buildFingerprint)
|
||||
.apply()
|
||||
|
||||
logger.info(
|
||||
"Generated randomized device profile ${profile.profileId}: " +
|
||||
"${profile.deviceInfo.manufacturer} ${profile.deviceInfo.model}, " +
|
||||
"androidId=${profile.androidId}, ip=${profile.ipAddress}, locale=${profile.localeTag}, tz=${profile.timeZoneId}, " +
|
||||
"carrier=${profile.simOperatorName}"
|
||||
)
|
||||
return profile
|
||||
}
|
||||
|
||||
private fun generateProfile(previousProfile: RandomizedDeviceProfile?): RandomizedDeviceProfile {
|
||||
val eligibleDevices = DeviceSpoofer.getAvailableDevices().filter {
|
||||
DeviceSpoofer.getDeviceTemplate(it)?.capabilities?.let { capabilities ->
|
||||
capabilities.isSmsCapable && capabilities.isVoiceCapable && capabilities.isWorldPhone
|
||||
} == true
|
||||
}.ifEmpty { DeviceSpoofer.getAvailableDevices() }
|
||||
val deviceCandidates = eligibleDevices.filterNot {
|
||||
previousProfile != null &&
|
||||
DeviceSpoofer.getDeviceTemplate(it)?.deviceInfo?.model == previousProfile.deviceInfo.model
|
||||
}.ifEmpty { eligibleDevices }
|
||||
val deviceName = pick(deviceCandidates)
|
||||
val deviceTemplate = DeviceSpoofer.getDeviceTemplate(deviceName) ?: error("Missing device template for $deviceName")
|
||||
val regionCandidates = regionProfiles.filterNot {
|
||||
previousProfile != null &&
|
||||
it.localeTag == previousProfile.localeTag &&
|
||||
it.simOperatorName == previousProfile.simOperatorName
|
||||
}.ifEmpty { regionProfiles }
|
||||
val region = pick(regionCandidates)
|
||||
val buildCandidates = deviceTemplate.builds.filter { it.androidRelease in region.androidReleaseOptions }
|
||||
.ifEmpty { deviceTemplate.builds }
|
||||
val buildProfile = pick(buildCandidates)
|
||||
val deviceInfo = deviceTemplate.deviceInfo.copy(
|
||||
display = buildProfile.display,
|
||||
host = buildProfile.host,
|
||||
bootloader = buildProfile.bootloader ?: deviceTemplate.deviceInfo.bootloader
|
||||
)
|
||||
val androidRelease = buildProfile.androidRelease
|
||||
val buildIncremental = buildProfile.incremental
|
||||
val buildDisplayId = buildProfile.display
|
||||
val buildFingerprint = DeviceSpoofer.generateFingerprint(deviceInfo, buildProfile)
|
||||
val buildHost = buildProfile.host
|
||||
val buildTime = System.currentTimeMillis() - randomLong(45L, 220L) * 24L * 60L * 60L * 1000L
|
||||
val wifiMac = randomMacAddress()
|
||||
val bluetoothMac = randomMacAddress()
|
||||
val ipAddress = region.randomPublicIpAddress()
|
||||
val locale = Locale.forLanguageTag(region.localeTag)
|
||||
val timeZone = TimeZone.getTimeZone(region.timeZoneId)
|
||||
val capabilities = deviceTemplate.capabilities
|
||||
val secureStringSettings = mapOf(
|
||||
"accessibility_enabled" to "0",
|
||||
"speak_password" to "0",
|
||||
"allowed_geolocation_origins" to "",
|
||||
"install_non_market_apps" to "0",
|
||||
"device_provisioned" to "1",
|
||||
"enabled_notification_listeners" to ""
|
||||
)
|
||||
val secureIntSettings = mapOf(
|
||||
"input_method_selector_visibility" to 0,
|
||||
"accessibility_display_inversion_enabled" to 0,
|
||||
"enabled_accessibility_services" to 0,
|
||||
"skip_first_use_hints" to 0,
|
||||
"tts_default_synth" to 0
|
||||
)
|
||||
val systemStringSettings = mapOf(
|
||||
"dtmf_tone_type" to "normal",
|
||||
"mode_ringer_streams_affected" to "166",
|
||||
"mute_streams_affected" to "46",
|
||||
"show_password" to "1",
|
||||
"user_rotation" to "0"
|
||||
)
|
||||
val systemIntSettings = mapOf(
|
||||
"bluetooth_discoverability" to 0,
|
||||
"bluetooth_discoverability_timeout" to 120,
|
||||
"date_format" to 0,
|
||||
"end_button_behavior" to 2
|
||||
)
|
||||
val globalStringSettings = mapOf(
|
||||
"adb_enabled" to "0",
|
||||
"auto_time" to "1",
|
||||
"auto_time_zone" to "1",
|
||||
"development_settings_enabled" to "0",
|
||||
"stay_on_while_plugged_in" to "0",
|
||||
"usb_mass_storage_enabled" to "0",
|
||||
"wifi_networks_available_notification_on" to "0",
|
||||
"data_roaming" to "1"
|
||||
)
|
||||
val globalIntSettings = mapOf(
|
||||
"always_finish_activities" to 0,
|
||||
"animator_duration_scale" to 1,
|
||||
"http_proxy" to 0,
|
||||
"network_preference" to 1,
|
||||
"transition_animation_scale" to 1,
|
||||
"use_google_mail" to 1,
|
||||
"wait_for_debugger" to 0
|
||||
)
|
||||
|
||||
return RandomizedDeviceProfile(
|
||||
schemaVersion = schemaVersion,
|
||||
profileId = UUID.randomUUID().toString().substring(0, 8),
|
||||
deviceInfo = deviceInfo,
|
||||
androidRelease = androidRelease,
|
||||
buildIncremental = buildIncremental,
|
||||
buildDisplayId = buildDisplayId,
|
||||
buildFingerprint = buildFingerprint,
|
||||
buildHost = buildHost,
|
||||
buildTime = buildTime,
|
||||
supportedAbis = capabilities.supportedAbis,
|
||||
supported32BitAbis = capabilities.supported32BitAbis,
|
||||
supported64BitAbis = capabilities.supported64BitAbis,
|
||||
androidId = randomHex(16),
|
||||
gsfId = randomHex(16),
|
||||
advertisingId = UUID.randomUUID().toString(),
|
||||
wifiMacAddress = wifiMac,
|
||||
bluetoothMacAddress = bluetoothMac,
|
||||
ipAddress = ipAddress,
|
||||
wifiSsid = region.randomWifiSsid(),
|
||||
wifiRssi = random.nextInt(-72, -36),
|
||||
localeTag = locale.toLanguageTag(),
|
||||
countryIso = region.countryIso,
|
||||
timeZoneId = region.timeZoneId,
|
||||
timeZoneDisplayName = timeZone.getDisplayName(false, TimeZone.SHORT, locale),
|
||||
networkType = 13,
|
||||
networkOperator = region.networkOperator,
|
||||
networkOperatorName = region.networkOperatorName,
|
||||
networkCountryIso = region.countryIso.lowercase(Locale.US),
|
||||
simCountryIso = region.countryIso.lowercase(Locale.US),
|
||||
simOperator = region.simOperator,
|
||||
simOperatorName = region.simOperatorName,
|
||||
simState = 5,
|
||||
hasIccCard = true,
|
||||
phoneCount = capabilities.phoneCount,
|
||||
isHearingAidCompatibilitySupported = capabilities.isHearingAidCompatibilitySupported,
|
||||
isTtySupported = capabilities.isTtySupported,
|
||||
isWorldPhone = capabilities.isWorldPhone,
|
||||
isNetworkRoaming = false,
|
||||
isSmsCapable = capabilities.isSmsCapable,
|
||||
isVoiceCapable = capabilities.isVoiceCapable,
|
||||
phoneType = capabilities.phoneType,
|
||||
phoneTypeString = capabilities.phoneTypeString,
|
||||
mmsUaProfUrl = "",
|
||||
mmsUserAgent = region.mmsUserAgent(deviceTemplate.marketingName, androidRelease),
|
||||
dnsServers = region.dnsServers.sortedBy { random.nextInt() }.take(2),
|
||||
dnsSearchDomains = region.dnsSearchDomains,
|
||||
privateDnsServerName = region.privateDnsServerName,
|
||||
privateDnsActive = true,
|
||||
hasCaptivePortal = false,
|
||||
secureStringSettings = secureStringSettings,
|
||||
secureIntSettings = secureIntSettings,
|
||||
systemStringSettings = systemStringSettings,
|
||||
systemIntSettings = systemIntSettings,
|
||||
globalStringSettings = globalStringSettings,
|
||||
globalIntSettings = globalIntSettings
|
||||
)
|
||||
}
|
||||
|
||||
private fun randomHex(length: Int): String {
|
||||
val chars = CharArray(length)
|
||||
val alphabet = "0123456789abcdef"
|
||||
for (index in chars.indices) {
|
||||
chars[index] = alphabet[random.nextInt(alphabet.length)]
|
||||
}
|
||||
return String(chars)
|
||||
}
|
||||
|
||||
private fun randomDigits(length: Int): String {
|
||||
val chars = CharArray(length)
|
||||
for (index in chars.indices) {
|
||||
chars[index] = ('0'.code + random.nextInt(10)).toChar()
|
||||
}
|
||||
return String(chars)
|
||||
}
|
||||
|
||||
private fun randomMacAddress(): String {
|
||||
val bytes = ByteArray(6)
|
||||
random.nextBytes(bytes)
|
||||
bytes[0] = (bytes[0].toInt() and 0xFE or 0x02).toByte()
|
||||
return bytes.joinToString(":") { "%02x".format(it.toInt() and 0xFF) }
|
||||
}
|
||||
|
||||
private fun randomPublicIpv4(prefixes: List<Int>? = null): String {
|
||||
val firstOctet = prefixes?.takeIf { it.isNotEmpty() }?.let { pick(it) } ?: run {
|
||||
generateSequence { random.nextInt(1, 224) }
|
||||
.first { candidate ->
|
||||
candidate != 10 &&
|
||||
candidate != 127 &&
|
||||
candidate != 169 &&
|
||||
candidate != 172 &&
|
||||
candidate != 192
|
||||
}
|
||||
}
|
||||
val secondOctet = random.nextInt(1, 255)
|
||||
val thirdOctet = random.nextInt(1, 255)
|
||||
val fourthOctet = random.nextInt(2, 255)
|
||||
val candidate = "$firstOctet.$secondOctet.$thirdOctet.$fourthOctet"
|
||||
return runCatching { InetAddress.getByName(candidate).hostAddress }.getOrDefault(candidate)
|
||||
}
|
||||
|
||||
private fun randomLong(minInclusive: Long, maxExclusive: Long): Long {
|
||||
require(maxExclusive > minInclusive)
|
||||
val bound = maxExclusive - minInclusive
|
||||
var bits: Long
|
||||
var candidate: Long
|
||||
do {
|
||||
bits = random.nextLong() ushr 1
|
||||
candidate = bits % bound
|
||||
} while (bits - candidate + (bound - 1) < 0L)
|
||||
return minInclusive + candidate
|
||||
}
|
||||
|
||||
private fun <T> pick(values: List<T>): T = values[random.nextInt(values.size)]
|
||||
|
||||
private data class RegionProfile(
|
||||
val localeTag: String,
|
||||
val countryIso: String,
|
||||
val timeZoneId: String,
|
||||
val networkOperator: String,
|
||||
val networkOperatorName: String,
|
||||
val simOperator: String,
|
||||
val simOperatorName: String,
|
||||
val dnsServers: List<String>,
|
||||
val dnsSearchDomains: String,
|
||||
val privateDnsServerName: String,
|
||||
val timeFormat: String,
|
||||
val androidReleaseOptions: List<String>,
|
||||
val wifiPrefixes: List<String>,
|
||||
val ipPrefixes: List<Int>
|
||||
) {
|
||||
fun randomWifiSsid(): String = "${pick(wifiPrefixes)}-${randomDigits(4)}"
|
||||
fun randomPublicIpAddress(): String = randomPublicIpv4(ipPrefixes)
|
||||
|
||||
fun mmsUserAgent(model: String, androidRelease: String): String {
|
||||
return "$model/$androidRelease"
|
||||
}
|
||||
}
|
||||
|
||||
private val regionProfiles = listOf(
|
||||
RegionProfile(
|
||||
localeTag = "en-US",
|
||||
countryIso = "US",
|
||||
timeZoneId = "America/New_York",
|
||||
networkOperator = "310260",
|
||||
networkOperatorName = "T-Mobile",
|
||||
simOperator = "310260",
|
||||
simOperatorName = "T-Mobile",
|
||||
dnsServers = listOf("8.8.8.8", "8.8.4.4", "1.1.1.1"),
|
||||
dnsSearchDomains = "hsd1.ny.comcast.net",
|
||||
privateDnsServerName = "dns.google",
|
||||
timeFormat = "12",
|
||||
androidReleaseOptions = listOf("14", "15"),
|
||||
wifiPrefixes = listOf("TP-Link", "NETGEAR", "XFINITY", "HomeWiFi"),
|
||||
ipPrefixes = listOf(23, 24, 45, 47, 66, 67, 68, 69, 72, 73, 98, 104, 107, 108, 162, 184, 198, 199)
|
||||
),
|
||||
RegionProfile(
|
||||
localeTag = "en-GB",
|
||||
countryIso = "GB",
|
||||
timeZoneId = "Europe/London",
|
||||
networkOperator = "23430",
|
||||
networkOperatorName = "EE",
|
||||
simOperator = "23430",
|
||||
simOperatorName = "EE",
|
||||
dnsServers = listOf("1.1.1.1", "1.0.0.1", "8.8.8.8"),
|
||||
dnsSearchDomains = "bb.sky.com",
|
||||
privateDnsServerName = "one.one.one.one",
|
||||
timeFormat = "24",
|
||||
androidReleaseOptions = listOf("14", "15"),
|
||||
wifiPrefixes = listOf("Sky", "BT-Hub", "VirginMedia", "Linksys"),
|
||||
ipPrefixes = listOf(51, 62, 77, 81, 86, 87, 88, 89, 90, 91, 92, 109, 141, 176, 185, 188)
|
||||
),
|
||||
RegionProfile(
|
||||
localeTag = "de-DE",
|
||||
countryIso = "DE",
|
||||
timeZoneId = "Europe/Berlin",
|
||||
networkOperator = "26202",
|
||||
networkOperatorName = "Vodafone DE",
|
||||
simOperator = "26202",
|
||||
simOperatorName = "Vodafone DE",
|
||||
dnsServers = listOf("9.9.9.9", "149.112.112.112", "1.1.1.1"),
|
||||
dnsSearchDomains = "fritz.box",
|
||||
privateDnsServerName = "dns.quad9.net",
|
||||
timeFormat = "24",
|
||||
androidReleaseOptions = listOf("14", "15"),
|
||||
wifiPrefixes = listOf("FRITZBox", "Vodafone", "Telekom", "WLAN"),
|
||||
ipPrefixes = listOf(2, 5, 31, 37, 46, 79, 80, 84, 85, 87, 91, 93, 95, 109, 134, 176, 178, 188)
|
||||
),
|
||||
RegionProfile(
|
||||
localeTag = "en-IN",
|
||||
countryIso = "IN",
|
||||
timeZoneId = "Asia/Kolkata",
|
||||
networkOperator = "405874",
|
||||
networkOperatorName = "Jio",
|
||||
simOperator = "405874",
|
||||
simOperatorName = "Jio",
|
||||
dnsServers = listOf("1.1.1.1", "8.8.8.8", "9.9.9.9"),
|
||||
dnsSearchDomains = "airtelbroadband.in",
|
||||
privateDnsServerName = "dns.google",
|
||||
timeFormat = "12",
|
||||
androidReleaseOptions = listOf("14", "15"),
|
||||
wifiPrefixes = listOf("JioFiber", "Airtel", "ACTFibernet", "HomeNet"),
|
||||
ipPrefixes = listOf(14, 27, 42, 49, 59, 61, 101, 103, 106, 117, 122, 125, 157, 182)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,87 +1,244 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.messaging
|
||||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.ToneGenerator
|
||||
import android.media.AudioTrack
|
||||
import kotlinx.coroutines.delay
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.ConversationUpdateEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.sin
|
||||
|
||||
class ConversationSoundEffects : Feature("Conversation Sound Effects") {
|
||||
private val seenIncomingMessageIds = LinkedHashSet<Long>()
|
||||
private val maxTrackedMessages = 512
|
||||
|
||||
private data class ToneStep(
|
||||
val tone: Int,
|
||||
private data class BubbleSpec(
|
||||
val durationMs: Int,
|
||||
val startFreqHz: Double,
|
||||
val endFreqHz: Double,
|
||||
val overtoneFreqHz: Double,
|
||||
val amplitude: Double
|
||||
)
|
||||
|
||||
private data class BubbleStep(
|
||||
val spec: BubbleSpec,
|
||||
val pauseAfterMs: Long = 0L
|
||||
)
|
||||
|
||||
private data class ToneSpec(
|
||||
val sendPattern: List<ToneStep>,
|
||||
val receivePattern: List<ToneStep>
|
||||
private val iMessageSendBubble = BubbleSpec(
|
||||
durationMs = 78,
|
||||
startFreqHz = 1160.0,
|
||||
endFreqHz = 690.0,
|
||||
overtoneFreqHz = 1820.0,
|
||||
amplitude = 0.50
|
||||
)
|
||||
|
||||
private val iMessageReceiveBubble = BubbleSpec(
|
||||
durationMs = 92,
|
||||
startFreqHz = 1040.0,
|
||||
endFreqHz = 640.0,
|
||||
overtoneFreqHz = 1680.0,
|
||||
amplitude = 0.46
|
||||
)
|
||||
|
||||
private val whatsappSendBubble = BubbleSpec(
|
||||
durationMs = 86,
|
||||
startFreqHz = 860.0,
|
||||
endFreqHz = 520.0,
|
||||
overtoneFreqHz = 1410.0,
|
||||
amplitude = 0.52
|
||||
)
|
||||
|
||||
private val whatsappReceiveBubble = BubbleSpec(
|
||||
durationMs = 94,
|
||||
startFreqHz = 920.0,
|
||||
endFreqHz = 560.0,
|
||||
overtoneFreqHz = 1520.0,
|
||||
amplitude = 0.50
|
||||
)
|
||||
|
||||
private val telegramSendPrimary = BubbleSpec(
|
||||
durationMs = 58,
|
||||
startFreqHz = 1110.0,
|
||||
endFreqHz = 820.0,
|
||||
overtoneFreqHz = 1710.0,
|
||||
amplitude = 0.42
|
||||
)
|
||||
|
||||
private val telegramSendAccent = BubbleSpec(
|
||||
durationMs = 34,
|
||||
startFreqHz = 1360.0,
|
||||
endFreqHz = 980.0,
|
||||
overtoneFreqHz = 2060.0,
|
||||
amplitude = 0.22
|
||||
)
|
||||
|
||||
private val telegramReceivePrimary = BubbleSpec(
|
||||
durationMs = 72,
|
||||
startFreqHz = 1080.0,
|
||||
endFreqHz = 780.0,
|
||||
overtoneFreqHz = 1680.0,
|
||||
amplitude = 0.44
|
||||
)
|
||||
|
||||
private val telegramReceiveAccent = BubbleSpec(
|
||||
durationMs = 42,
|
||||
startFreqHz = 1280.0,
|
||||
endFreqHz = 940.0,
|
||||
overtoneFreqHz = 1940.0,
|
||||
amplitude = 0.18
|
||||
)
|
||||
|
||||
private val subtleSendBubble = BubbleSpec(
|
||||
durationMs = 60,
|
||||
startFreqHz = 760.0,
|
||||
endFreqHz = 520.0,
|
||||
overtoneFreqHz = 1180.0,
|
||||
amplitude = 0.26
|
||||
)
|
||||
|
||||
private val subtleReceiveBubble = BubbleSpec(
|
||||
durationMs = 66,
|
||||
startFreqHz = 800.0,
|
||||
endFreqHz = 560.0,
|
||||
overtoneFreqHz = 1260.0,
|
||||
amplitude = 0.24
|
||||
)
|
||||
|
||||
private fun currentConversationId() = context.feature(Messaging::class).openedConversationUUID?.toString()
|
||||
|
||||
private fun styleSpec(): ToneSpec {
|
||||
return when (context.config.messaging.conversationSoundEffectsStyle.get()) {
|
||||
"telegram" -> ToneSpec(
|
||||
sendPattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_BEEP, 35),
|
||||
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 45, 25)
|
||||
),
|
||||
receivePattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 70),
|
||||
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 35, 20)
|
||||
)
|
||||
)
|
||||
"whatsapp" -> ToneSpec(
|
||||
sendPattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_ACK, 55)
|
||||
),
|
||||
receivePattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 85),
|
||||
ToneStep(ToneGenerator.TONE_PROP_ACK, 35, 15)
|
||||
)
|
||||
)
|
||||
"subtle" -> ToneSpec(
|
||||
sendPattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_PROMPT, 22)
|
||||
),
|
||||
receivePattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_ACK, 28)
|
||||
)
|
||||
)
|
||||
else -> ToneSpec(
|
||||
sendPattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_PROMPT, 40),
|
||||
ToneStep(ToneGenerator.TONE_PROP_BEEP, 28, 18)
|
||||
),
|
||||
receivePattern = listOf(
|
||||
ToneStep(ToneGenerator.TONE_PROP_ACK, 55),
|
||||
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 40, 22)
|
||||
)
|
||||
private fun buildBubblePcm(spec: BubbleSpec, sampleRate: Int = 44_100): ByteArray {
|
||||
val sampleCount = (sampleRate * (spec.durationMs / 1000.0)).toInt().coerceAtLeast(1)
|
||||
val pcm = ByteArray(sampleCount * 2)
|
||||
for (i in 0 until sampleCount) {
|
||||
val progress = i.toDouble() / sampleCount.toDouble()
|
||||
val envelope = exp(-4.8 * progress) * (1.0 - exp(-20.0 * progress))
|
||||
val freq = spec.startFreqHz + (spec.endFreqHz - spec.startFreqHz) * progress
|
||||
val t = i.toDouble() / sampleRate.toDouble()
|
||||
val fundamental = sin(2.0 * PI * freq * t)
|
||||
val overtone = 0.18 * sin(2.0 * PI * spec.overtoneFreqHz * t)
|
||||
val airyTail = 0.08 * sin(2.0 * PI * (freq * 0.48) * t)
|
||||
val warmth = 0.14 * sin(2.0 * PI * (freq * 0.24) * t)
|
||||
val sample = ((fundamental + overtone + airyTail + warmth) * envelope * spec.amplitude)
|
||||
.coerceIn(-1.0, 1.0)
|
||||
val shortValue = (sample * Short.MAX_VALUE).toInt().toShort()
|
||||
pcm[i * 2] = (shortValue.toInt() and 0xFF).toByte()
|
||||
pcm[i * 2 + 1] = ((shortValue.toInt() shr 8) and 0xFF).toByte()
|
||||
}
|
||||
return pcm
|
||||
}
|
||||
|
||||
private fun playBubble(spec: BubbleSpec) {
|
||||
if (context.isMainActivityPaused) return
|
||||
context.executeAsync {
|
||||
val sampleRate = 44_100
|
||||
val pcm = buildBubblePcm(spec, sampleRate)
|
||||
val audioTrack = AudioTrack(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build(),
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(sampleRate)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||
.build(),
|
||||
pcm.size,
|
||||
AudioTrack.MODE_STATIC,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE
|
||||
)
|
||||
runCatching {
|
||||
audioTrack.write(pcm, 0, pcm.size)
|
||||
audioTrack.play()
|
||||
delay(spec.durationMs.toLong() + 24L)
|
||||
}.also {
|
||||
runCatching {
|
||||
audioTrack.stop()
|
||||
audioTrack.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playPattern(pattern: List<ToneStep>) {
|
||||
private fun playBubbleSequence(steps: List<BubbleStep>) {
|
||||
if (context.isMainActivityPaused) return
|
||||
context.executeAsync {
|
||||
var toneGenerator: ToneGenerator? = null
|
||||
runCatching {
|
||||
toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 55)
|
||||
pattern.forEach { step ->
|
||||
toneGenerator?.startTone(step.tone, step.durationMs)
|
||||
if (step.pauseAfterMs > 0) delay(step.pauseAfterMs)
|
||||
steps.forEach { step ->
|
||||
val sampleRate = 44_100
|
||||
val pcm = buildBubblePcm(step.spec, sampleRate)
|
||||
val audioTrack = AudioTrack(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build(),
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(sampleRate)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||
.build(),
|
||||
pcm.size,
|
||||
AudioTrack.MODE_STATIC,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE
|
||||
)
|
||||
runCatching {
|
||||
audioTrack.write(pcm, 0, pcm.size)
|
||||
audioTrack.play()
|
||||
delay(step.spec.durationMs.toLong() + step.pauseAfterMs + 18L)
|
||||
}.also {
|
||||
runCatching {
|
||||
audioTrack.stop()
|
||||
audioTrack.release()
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
runCatching { toneGenerator?.release() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playStyledSend() {
|
||||
when (context.config.messaging.conversationSoundEffectsStyle.get()) {
|
||||
"imessage" -> playBubble(iMessageSendBubble)
|
||||
"whatsapp" -> playBubble(whatsappSendBubble)
|
||||
"telegram" -> playBubbleSequence(
|
||||
listOf(
|
||||
BubbleStep(telegramSendPrimary, pauseAfterMs = 16L),
|
||||
BubbleStep(telegramSendAccent)
|
||||
)
|
||||
)
|
||||
else -> playBubble(subtleSendBubble)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playStyledReceive() {
|
||||
when (context.config.messaging.conversationSoundEffectsStyle.get()) {
|
||||
"imessage" -> playBubble(iMessageReceiveBubble)
|
||||
"whatsapp" -> playBubbleSequence(
|
||||
listOf(
|
||||
BubbleStep(whatsappReceiveBubble, pauseAfterMs = 12L),
|
||||
BubbleStep(
|
||||
whatsappReceiveBubble.copy(
|
||||
durationMs = 42,
|
||||
startFreqHz = 1210.0,
|
||||
endFreqHz = 860.0,
|
||||
overtoneFreqHz = 1980.0,
|
||||
amplitude = 0.20
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
"telegram" -> playBubbleSequence(
|
||||
listOf(
|
||||
BubbleStep(telegramReceivePrimary, pauseAfterMs = 14L),
|
||||
BubbleStep(telegramReceiveAccent)
|
||||
)
|
||||
)
|
||||
else -> playBubble(subtleReceiveBubble)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markSeen(messageId: Long): Boolean {
|
||||
synchronized(seenIncomingMessageIds) {
|
||||
val added = seenIncomingMessageIds.add(messageId)
|
||||
@@ -93,15 +250,14 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (!context.config.messaging.conversationSoundEffects.get()) return
|
||||
if (context.config.messaging.conversationSoundEffectsStyle.get() == "disabled") return
|
||||
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
val activeConversationId = currentConversationId() ?: return@subscribe
|
||||
if (event.destinations.conversations?.none { it.toString() == activeConversationId } != false) return@subscribe
|
||||
|
||||
event.addCallbackResult("onSuccess") {
|
||||
val spec = styleSpec()
|
||||
playPattern(spec.sendPattern)
|
||||
playStyledSend()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +266,6 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
|
||||
if (event.conversationId != activeConversationId) return@subscribe
|
||||
|
||||
val myUserId = context.database.myUserId ?: return@subscribe
|
||||
val spec = styleSpec()
|
||||
|
||||
event.messages
|
||||
.asSequence()
|
||||
@@ -119,7 +274,7 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
|
||||
.filter { markSeen(it) }
|
||||
.firstOrNull()
|
||||
?.let {
|
||||
playPattern(spec.receivePattern)
|
||||
playStyledReceive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,11 +121,6 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
||||
param.setArg(1, captureResolutionConfig[1])
|
||||
}
|
||||
|
||||
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
|
||||
val key = param.arg<CaptureRequest.Key<*>>(0)
|
||||
if (key == CaptureRequest.CONTROL_ZOOM_RATIO) return@hook
|
||||
}
|
||||
|
||||
CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param ->
|
||||
val key = param.argNullable<Key<*>>(0) ?: return@hook
|
||||
|
||||
@@ -151,6 +146,14 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) {
|
||||
val isFrontCamera = param.invokeOriginal(
|
||||
arrayOf(CameraCharacteristics.LENS_FACING)
|
||||
) == CameraCharacteristics.LENS_FACING_FRONT
|
||||
val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook
|
||||
param.setResult(arrayOf(Range(customFrameRate, customFrameRate)))
|
||||
}
|
||||
}
|
||||
|
||||
if (config.blackPhotos.get()) {
|
||||
|
||||
@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
nativeAbis=arm64-v8a
|
||||
|
||||
APP_VERSION_NAME=1.6.0
|
||||
APP_VERSION_CODE=310
|
||||
APP_VERSION_NAME=1.6.2
|
||||
APP_VERSION_CODE=314
|
||||
debug_build_hash=18fe2a814d0e2eb5
|
||||
psIntegrityPinnedSha256=
|
||||
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c
|
||||
|
||||
Reference in New Issue
Block a user