commit c2816766a8c12c025439e985bb1286db365ab1e2
parent 10bcb93d45a7207380303a30035ab182fd7d1038
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat, 23 Mar 2024 02:09:09 +0100

feat(experimental): better location

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 32++++++++++++++++++++++++--------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt | 12++++++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt | 2+-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/LocationSpoofer.kt | 29-----------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt | 59+++++++----------------------------------------------------
6 files changed, 157 insertions(+), 94 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -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" } } }, - "suspend_location_updates": { - "name": "Suspend Location Updates", - "description": "Adds a button in map settings to suspend location updates" - }, "snapchat_plus": { "name": "Snapchat Plus", "description": "Enables Snapchat Plus features\nSome Server-sided features may not work" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -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 + 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 spoofLocation = container("spoofLocation", SpoofLocation()) - val suspendLocationUpdates = boolean("suspend_location_updates") { 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() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -85,7 +85,6 @@ class FeatureManager( MediaQualityLevelOverride(), MeoPasscodeBypass(), AppPasscode(), - LocationSpoofer(), CameraTweaks(), InfiniteStoryBoost(), AmoledDarkMode(), @@ -126,6 +125,7 @@ class FeatureManager( AccountSwitcher(), RemoveGroupsLockedStatus(), BypassMessageActionRestrictions(), + BetterLocation(), ) initializeFeatures() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt @@ -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() + }) + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/LocationSpoofer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/LocationSpoofer.kt @@ -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) } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt @@ -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) } }) }