refactor: location spoofer

This commit is contained in:
rhunk 2023-10-11 16:53:53 +02:00
parent 1a7755e45c
commit 32a458a690
15 changed files with 238 additions and 257 deletions

View File

@ -52,10 +52,6 @@
android:exported="true"
android:theme="@style/AppTheme"
android:excludeFromRecents="true" />
<activity
android:name=".ui.MapActivity"
android:exported="true"
android:excludeFromRecents="true" />
<activity android:name=".bridge.ForceStartActivity"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"

View File

@ -1,98 +0,0 @@
package me.rhunk.snapenhance.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.MotionEvent
import android.widget.Button
import android.widget.EditText
import me.rhunk.snapenhance.R
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.Projection
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Overlay
class MapActivity : Activity() {
private lateinit var mapView: MapView
@SuppressLint("MissingInflatedId", "ResourceType")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val contextBundle = intent.extras?.getBundle("location") ?: return
val locationLatitude = contextBundle.getDouble("latitude")
val locationLongitude = contextBundle.getDouble("longitude")
Configuration.getInstance().load(applicationContext, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
setContentView(R.layout.map)
mapView = findViewById(R.id.mapView)
mapView.setMultiTouchControls(true);
mapView.setTileSource(TileSourceFactory.MAPNIK)
val startPoint = GeoPoint(locationLatitude, locationLongitude)
mapView.controller.setZoom(10.0)
mapView.controller.setCenter(startPoint)
val marker = Marker(mapView)
marker.isDraggable = true
marker.position = startPoint
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
mapView.overlays.add(object: Overlay() {
override fun onSingleTapConfirmed(e: MotionEvent?, mapView: MapView?): Boolean {
val proj: Projection = mapView!!.projection
val loc = proj.fromPixels(e!!.x.toInt(), e.y.toInt()) as GeoPoint
marker.position = loc
mapView.invalidate()
return true
}
})
mapView.overlays.add(marker)
val applyButton = findViewById<Button>(R.id.apply_location_button)
applyButton.setOnClickListener {
val bundle = Bundle()
bundle.putFloat("latitude", marker.position.latitude.toFloat())
bundle.putFloat("longitude", marker.position.longitude.toFloat())
setResult(RESULT_OK, intent.putExtra("location", bundle))
finish()
}
val setPreciseLocationButton = findViewById<Button>(R.id.set_precise_location_button)
setPreciseLocationButton.setOnClickListener {
val locationDialog = layoutInflater.inflate(R.layout.precise_location_dialog, null)
val dialogLatitude = locationDialog.findViewById<EditText>(R.id.dialog_latitude).also { it.setText(marker.position.latitude.toString()) }
val dialogLongitude = locationDialog.findViewById<EditText>(R.id.dialog_longitude).also { it.setText(marker.position.longitude.toString()) }
AlertDialog.Builder(this)
.setView(locationDialog)
.setTitle("Set a precise location")
.setPositiveButton("Set") { _, _ ->
val latitude = dialogLatitude.text.toString().toDoubleOrNull()
val longitude = dialogLongitude.text.toString().toDoubleOrNull()
if (latitude != null && longitude != null) {
val preciseLocation = GeoPoint(latitude, longitude)
mapView.controller.setCenter(preciseLocation)
marker.position = preciseLocation
mapView.invalidate()
}
}.setNegativeButton("Cancel") { _, _ -> }.show()
}
}
override fun onDestroy() {
super.onDestroy()
mapView.onDetach()
}
}

View File

@ -149,7 +149,10 @@ class FeaturesSection : Section() {
if (showDialog) {
Dialog(
onDismissRequest = { showDialog = false }
properties = DialogProperties(
usePlatformDefaultWidth = false
),
onDismissRequest = { showDialog = false },
) {
dialogComposable()
}
@ -182,6 +185,24 @@ class FeaturesSection : Section() {
)
}
DataProcessors.Type.MAP_COORDINATES -> {
registerDialogOnClickCallback()
dialogComposable = {
alertDialogs.ChooseLocationDialog(property) {
showDialog = false
}
}
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.widthIn(0.dp, 120.dp),
text = (propertyValue.get() as Pair<*, *>).let {
"${it.first.toString().toFloatOrNull() ?: 0F}, ${it.second.toString().toFloatOrNull() ?: 0F}"
}
)
}
DataProcessors.Type.STRING_UNIQUE_SELECTION -> {
registerDialogOnClickCallback()

View File

@ -1,41 +1,52 @@
package me.rhunk.snapenhance.ui.util
import android.content.Context
import android.view.MotionEvent
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.common.config.DataProcessors
import me.rhunk.snapenhance.common.config.PropertyPair
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Overlay
class AlertDialogs(
private val translation: LocaleWrapper,
){
@Composable
fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) {
fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.padding(10.dp, 5.dp, 10.dp, 10.dp),
.padding(10.dp, 5.dp, 10.dp, 10.dp)
.then(modifier),
) {
Column(
modifier = Modifier
@ -195,7 +206,9 @@ class AlertDialogs(
)
Row(
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(),
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = { dismiss() }) {
@ -252,7 +265,9 @@ class AlertDialogs(
)
Row(
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(),
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = { onDismiss() }) {
@ -305,4 +320,145 @@ class AlertDialogs(
}
}
}
@Composable
fun ChooseLocationDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) {
val coordinates = remember {
(property.value.get() as Pair<*, *>).let {
it.first.toString().toDouble() to it.second.toString().toDouble()
}
}
val context = LocalContext.current
LaunchedEffect(Unit) {
Configuration.getInstance().load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
}
var marker by remember { mutableStateOf<Marker?>(null) }
val mapView = remember {
MapView(context).apply {
setMultiTouchControls(true)
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
setTileSource(TileSourceFactory.MAPNIK)
val startPoint = GeoPoint(coordinates.first, coordinates.second)
controller.setZoom(10.0)
controller.setCenter(startPoint)
marker = Marker(this).apply {
isDraggable = true
position = startPoint
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
}
overlays.add(object: Overlay() {
override fun onSingleTapConfirmed(e: MotionEvent, mapView: MapView): Boolean {
marker?.position = mapView.projection.fromPixels(e.x.toInt(), e.y.toInt()) as GeoPoint
mapView.invalidate()
return true
}
})
overlays.add(marker)
}
}
DisposableEffect(Unit) {
onDispose {
mapView.onDetach()
}
}
var customCoordinatesDialog by remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.9f),
) {
AndroidView(
factory = { mapView }
)
Row(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
FilledIconButton(
onClick = {
val lat = marker?.position?.latitude ?: coordinates.first
val lon = marker?.position?.longitude ?: coordinates.second
property.value.setAny(lat to lon)
dismiss()
}) {
Icon(
modifier = Modifier
.size(60.dp)
.padding(5.dp),
imageVector = Icons.Filled.Check,
contentDescription = null
)
}
FilledIconButton(
onClick = {
customCoordinatesDialog = true
}) {
Icon(
modifier = Modifier
.size(60.dp)
.padding(5.dp),
imageVector = Icons.Filled.Edit,
contentDescription = null
)
}
}
if (customCoordinatesDialog) {
val lat = remember { mutableStateOf(coordinates.first.toString()) }
val lon = remember { mutableStateOf(coordinates.second.toString()) }
DefaultDialogCard(
modifier = Modifier.align(Alignment.Center)
) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 10.dp),
value = lat.value,
onValueChange = { lat.value = it },
label = { Text(text = "Latitude") },
singleLine = true
)
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 10.dp),
value = lon.value,
onValueChange = { lon.value = it },
label = { Text(text = "Longitude") },
singleLine = true
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = {
customCoordinatesDialog = false
}) {
Text(text = translation["button.cancel"])
}
Button(onClick = {
marker?.position = GeoPoint(lat.value.toDouble(), lon.value.toDouble())
mapView.controller.setCenter(marker?.position)
mapView.invalidate()
customCoordinatesDialog = false
}) {
Text(text = translation["button.ok"])
}
}
}
}
}
}
}

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.osmdroid.views.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</org.osmdroid.views.MapView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/set_precise_location_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="left"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:padding="10dp"
android:background="@android:color/white"
android:text="Set Precise Location"
android:textSize="20sp"
tools:ignore="HardcodedText,RtlHardcoded" />
<Button
android:id="@+id/apply_location_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:background="@android:color/white"
android:text="Apply"
android:textSize="20sp"
tools:ignore="HardcodedText,RtlHardcoded" />
</FrameLayout>
</FrameLayout>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:orientation="vertical"
tools:ignore="HardcodedText">
<EditText
android:id="@+id/dialog_latitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:hint="Latitude"
android:inputType="number|numberDecimal|numberSigned" />
<EditText
android:id="@+id/dialog_longitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:hint="Longitude"
android:inputType="number|numberDecimal|numberSigned" />
</LinearLayout>

View File

@ -356,6 +356,16 @@
"name": "Global",
"description": "Tweak Global Snapchat Settings",
"properties": {
"spoofLocation": {
"name": "Location",
"description": "Spoof your location",
"properties": {
"coordinates": {
"name": "Coordinates",
"description": "Set the coordinates"
}
}
},
"snapchat_plus": {
"name": "Snapchat Plus",
"description": "Enables Snapchat Plus features\nSome Server-sided features may not work"
@ -464,20 +474,6 @@
"name": "Spoof",
"description": "Spoof various information about you",
"properties": {
"location": {
"name": "Location",
"description": "Spoof your location",
"properties": {
"location_latitude": {
"name": "Latitude",
"description": "The latitude of the location"
},
"location_longitude": {
"name": "Longitude",
"description": "The longitude of the location"
}
}
},
"device": {
"name": "Device",
"description": "Spoof your device information",

View File

@ -8,8 +8,7 @@ enum class EnumAction(
val isCritical: Boolean = false,
) {
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true),
EXPORT_CHAT_MESSAGES("export_chat_messages"),
OPEN_MAP("open_map");
EXPORT_CHAT_MESSAGES("export_chat_messages");
companion object {
const val ACTION_PARAMETER = "se_action"

View File

@ -60,6 +60,12 @@ open class ConfigContainer(
container.parentContainerKey = it
}.get()
protected fun mapCoordinates(
key: String,
defaultValue: Pair<Double, Double> = 0.0 to 0.0,
params: ConfigParamsBuilder = {}
) = registerProperty(key, DataProcessors.MAP_COORDINATES, PropertyValue(defaultValue), params)
fun toJson(): JsonObject {
val json = JsonObject()
properties.forEach { (propertyKey, propertyValue) ->

View File

@ -14,6 +14,7 @@ object DataProcessors {
FLOAT,
STRING_MULTIPLE_SELECTION,
STRING_UNIQUE_SELECTION,
MAP_COORDINATES,
CONTAINER,
}
@ -75,6 +76,20 @@ object DataProcessors {
deserialize = { obj -> obj.takeIf { !it.isJsonNull }?.asString }
)
val MAP_COORDINATES = PropertyDataProcessor(
type = Type.MAP_COORDINATES,
serialize = {
JsonObject().apply {
addProperty("lat", it.first)
addProperty("lng", it.second)
}
},
deserialize = { obj ->
val jsonObject = obj.asJsonObject
jsonObject["lat"].asDouble to jsonObject["lng"].asDouble
},
)
fun <T : ConfigContainer> container(container: T) = PropertyDataProcessor(
type = Type.CONTAINER,
serialize = {

View File

@ -4,6 +4,10 @@ import me.rhunk.snapenhance.common.config.ConfigContainer
import me.rhunk.snapenhance.common.config.FeatureNotice
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())
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads")

View File

@ -4,12 +4,6 @@ import me.rhunk.snapenhance.common.config.ConfigContainer
import me.rhunk.snapenhance.common.config.FeatureNotice
class Spoof : ConfigContainer() {
inner class Location : ConfigContainer(hasGlobalState = true) {
val latitude = float("location_latitude")
val longitude = float("location_longitude")
}
val location = container("location", Location())
inner class Device : ConfigContainer(hasGlobalState = true) {
val fingerprint = string("fingerprint")
val androidId = string("android_id")

View File

@ -1,21 +0,0 @@
package me.rhunk.snapenhance.core.action.impl
import android.content.Intent
import android.os.Bundle
import me.rhunk.snapenhance.common.BuildConfig
import me.rhunk.snapenhance.core.action.AbstractAction
class OpenMap: AbstractAction() {
override fun run() {
context.runOnUiThread {
val mapActivityIntent = Intent()
mapActivityIntent.setClassName(BuildConfig.APPLICATION_ID, BuildConfig.APPLICATION_ID + ".ui.MapActivity")
mapActivityIntent.putExtra("location", Bundle().apply {
putDouble("latitude", context.config.experimental.spoof.location.latitude.get().toDouble())
putDouble("longitude", context.config.experimental.spoof.location.longitude.get().toDouble())
})
context.mainActivity!!.startActivityForResult(mapActivityIntent, 0x1337)
}
}
}

View File

@ -1,41 +1,28 @@
package me.rhunk.snapenhance.core.features.impl.global
import android.content.Intent
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.Hooker
import me.rhunk.snapenhance.core.util.hook.hook
class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {
Hooker.hook(context.mainActivity!!.javaClass, "onActivityResult", HookStage.BEFORE) { param ->
val intent = param.argNullable<Intent>(2) ?: return@hook
val bundle = intent.getBundleExtra("location") ?: return@hook
param.setResult(null)
class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() {
if (context.config.global.spoofLocation.globalState != true) return
with(context.config.experimental.spoof.location) {
latitude.set(bundle.getFloat("latitude"))
longitude.set(bundle.getFloat("longitude"))
val coordinates by context.config.global.spoofLocation.coordinates
context.longToast("Location set to $latitude, $longitude")
}
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) }
}
if (context.config.experimental.spoof.location.globalState != true) return
val latitude by context.config.experimental.spoof.location.latitude
val longitude by context.config.experimental.spoof.location.longitude
val locationClass = android.location.Location::class.java
val locationManagerClass = android.location.LocationManager::class.java
locationClass.hook("getLatitude", HookStage.BEFORE) { it.setResult(latitude.toDouble()) }
locationClass.hook("getLongitude", HookStage.BEFORE) { it.setResult(longitude.toDouble()) }
locationClass.hook("getAccuracy", HookStage.BEFORE) { it.setResult(0.0F) }
//Might be redundant because it calls isProviderEnabledForUser which we also hook, meaning if isProviderEnabledForUser returns true this will also return true
locationManagerClass.hook("isProviderEnabled", HookStage.BEFORE) { it.setResult(true) }
locationManagerClass.hook("isProviderEnabledForUser", HookStage.BEFORE) { it.setResult(true) }
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

@ -5,7 +5,6 @@ import me.rhunk.snapenhance.common.action.EnumAction
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.action.impl.CleanCache
import me.rhunk.snapenhance.core.action.impl.ExportChatMessages
import me.rhunk.snapenhance.core.action.impl.OpenMap
import me.rhunk.snapenhance.core.manager.Manager
class ActionManager(
@ -16,7 +15,6 @@ class ActionManager(
mapOf(
EnumAction.CLEAN_CACHE to CleanCache::class,
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages::class,
EnumAction.OPEN_MAP to OpenMap::class,
).map {
it.key to it.value.java.getConstructor().newInstance().apply {
this.context = modContext