mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-29 13:00:17 +02:00
feat(experimental): better location
This commit is contained in:
parent
10bcb93d45
commit
c2816766a8
@ -500,20 +500,36 @@
|
|||||||
"name": "Global",
|
"name": "Global",
|
||||||
"description": "Tweak Global Snapchat Settings",
|
"description": "Tweak Global Snapchat Settings",
|
||||||
"properties": {
|
"properties": {
|
||||||
"spoofLocation": {
|
"better_location": {
|
||||||
"name": "Location",
|
"name": "Better Location",
|
||||||
"description": "Spoof your location",
|
"description": "Enhances the Snapchat Location",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"spoof_location": {
|
||||||
|
"name": "Spoof Location",
|
||||||
|
"description": "Spoofs your location to a specified one"
|
||||||
|
},
|
||||||
"coordinates": {
|
"coordinates": {
|
||||||
"name": "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": {
|
"snapchat_plus": {
|
||||||
"name": "Snapchat Plus",
|
"name": "Snapchat Plus",
|
||||||
"description": "Enables Snapchat Plus features\nSome Server-sided features may not work"
|
"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) {
|
inner class BetterLocation : ConfigContainer(hasGlobalState = true) {
|
||||||
val coordinates = mapCoordinates("coordinates", 0.0 to 0.0) { requireRestart()} // lat, long
|
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 betterLocation = container("better_location", BetterLocation())
|
||||||
val suspendLocationUpdates = boolean("suspend_location_updates") { requireRestart() }
|
|
||||||
val snapchatPlus = boolean("snapchat_plus") { requireRestart() }
|
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 disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
|
||||||
val disableMetrics = boolean("disable_metrics") { requireRestart() }
|
val disableMetrics = boolean("disable_metrics") { requireRestart() }
|
||||||
|
@ -85,7 +85,6 @@ class FeatureManager(
|
|||||||
MediaQualityLevelOverride(),
|
MediaQualityLevelOverride(),
|
||||||
MeoPasscodeBypass(),
|
MeoPasscodeBypass(),
|
||||||
AppPasscode(),
|
AppPasscode(),
|
||||||
LocationSpoofer(),
|
|
||||||
CameraTweaks(),
|
CameraTweaks(),
|
||||||
InfiniteStoryBoost(),
|
InfiniteStoryBoost(),
|
||||||
AmoledDarkMode(),
|
AmoledDarkMode(),
|
||||||
@ -126,6 +125,7 @@ class FeatureManager(
|
|||||||
AccountSwitcher(),
|
AccountSwitcher(),
|
||||||
RemoveGroupsLockedStatus(),
|
RemoveGroupsLockedStatus(),
|
||||||
BypassMessageActionRestrictions(),
|
BypassMessageActionRestrictions(),
|
||||||
|
BetterLocation(),
|
||||||
)
|
)
|
||||||
|
|
||||||
initializeFeatures()
|
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.BridgeFileFeature
|
||||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
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 me.rhunk.snapenhance.core.util.ktx.getId
|
||||||
import java.util.WeakHashMap
|
|
||||||
|
|
||||||
//TODO: bridge shared preferences
|
|
||||||
class SuspendLocationUpdates : BridgeFileFeature(
|
class SuspendLocationUpdates : BridgeFileFeature(
|
||||||
"Suspend Location Updates",
|
"Suspend Location Updates",
|
||||||
loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC,
|
loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC, bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE) {
|
||||||
bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE
|
fun isSuspended() = exists("true")
|
||||||
) {
|
private fun setSuspended(suspended: Boolean) = setState("true", suspended)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityCreate() {
|
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 locationSharingSettingsContainerId = context.resources.getId("location_sharing_settings_container")
|
||||||
val recyclerViewContainerId = context.resources.getId("recycler_view_container")
|
val recyclerViewContainerId = context.resources.getId("recycler_view_container")
|
||||||
@ -64,7 +25,7 @@ class SuspendLocationUpdates : BridgeFileFeature(
|
|||||||
context.event.subscribe(AddViewEvent::class) { event ->
|
context.event.subscribe(AddViewEvent::class) { event ->
|
||||||
if (event.parent.id == locationSharingSettingsContainerId && event.view.id == recyclerViewContainerId) {
|
if (event.parent.id == locationSharingSettingsContainerId && event.view.id == recyclerViewContainerId) {
|
||||||
(event.view as ViewGroup).addView(Switch(event.view.context).apply {
|
(event.view as ViewGroup).addView(Switch(event.view.context).apply {
|
||||||
isChecked = exists("true")
|
isChecked = isSuspended()
|
||||||
ViewAppearanceHelper.applyTheme(this)
|
ViewAppearanceHelper.applyTheme(this)
|
||||||
text = this@SuspendLocationUpdates.context.translation["suspend_location_updates.switch_text"]
|
text = this@SuspendLocationUpdates.context.translation["suspend_location_updates.switch_text"]
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
@ -72,13 +33,7 @@ class SuspendLocationUpdates : BridgeFileFeature(
|
|||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
setOnCheckedChangeListener { _, isChecked ->
|
setOnCheckedChangeListener { _, isChecked ->
|
||||||
if (isChecked) {
|
setSuspended(isChecked)
|
||||||
streamSendHandlerInstanceMap.entries.removeIf { (_, closeStream) ->
|
|
||||||
closeStream()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState("true", isChecked)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user