diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/location/BetterLocationRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/location/BetterLocationRoot.kt index 22cf7f0c..6372c451 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/location/BetterLocationRoot.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/location/BetterLocationRoot.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.ui.manager.pages.location import android.os.Parcel -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -9,13 +8,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.DeleteOutline import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.EditLocation import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -102,6 +99,7 @@ class BetterLocationRoot : Routes.Route() { } } ?: friendsLocation } + ElevatedCard( shape = MaterialTheme.shapes.large, modifier = Modifier.padding(top = 32.dp, bottom = 32.dp) @@ -151,24 +149,11 @@ class BetterLocationRoot : Routes.Route() { } } - @Composable - private fun ThemedEditLocationButton(onClick: () -> Unit) { - FilledIconButton( - modifier = Modifier.size(40.dp), - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = if (isSystemInDarkTheme()) Color.White else Color(0xFF151A1A), - ), - onClick = onClick - ) { - Icon(Icons.Default.EditLocation, contentDescription = null) - } - } - override val content: @Composable (NavBackStackEntry) -> Unit = { val coordinatesProperty = remember { context.config.root.global.betterLocation.getPropertyPair("coordinates") } + val updateDispatcher = rememberAsyncUpdateDispatcher() val savedCoordinates = rememberAsyncMutableStateList( defaultValue = listOf(), @@ -179,14 +164,17 @@ class BetterLocationRoot : Routes.Route() { var showMap by remember { mutableStateOf(false) } var addSavedCoordinateDialog by remember { mutableStateOf(false) } var showTeleportDialog by remember { mutableStateOf(false) } + val marker = remember { mutableStateOf(null) } val mapView = remember { mutableStateOf(null) } var spoofedCoordinates by remember(showTeleportDialog, showMap) { mutableStateOf(coordinatesProperty.value.get() as? Pair<*, *>) } + fun addSavedCoordinate(id: Int?, locationCoordinates: LocationCoordinates, onSuccess: suspend (id: Int) -> Unit = {}) { context.coroutineScope.launch { onSuccess(context.database.addOrUpdateLocationCoordinate(id, locationCoordinates)) } } + if (showTeleportDialog) { me.rhunk.snapenhance.ui.util.Dialog( properties = DialogProperties(usePlatformDefaultWidth = false), @@ -201,6 +189,7 @@ class BetterLocationRoot : Routes.Route() { } ) } + Column( modifier = Modifier .fillMaxSize() @@ -218,6 +207,7 @@ class BetterLocationRoot : Routes.Route() { .fillMaxWidth() .padding(8.dp) ) + if (addSavedCoordinateDialog) { me.rhunk.snapenhance.ui.util.Dialog( onDismissRequest = { addSavedCoordinateDialog = false }, @@ -240,6 +230,7 @@ class BetterLocationRoot : Routes.Route() { } ) } + if (showMap) { me.rhunk.snapenhance.ui.util.Dialog( onDismissRequest = { showMap = false }, @@ -258,12 +249,13 @@ class BetterLocationRoot : Routes.Route() { } ) } + LazyColumn( modifier = Modifier .fillMaxSize() - .clipToBounds(), - contentPadding = PaddingValues(bottom = routes.bottomPadding) + .clipToBounds() ) { + item { @Composable fun ConfigToggle( @@ -353,6 +345,7 @@ class BetterLocationRoot : Routes.Route() { val isSelected = spoofedCoordinates == mutableCoordinates.latitude to mutableCoordinates.longitude var showDeleteDialog by remember { mutableStateOf(false) } var showEditDialog by remember { mutableStateOf(false) } + fun setSpoofedCoordinates() { spoofedCoordinates = mutableCoordinates.latitude to mutableCoordinates.longitude coordinatesProperty.value.setAny(spoofedCoordinates) @@ -360,6 +353,7 @@ class BetterLocationRoot : Routes.Route() { context.config.writeConfig() } } + if (showDeleteDialog) { me.rhunk.snapenhance.ui.util.Dialog( onDismissRequest = { showDeleteDialog = false }, @@ -379,6 +373,7 @@ class BetterLocationRoot : Routes.Route() { } ) } + if (showEditDialog) { me.rhunk.snapenhance.ui.util.Dialog( onDismissRequest = { showEditDialog = false }, @@ -406,6 +401,7 @@ class BetterLocationRoot : Routes.Route() { } ) } + ElevatedCard( onClick = { mutableCoordinates = coordinates @@ -451,7 +447,7 @@ class BetterLocationRoot : Routes.Route() { FilledIconButton(onClick = { showEditDialog = true }) { - Icon(Icons.Default.Edit, contentDescription = "Edit") + Icon(Icons.Default.Edit, contentDescription = "Delete") } Spacer(modifier = Modifier.width(4.dp)) FilledIconButton(onClick = { @@ -465,4 +461,4 @@ class BetterLocationRoot : Routes.Route() { } } } -} +} \ No newline at end of file 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 index a1fcb163..b6474b24 100644 --- 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 @@ -5,6 +5,18 @@ import android.location.LocationManager import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.EditLocation +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import me.rhunk.snapenhance.common.ui.OverlayType import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.common.util.protobuf.EditorContext @@ -42,21 +54,26 @@ data class FriendLocation( fun distanceTo(other: FriendLocation): Double { val deltaLat = Math.toRadians(other.latitude - this.latitude) val deltaLong = Math.toRadians(other.longitude - this.longitude) + val a = sin(deltaLat / 2) * sin(deltaLat / 2) + cos(Math.toRadians(this.latitude)) * cos(Math.toRadians(other.latitude)) * sin(deltaLong / 2) * sin(deltaLong / 2) + return 6371 * 2 * atan2(sqrt(a), sqrt(1 - a)) } } class BetterLocation : Feature("Better Location") { val locationHistory = mutableMapOf() + private val walkRadius by lazy { context.config.global.betterLocation.walkRadius.getNullable() } + private val randomWalking by lazy { RandomWalking(walkRadius?.toDoubleOrNull()) } + private fun getLat() : Double { var spoofedLatitude = context.config.global.betterLocation.coordinates.get().first walkRadius?.let { @@ -64,6 +81,7 @@ class BetterLocation : Feature("Better Location") { } return spoofedLatitude } + private fun getLong() : Double { var spoofedLongitude = context.config.global.betterLocation.coordinates.get().second walkRadius?.let { @@ -71,8 +89,10 @@ class BetterLocation : Feature("Better Location") { } return spoofedLongitude } + private fun editClientUpdate(editor: EditorContext) { val config = context.config.global.betterLocation + editor.apply { // SCVSLocationUpdate edit(1) { @@ -83,14 +103,17 @@ class BetterLocation : Feature("Better Location") { addFixed32(1, getLat().toFloat()) // lat addFixed32(2, getLong().toFloat()) // lng } + if (config.alwaysUpdateLocation.get()) { remove(7) addVarInt(7, System.currentTimeMillis()) // timestamp } } + if (context.config.global.betterLocation.suspendLocationUpdates.get()) { remove(1) } + // SCVSDeviceData edit(3) { config.spoofBatteryLevel.getNullable()?.takeIf { it.isNotEmpty() }?.let { @@ -102,12 +125,14 @@ class BetterLocation : Feature("Better Location") { 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 @@ -117,11 +142,14 @@ class BetterLocation : Feature("Better Location") { } } } + private fun onLocationEvent(protoReader: ProtoReader) { protoReader.eachBuffer(3, 1) { val clusterId = UUID(getFixed64(1, 1) ?: return@eachBuffer, getFixed64(1, 2) ?: return@eachBuffer).toString() + val latitude = getFixed32(4)?.let { Float.fromBits(it) }?.toDouble() ?: return@eachBuffer val longitude = getFixed32(5)?.let { Float.fromBits(it) }?.toDouble() ?: return@eachBuffer + val locality = getString(10) val localityPieces = mutableListOf().also { forEach { index, wire -> @@ -129,6 +157,7 @@ class BetterLocation : Feature("Better Location") { it.add((wire.value as ByteArray).toString(Charsets.UTF_8) ) } } + eachBuffer(7) friend@{ val userId = if (contains(1)) UUID(getFixed64(1, 1) ?: return@friend, getFixed64(1, 2) ?: return@friend).toString() else clusterId val friendLocation = FriendLocation( @@ -140,14 +169,17 @@ class BetterLocation : Feature("Better Location") { localityPieces = localityPieces, batteryLevel = getFixed32(13)?.let { Float.fromBits(it) } ?: -1F, ) + locationHistory[userId] = friendLocation } } } + private fun openManagementOverlay() { context.bridgeClient.getLocationManager().provideFriendsLocation( locationHistory.values.toList().mapNotNull { locationHistory -> val friendInfo = context.database.getFriendInfo(locationHistory.userId) ?: return@mapNotNull null + me.rhunk.snapenhance.bridge.location.FriendLocation().also { it.username = friendInfo.mutableUsername ?: return@mapNotNull null it.displayName = friendInfo.displayName @@ -163,9 +195,12 @@ class BetterLocation : Feature("Better Location") { ) context.bridgeClient.openOverlay(OverlayType.BETTER_LOCATION) } + override fun init() { if (context.config.global.betterLocation.globalState != true) return + val canSpoofLocation = { context.config.global.betterLocation.spoofLocation.get() } + LocationManager::class.java.apply { hook("isProviderEnabled", HookStage.BEFORE, { canSpoofLocation() }) { it.setResult(true) } hook("isProviderEnabledForUser", HookStage.BEFORE, { canSpoofLocation() }) { it.setResult(true) } @@ -174,7 +209,9 @@ class BetterLocation : Feature("Better Location") { hook("getLatitude", HookStage.BEFORE, { canSpoofLocation() }) { it.setResult(getLat()) } hook("getLongitude", HookStage.BEFORE, { canSpoofLocation() }) { it.setResult(getLong()) } } + val mapViewId = context.resources.getId("mapview") + if (context.config.global.betterLocation.showBatteryLevel.get()) { findClass("snap.snap_maps_sdk.nano.SnapMapsSdk\$PublicUserInfo").hook("setDisplayName", HookStage.BEFORE) { param -> val instance = param.thisObject() @@ -182,26 +219,55 @@ class BetterLocation : Feature("Better Location") { val batteryLevel = locationHistory[userId]?.batteryLevel?.takeIf { it > -1F } ?: return@hook param.setArg(0, param.arg(0) + " (${(batteryLevel * 100).toInt()}%)") } + findClass("com.snap.map_friend_focus_view.MapFocusViewFriendSectionDataModel").hookConstructor(HookStage.AFTER) { param -> val instance = param.thisObject() val userId = instance.getObjectField("_userId") as? String ?: return@hookConstructor val batteryLevel = locationHistory[userId]?.batteryLevel?.takeIf { it > -1F } ?: return@hookConstructor + param.thisObject().dataBuilder { val prevText = get("_lastSeen")?.let { " - $it" } ?: "" set("_lastSeen", "(${(batteryLevel * 100).toInt()}%)$prevText") } } } + context.event.subscribe(AddViewEvent::class) { event -> if (!event.viewClassName.endsWith("MapScreenRoot")) return@subscribe + event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { - // Overlay Compose UI is now created in the app module only! - // No Compose code or theming here in the core module. + val mapView = event.view.findViewById(mapViewId) ?: throw IllegalStateException("Map view not found") + val view = (mapView.parent as ViewGroup).children().firstOrNull { it is RelativeLayout } as? RelativeLayout ?: throw IllegalStateException("Map view parent not found") + + view.addView(createComposeView(view.context) { + val darkTheme = remember { context.androidContext.isDarkTheme() } + Box( + modifier = Modifier.padding(start = 8.dp) + ) { + FilledIconButton( + modifier = Modifier.size(40.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = if (darkTheme) Color(0xFF1D1D1D) else Color.White, + contentColor = if (darkTheme) Color.White else Color(0xFF151A1A), + ), + onClick = { openManagementOverlay() } + ) { + Icon(Icons.Default.EditLocation, contentDescription = null) + } + } + }.apply { + layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { + addRule(RelativeLayout.ALIGN_PARENT_LEFT) + setMargins(0, (60 * context.resources.displayMetrics.density).toInt(), 0, 0) + } + }) } + override fun onViewDetachedFromWindow(v: View) {} }) } + context.event.subscribe(UnaryCallEvent::class) { event -> if (event.uri == "/snapchat.valis.Valis/SendClientUpdate") { event.buffer = ProtoEditor(event.buffer).apply { @@ -213,6 +279,7 @@ class BetterLocation : Feature("Better Location") { }.toByteArray() } } + context.mappings.useMapper(CallbackMapper::class) { callbacks.getClass("ServerStreamingEventHandler")?.hook("onEvent", HookStage.BEFORE) { param -> val buffer = param.argNullable(1)?.let { @@ -222,11 +289,13 @@ class BetterLocation : Feature("Better Location") { onLocationEvent(ProtoReader(buffer)) } } + findClass("com.snapchat.client.grpc.ClientStreamSendHandler\$CppProxy").hook("send", HookStage.BEFORE) { param -> val array = param.arg(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) @@ -236,4 +305,4 @@ class BetterLocation : Feature("Better Location") { }) } } -} +} \ No newline at end of file