feat(experimental): better location

This commit is contained in:
rhunk 2024-03-23 02:09:09 +01:00
parent 10bcb93d45
commit c2816766a8
6 changed files with 156 additions and 93 deletions

View File

@ -500,20 +500,36 @@
"name": "Global",
"description": "Tweak Global Snapchat Settings",
"properties": {
"spoofLocation": {
"name": "Location",
"description": "Spoof your location",
"better_location": {
"name": "Better Location",
"description": "Enhances the Snapchat Location",
"properties": {
"spoof_location": {
"name": "Spoof Location",
"description": "Spoofs your location to a specified one"
},
"coordinates": {
"name": "Coordinates",
"description": "Set the coordinates"
}
}
"description": "Set the coordinates of the spoofed location"
},
"always_update_location": {
"name": "Always Update Location",
"description": "Force Snapchat to update location even if no GPS data is received"
},
"suspend_location_updates": {
"name": "Suspend Location Updates",
"description": "Adds a button in map settings to suspend location updates"
},
"spoof_battery_level": {
"name": "Spoof Battery Level",
"description": "Spoofs the battery level of your device on map\nValue must be between 0 and 100"
},
"spoof_headphones": {
"name": "Spoof Headphones",
"description": "Spoofs the status of listening to music on map"
}
}
},
"snapchat_plus": {
"name": "Snapchat Plus",
"description": "Enables Snapchat Plus features\nSome Server-sided features may not work"

View File

@ -18,11 +18,15 @@ class Global : ConfigContainer() {
)
}
inner class SpoofLocation : ConfigContainer(hasGlobalState = true) {
val coordinates = mapCoordinates("coordinates", 0.0 to 0.0) { requireRestart()} // lat, long
}
val spoofLocation = container("spoofLocation", SpoofLocation())
inner class BetterLocation : ConfigContainer(hasGlobalState = true) {
val spoofLocation = boolean("spoof_location") { requireRestart() }
val coordinates = mapCoordinates("coordinates", 0.0 to 0.0) // lat, long
val alwaysUpdateLocation = boolean("always_update_location") { requireRestart() }
val suspendLocationUpdates = boolean("suspend_location_updates") { requireRestart() }
val spoofBatteryLevel = string("spoof_battery_level") { requireRestart(); inputCheck = { it.isEmpty() || it.toIntOrNull() in 0..100 } }
val spoofHeadphones = boolean("spoof_headphones") { requireRestart() }
}
val betterLocation = container("better_location", BetterLocation())
val snapchatPlus = boolean("snapchat_plus") { requireRestart() }
val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
val disableMetrics = boolean("disable_metrics") { requireRestart() }

View File

@ -85,7 +85,6 @@ class FeatureManager(
MediaQualityLevelOverride(),
MeoPasscodeBypass(),
AppPasscode(),
LocationSpoofer(),
CameraTweaks(),
InfiniteStoryBoost(),
AmoledDarkMode(),
@ -126,6 +125,7 @@ class FeatureManager(
AccountSwitcher(),
RemoveGroupsLockedStatus(),
BypassMessageActionRestrictions(),
BetterLocation(),
)
initializeFeatures()

View File

@ -0,0 +1,116 @@
package me.rhunk.snapenhance.core.features.impl.experiments
import android.location.Location
import android.location.LocationManager
import me.rhunk.snapenhance.common.util.protobuf.EditorContext
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.global.SuspendLocationUpdates
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import java.nio.ByteBuffer
import kotlin.time.Duration.Companion.days
class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams.INIT_SYNC) {
private fun editClientUpdate(editor: EditorContext) {
val config = context.config.global.betterLocation
editor.apply {
// SCVSLocationUpdate
edit(1) {
context.log.verbose("SCVSLocationUpdate ${this@apply}")
if (config.spoofLocation.get()) {
val coordinates by config.coordinates
remove(1)
remove(2)
addFixed32(1, coordinates.first.toFloat()) // lat
addFixed32(2, coordinates.second.toFloat()) // lng
}
if (config.alwaysUpdateLocation.get()) {
remove(7)
addVarInt(7, System.currentTimeMillis()) // timestamp
}
if (context.feature(SuspendLocationUpdates::class).isSuspended()) {
remove(7)
addVarInt(7, System.currentTimeMillis() - 15.days.inWholeMilliseconds)
}
}
// SCVSDeviceData
edit(3) {
config.spoofBatteryLevel.getNullable()?.takeIf { it.isNotEmpty() }?.let {
val value = it.toIntOrNull()?.toFloat()?.div(100) ?: return@edit
remove(2)
addFixed32(2, value)
if (value == 100F) {
remove(3)
addVarInt(3, 1) // devicePluggedIn
}
}
if (config.spoofHeadphones.get()) {
remove(4)
addVarInt(4, 1) // headphoneOutput
remove(6)
addVarInt(6, 1) // isOtherAudioPlaying
}
edit(10) {
remove(1)
addVarInt(1, 4) // type = ALWAYS
remove(2)
addVarInt(2, 1) // precise = true
}
}
}
}
override fun init() {
if (context.config.global.betterLocation.globalState != true) return
if (context.config.global.betterLocation.spoofLocation.get()) {
LocationManager::class.java.apply {
hook("isProviderEnabled", HookStage.BEFORE) { it.setResult(true) }
hook("isProviderEnabledForUser", HookStage.BEFORE) { it.setResult(true) }
}
Location::class.java.apply {
hook("getLatitude", HookStage.BEFORE) {
it.setResult(context.config.global.betterLocation.coordinates.get().first) }
hook("getLongitude", HookStage.BEFORE) {
it.setResult(context.config.global.betterLocation.coordinates.get().second)
}
}
}
context.event.subscribe(UnaryCallEvent::class) { event ->
if (event.uri == "/snapchat.valis.Valis/SendClientUpdate") {
event.buffer = ProtoEditor(event.buffer).apply {
edit {
editEach(1) {
editClientUpdate(this)
}
}
}.toByteArray()
}
}
findClass("com.snapchat.client.grpc.ClientStreamSendHandler\$CppProxy").hook("send", HookStage.BEFORE) { param ->
val array = param.arg<ByteBuffer>(0).let {
it.position(0)
ByteArray(it.capacity()).also { buffer -> it.get(buffer); it.position(0) }
}
param.setArg(0, ProtoEditor(array).apply {
edit {
editClientUpdate(this)
}
}.toByteArray().let {
ByteBuffer.allocateDirect(it.size).put(it).rewind()
})
}
}
}

View File

@ -1,28 +0,0 @@
package me.rhunk.snapenhance.core.features.impl.global
import android.location.Location
import android.location.LocationManager
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() {
if (context.config.global.spoofLocation.globalState != true) return
val coordinates by context.config.global.spoofLocation.coordinates
Location::class.java.apply {
hook("getLatitude", HookStage.BEFORE) { it.setResult(coordinates.first) }
hook("getLongitude", HookStage.BEFORE) { it.setResult(coordinates.second) }
hook("getAccuracy", HookStage.BEFORE) { it.setResult(0.0F) }
}
LocationManager::class.java.apply {
//Might be redundant because it calls isProviderEnabledForUser which we also hook, meaning if isProviderEnabledForUser returns true this will also return true
hook("isProviderEnabled", HookStage.BEFORE) { it.setResult(true) }
hook("isProviderEnabledForUser", HookStage.BEFORE) { it.setResult(true) }
}
}
}

View File

@ -7,56 +7,17 @@ import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.BridgeFileFeature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getId
import java.util.WeakHashMap
//TODO: bridge shared preferences
class SuspendLocationUpdates : BridgeFileFeature(
"Suspend Location Updates",
loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC,
bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE
) {
private val streamSendHandlerInstanceMap = WeakHashMap<Any, () -> Unit>()
private val isEnabled get() = context.config.global.suspendLocationUpdates.get()
override fun init() {
if (!isEnabled) return
reload()
findClass("com.snapchat.client.grpc.ClientStreamSendHandler\$CppProxy").hook("send", HookStage.BEFORE) { param ->
if (param.nullableThisObject<Any>() !in streamSendHandlerInstanceMap) return@hook
if (!exists("true")) return@hook
param.setResult(null)
}
context.classCache.unifiedGrpcService.apply {
hook("unaryCall", HookStage.BEFORE) { param ->
val uri = param.arg<String>(0)
if (exists("true") && uri == "/snapchat.valis.Valis/SendClientUpdate") {
param.setResult(null)
}
}
hook("bidiStreamingCall", HookStage.AFTER) { param ->
val uri = param.arg<String>(0)
if (uri != "/snapchat.valis.Valis/Communicate") return@hook
param.getResult()?.let { instance ->
streamSendHandlerInstanceMap[instance] = {
runCatching {
instance::class.java.methods.first { it.name == "closeStream" }.invoke(instance)
}.onFailure {
context.log.error("Failed to close stream send handler instance", it)
}
}
}
}
}
}
loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC, bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE) {
fun isSuspended() = exists("true")
private fun setSuspended(suspended: Boolean) = setState("true", suspended)
override fun onActivityCreate() {
if (!isEnabled) return
if (context.config.global.betterLocation.takeIf { it.globalState == true }?.suspendLocationUpdates?.get() != true) return
reload()
val locationSharingSettingsContainerId = context.resources.getId("location_sharing_settings_container")
val recyclerViewContainerId = context.resources.getId("recycler_view_container")
@ -64,7 +25,7 @@ class SuspendLocationUpdates : BridgeFileFeature(
context.event.subscribe(AddViewEvent::class) { event ->
if (event.parent.id == locationSharingSettingsContainerId && event.view.id == recyclerViewContainerId) {
(event.view as ViewGroup).addView(Switch(event.view.context).apply {
isChecked = exists("true")
isChecked = isSuspended()
ViewAppearanceHelper.applyTheme(this)
text = this@SuspendLocationUpdates.context.translation["suspend_location_updates.switch_text"]
layoutParams = ViewGroup.LayoutParams(
@ -72,13 +33,7 @@ class SuspendLocationUpdates : BridgeFileFeature(
ViewGroup.LayoutParams.WRAP_CONTENT
)
setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
streamSendHandlerInstanceMap.entries.removeIf { (_, closeStream) ->
closeStream()
true
}
}
setState("true", isChecked)
setSuspended(isChecked)
}
})
}