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:
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)
}
})
}