mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 12:30:12 +02:00
feat(experimental): better location
This commit is contained in:
parent
10bcb93d45
commit
c2816766a8
@ -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"
|
||||
|
@ -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() }
|
||||
|
@ -85,7 +85,6 @@ class FeatureManager(
|
||||
MediaQualityLevelOverride(),
|
||||
MeoPasscodeBypass(),
|
||||
AppPasscode(),
|
||||
LocationSpoofer(),
|
||||
CameraTweaks(),
|
||||
InfiniteStoryBoost(),
|
||||
AmoledDarkMode(),
|
||||
@ -126,6 +125,7 @@ class FeatureManager(
|
||||
AccountSwitcher(),
|
||||
RemoveGroupsLockedStatus(),
|
||||
BypassMessageActionRestrictions(),
|
||||
BetterLocation(),
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user