feat: better location overlay
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package me.rhunk.snapenhance
|
||||
|
||||
import me.rhunk.snapenhance.bridge.location.FriendLocation
|
||||
import me.rhunk.snapenhance.bridge.location.LocationManager
|
||||
|
||||
class RemoteLocationManager(
|
||||
private val remoteSideContext: RemoteSideContext
|
||||
): LocationManager.Stub() {
|
||||
var friendsLocation = listOf<FriendLocation>()
|
||||
private set
|
||||
|
||||
override fun provideFriendsLocation(friendsLocation: List<FriendLocation>) {
|
||||
this.friendsLocation = friendsLocation.sortedBy { -it.lastUpdated }
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||
import me.rhunk.snapenhance.ui.manager.data.ModInfo
|
||||
import me.rhunk.snapenhance.ui.manager.data.PlatformInfo
|
||||
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
|
||||
import me.rhunk.snapenhance.ui.overlay.SettingsOverlay
|
||||
import me.rhunk.snapenhance.ui.overlay.RemoteOverlay
|
||||
import me.rhunk.snapenhance.ui.setup.Requirements
|
||||
import me.rhunk.snapenhance.ui.setup.SetupActivity
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -70,11 +70,12 @@ class RemoteSideContext(
|
||||
val streaksReminder = StreaksReminder(this)
|
||||
val log = LogManager(this)
|
||||
val scriptManager = RemoteScriptManager(this)
|
||||
val settingsOverlay = SettingsOverlay(this)
|
||||
val remoteOverlay = RemoteOverlay(this)
|
||||
val e2eeImplementation = E2EEImplementation(this)
|
||||
val messageLogger by lazy { LoggerWrapper(androidContext) }
|
||||
val tracker = RemoteTracker(this)
|
||||
val accountStorage = RemoteAccountStorage(this)
|
||||
val locationManager = RemoteLocationManager(this)
|
||||
|
||||
//used to load bitmoji selfies and download previews
|
||||
val imageLoader by lazy {
|
||||
|
||||
@@ -12,6 +12,7 @@ import me.rhunk.snapenhance.common.data.MessagingFriendInfo
|
||||
import me.rhunk.snapenhance.common.data.MessagingGroupInfo
|
||||
import me.rhunk.snapenhance.common.data.SocialScope
|
||||
import me.rhunk.snapenhance.common.logger.LogLevel
|
||||
import me.rhunk.snapenhance.common.ui.OverlayType
|
||||
import me.rhunk.snapenhance.common.util.toParcelable
|
||||
import me.rhunk.snapenhance.download.DownloadProcessor
|
||||
import me.rhunk.snapenhance.download.FFMpegProcessor
|
||||
@@ -201,24 +202,31 @@ class BridgeService : Service() {
|
||||
override fun getTracker() = remoteSideContext.tracker
|
||||
override fun getAccountStorage() = remoteSideContext.accountStorage
|
||||
override fun getFileHandleManager() = remoteSideContext.fileHandleManager
|
||||
override fun getLocationManager() = remoteSideContext.locationManager
|
||||
|
||||
override fun registerMessagingBridge(bridge: MessagingBridge) {
|
||||
messagingBridge = bridge
|
||||
}
|
||||
|
||||
override fun openSettingsOverlay() {
|
||||
override fun openOverlay(type: String) {
|
||||
runCatching {
|
||||
remoteSideContext.settingsOverlay.show()
|
||||
val overlayType = OverlayType.fromKey(type) ?: throw IllegalArgumentException("Unknown overlay type: $type")
|
||||
remoteSideContext.remoteOverlay.show { routes ->
|
||||
when (overlayType) {
|
||||
OverlayType.SETTINGS -> routes.features
|
||||
OverlayType.BETTER_LOCATION -> routes.betterLocation
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
remoteSideContext.log.error("Failed to open settings overlay", it)
|
||||
remoteSideContext.log.error("Failed to open $type overlay", it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeSettingsOverlay() {
|
||||
override fun closeOverlay() {
|
||||
runCatching {
|
||||
remoteSideContext.settingsOverlay.close()
|
||||
remoteSideContext.remoteOverlay.close()
|
||||
}.onFailure {
|
||||
remoteSideContext.log.error("Failed to close settings overlay", it)
|
||||
remoteSideContext.log.error("Failed to close overlay", it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,14 @@ class AppDatabase(
|
||||
"quick_tiles" to listOf(
|
||||
"key VARCHAR PRIMARY KEY",
|
||||
"position INTEGER",
|
||||
)
|
||||
),
|
||||
"location_coordinates" to listOf(
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
||||
"name VARCHAR",
|
||||
"latitude DOUBLE",
|
||||
"longitude DOUBLE",
|
||||
"radius DOUBLE",
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
61
app/src/main/kotlin/me/rhunk/snapenhance/storage/Location.kt
Normal file
61
app/src/main/kotlin/me/rhunk/snapenhance/storage/Location.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package me.rhunk.snapenhance.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.rhunk.snapenhance.bridge.location.LocationCoordinates
|
||||
import me.rhunk.snapenhance.common.util.ktx.getDoubleOrNull
|
||||
import me.rhunk.snapenhance.common.util.ktx.getInteger
|
||||
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
|
||||
|
||||
|
||||
fun AppDatabase.getLocationCoordinates(): List<LocationCoordinates> {
|
||||
return runBlocking(executor.asCoroutineDispatcher()) {
|
||||
database.rawQuery("SELECT * FROM location_coordinates ORDER BY id DESC", null).use { cursor ->
|
||||
val locationCoordinates = mutableListOf<LocationCoordinates>()
|
||||
while (cursor.moveToNext()) {
|
||||
locationCoordinates.add(
|
||||
LocationCoordinates().run {
|
||||
id = cursor.getInteger("id")
|
||||
name = cursor.getStringOrNull("name") ?: return@run null
|
||||
latitude = cursor.getDoubleOrNull("latitude") ?: return@run null
|
||||
longitude = cursor.getDoubleOrNull("longitude") ?: return@run null
|
||||
radius = cursor.getDoubleOrNull("radius") ?: return@run null
|
||||
this
|
||||
} ?: continue
|
||||
)
|
||||
}
|
||||
locationCoordinates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AppDatabase.addOrUpdateLocationCoordinate(id: Int?, locationCoordinates: LocationCoordinates): Int {
|
||||
return runBlocking(executor.asCoroutineDispatcher()) {
|
||||
if (id == null) {
|
||||
val resultId = database.insert("location_coordinates", null, ContentValues().apply {
|
||||
put("name", locationCoordinates.name)
|
||||
put("latitude", locationCoordinates.latitude)
|
||||
put("longitude", locationCoordinates.longitude)
|
||||
put("radius", locationCoordinates.radius)
|
||||
})
|
||||
resultId.toInt()
|
||||
} else {
|
||||
database.update("location_coordinates", ContentValues().apply {
|
||||
put("name", locationCoordinates.name)
|
||||
put("latitude", locationCoordinates.latitude)
|
||||
put("longitude", locationCoordinates.longitude)
|
||||
put("radius", locationCoordinates.radius)
|
||||
}, "id = ?", arrayOf(id.toString()))
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AppDatabase.removeLocationCoordinate(id: Int) {
|
||||
runBlocking(executor.asCoroutineDispatcher()) {
|
||||
database.delete("location_coordinates", "id = ?", arrayOf(id.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class Navigation(
|
||||
fun BottomBar() {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = remember(navBackStackEntry) { routes.getCurrentRoute(navBackStackEntry) }
|
||||
val primaryRoutes = remember { routes.getRoutes().filter { it.routeInfo.primary } }
|
||||
val primaryRoutes = remember { routes.getRoutes().filter { it.routeInfo.showInNavBar } }
|
||||
|
||||
NavigationBar {
|
||||
primaryRoutes.forEach { route ->
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import me.rhunk.snapenhance.RemoteSideContext
|
||||
import me.rhunk.snapenhance.ui.manager.pages.BetterLocationRoot
|
||||
import me.rhunk.snapenhance.ui.manager.pages.FileImportsRoot
|
||||
import me.rhunk.snapenhance.ui.manager.pages.LoggerHistoryRoot
|
||||
import me.rhunk.snapenhance.ui.manager.pages.TasksRoot
|
||||
@@ -36,6 +37,7 @@ data class RouteInfo(
|
||||
val key: String = id,
|
||||
val icon: ImageVector = Icons.Default.Home,
|
||||
val primary: Boolean = false,
|
||||
val showInNavBar: Boolean = primary,
|
||||
) {
|
||||
var translatedKey: Lazy<String?>? = null
|
||||
val childIds = mutableListOf<String>()
|
||||
@@ -67,6 +69,8 @@ class Routes(
|
||||
|
||||
val scripting = route(RouteInfo("scripts", icon = Icons.Filled.DataObject, primary = true), ScriptingRoot())
|
||||
|
||||
val betterLocation = route(RouteInfo("better_location", showInNavBar = false, primary = true), BetterLocationRoot())
|
||||
|
||||
open class Route {
|
||||
open val init: () -> Unit = { }
|
||||
open val title: @Composable (() -> Unit)? = null
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package me.rhunk.snapenhance.ui.manager.pages
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.bridge.location.FriendLocation
|
||||
import me.rhunk.snapenhance.bridge.location.LocationCoordinates
|
||||
import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList
|
||||
import me.rhunk.snapenhance.common.ui.rememberAsyncUpdateDispatcher
|
||||
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
||||
import me.rhunk.snapenhance.storage.addOrUpdateLocationCoordinate
|
||||
import me.rhunk.snapenhance.storage.getLocationCoordinates
|
||||
import me.rhunk.snapenhance.storage.removeLocationCoordinate
|
||||
import me.rhunk.snapenhance.ui.manager.Routes
|
||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||
import me.rhunk.snapenhance.ui.util.DialogProperties
|
||||
import me.rhunk.snapenhance.ui.util.coil.BitmojiImage
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
|
||||
class BetterLocationRoot : Routes.Route() {
|
||||
private val alertDialogs by lazy { AlertDialogs(context.translation) }
|
||||
|
||||
@Composable
|
||||
private fun FriendLocationItem(
|
||||
friendLocation: FriendLocation,
|
||||
dismiss: () -> Unit
|
||||
) {
|
||||
ElevatedCard(onClick = {
|
||||
context.config.root.global.betterLocation.coordinates.setAny(friendLocation.latitude to friendLocation.longitude)
|
||||
dismiss()
|
||||
}, modifier = Modifier.padding(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BitmojiImage(
|
||||
context = context,
|
||||
url = BitmojiSelfie.getBitmojiSelfie(
|
||||
friendLocation.bitmojiSelfieId,
|
||||
friendLocation.bitmojiId,
|
||||
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
|
||||
),
|
||||
size = 48,
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(friendLocation.displayName?.let { "$it (${friendLocation.username})" }
|
||||
?: friendLocation.username, fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
text = buildString {
|
||||
append(friendLocation.localityPieces.joinToString(", "))
|
||||
append("\n")
|
||||
append("Lat: ${friendLocation.latitude.toFloat()}, Lng: ${friendLocation.longitude.toFloat()}")
|
||||
},
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
lineHeight = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FriendLocationsDialogs(
|
||||
friendsLocation: List<FriendLocation>,
|
||||
dismiss: () -> Unit
|
||||
) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
val filteredFriendsLocation = rememberAsyncMutableStateList(defaultValue = friendsLocation, keys = arrayOf(search)) {
|
||||
search.takeIf { it.isNotBlank() }?.let {
|
||||
friendsLocation.filter {
|
||||
it.displayName?.contains(search, ignoreCase = true) == true || it.username.contains(search, ignoreCase = true)
|
||||
}
|
||||
} ?: friendsLocation
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
modifier = Modifier.padding(top = 32.dp, bottom = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
translation["teleport_to_friend_title"],
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
value = search,
|
||||
onValueChange = { search = it },
|
||||
label = { Text(translation["search_bar"]) }
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
if (friendsLocation.isEmpty()) {
|
||||
Text(
|
||||
translation["no_friends_map"],
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
} else if (filteredFriendsLocation.isEmpty()) {
|
||||
Text(
|
||||
translation["no_friends_found"],
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
}
|
||||
}
|
||||
items(filteredFriendsLocation) { friendLocation ->
|
||||
FriendLocationItem(friendLocation, dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val coordinatesProperty = remember {
|
||||
context.config.root.global.betterLocation.getPropertyPair("coordinates")
|
||||
}
|
||||
|
||||
val updateDispatcher = rememberAsyncUpdateDispatcher()
|
||||
val savedCoordinates = rememberAsyncMutableStateList(
|
||||
defaultValue = listOf(),
|
||||
updateDispatcher = updateDispatcher
|
||||
) {
|
||||
context.database.getLocationCoordinates()
|
||||
}
|
||||
var showMap by remember { mutableStateOf(false) }
|
||||
var addSavedCoordinateDialog by remember { mutableStateOf(false) }
|
||||
var showTeleportDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val marker = remember { mutableStateOf<Marker?>(null) }
|
||||
val mapView = remember { mutableStateOf<MapView?>(null) }
|
||||
var spoofedCoordinates by remember(showTeleportDialog, showMap) { mutableStateOf(coordinatesProperty.value.get() as? Pair<*, *>) }
|
||||
|
||||
fun addSavedCoordinate(id: Int?, locationCoordinates: LocationCoordinates) {
|
||||
context.coroutineScope.launch {
|
||||
context.database.addOrUpdateLocationCoordinate(id, locationCoordinates)
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
}
|
||||
|
||||
if (showTeleportDialog) {
|
||||
me.rhunk.snapenhance.ui.util.Dialog(
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
onDismissRequest = { showTeleportDialog = false },
|
||||
content = {
|
||||
FriendLocationsDialogs(remember { context.locationManager.friendsLocation }) {
|
||||
showTeleportDialog = false
|
||||
context.coroutineScope.launch {
|
||||
context.config.writeConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Text(
|
||||
translation.format(
|
||||
"spoofed_coordinates_title",
|
||||
"latitude" to ((spoofedCoordinates?.first as? Double)?.toFloat() ?: "0.0").toString(),
|
||||
"longitude" to ((spoofedCoordinates?.second as? Double)?.toFloat() ?: "0.0").toString()
|
||||
),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
)
|
||||
|
||||
if (addSavedCoordinateDialog) {
|
||||
var savedName by remember { mutableStateOf("") }
|
||||
me.rhunk.snapenhance.ui.util.Dialog(
|
||||
onDismissRequest = { addSavedCoordinateDialog = false },
|
||||
content = {
|
||||
alertDialogs.DefaultDialogCard {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(translation["save_coordinates_dialog_title"], fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
focusRequester.requestFocus()
|
||||
},
|
||||
value = savedName,
|
||||
onValueChange = { savedName = it },
|
||||
label = { Text(translation["saved_name_dialog_hint"]) }
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
addSavedCoordinateDialog = false
|
||||
addSavedCoordinate(null, LocationCoordinates().apply {
|
||||
this.name = savedName
|
||||
this.latitude = marker.value?.position?.latitude as Double
|
||||
this.longitude = marker.value?.position?.longitude as Double
|
||||
})
|
||||
},
|
||||
enabled = savedName.isNotBlank()
|
||||
) {
|
||||
Text(translation["save_dialog_button"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showMap) {
|
||||
me.rhunk.snapenhance.ui.util.Dialog(
|
||||
onDismissRequest = { showMap = false },
|
||||
content = {
|
||||
alertDialogs.ChooseLocationDialog(property = coordinatesProperty, marker, mapView, saveCoordinates = {
|
||||
addSavedCoordinateDialog = true
|
||||
}) {
|
||||
showMap = false
|
||||
context.config.writeConfig()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clipToBounds()
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(onClick = { showMap = true }) {
|
||||
Text(translation["choose_location_button"])
|
||||
}
|
||||
Button(onClick = { showTeleportDialog = true }) {
|
||||
Text(translation["teleport_to_friend_button"])
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = translation["spoof_location_toggle"])
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var isSpoofing by remember { mutableStateOf(context.config.root.global.betterLocation.spoofLocation.get()) }
|
||||
Switch(
|
||||
checked = isSpoofing,
|
||||
onCheckedChange = {
|
||||
isSpoofing = it
|
||||
context.config.root.global.betterLocation.spoofLocation.set(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
translation["saved_coordinates_title"],
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
item {
|
||||
if (savedCoordinates.isEmpty()) {
|
||||
Text(
|
||||
translation["no_saved_coordinates_hint"],
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(start = 20.dp),
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
}
|
||||
}
|
||||
items(savedCoordinates) { coordinates ->
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteDialog) {
|
||||
me.rhunk.snapenhance.ui.util.Dialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
content = {
|
||||
alertDialogs.ConfirmDialog(
|
||||
title = translation["delete_dialog_title"],
|
||||
message = translation["delete_dialog_message"],
|
||||
onConfirm = {
|
||||
showDeleteDialog = false
|
||||
context.coroutineScope.launch {
|
||||
context.database.removeLocationCoordinate(coordinates.id)
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
},
|
||||
onDismiss = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "${coordinates.name} (${coordinates.latitude.toFloat()}, ${coordinates.longitude.toFloat()})",
|
||||
fontWeight = if (spoofedCoordinates == coordinates.latitude to coordinates.longitude) FontWeight.Bold else FontWeight.Light,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
spoofedCoordinates =
|
||||
coordinates.latitude to coordinates.longitude
|
||||
coordinatesProperty.value.setAny(spoofedCoordinates)
|
||||
context.coroutineScope.launch {
|
||||
context.config.writeConfig()
|
||||
}
|
||||
GeoPoint(coordinates.latitude, coordinates.longitude).also {
|
||||
marker.value?.position = it
|
||||
mapView.value?.controller?.apply {
|
||||
animateTo(it)
|
||||
setZoom(16.0)
|
||||
}
|
||||
}
|
||||
},
|
||||
fontSize = 16.sp
|
||||
)
|
||||
FilledIconButton(onClick = {
|
||||
showDeleteDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.DeleteOutline, contentDescription = "Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,10 @@ import me.rhunk.snapenhance.R
|
||||
import me.rhunk.snapenhance.RemoteSideContext
|
||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||
import me.rhunk.snapenhance.ui.manager.Navigation
|
||||
import me.rhunk.snapenhance.ui.manager.Routes
|
||||
|
||||
|
||||
class SettingsOverlay(
|
||||
class RemoteOverlay(
|
||||
private val context: RemoteSideContext
|
||||
) {
|
||||
private lateinit var dialog: Dialog
|
||||
@@ -46,7 +47,7 @@ class SettingsOverlay(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverlayContent() {
|
||||
private fun OverlayContent(startRoute: (Routes) -> Routes.Route) {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -61,7 +62,7 @@ class SettingsOverlay(
|
||||
) { innerPadding ->
|
||||
navigation.Content(
|
||||
innerPadding,
|
||||
startDestination = navigation.routes.features.routeInfo.id
|
||||
startDestination = remember { startRoute(navigation.routes).routeInfo.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +75,7 @@ class SettingsOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
fun show(route: (Routes) -> Routes.Route) {
|
||||
if (!checkForPermissions()) {
|
||||
return
|
||||
}
|
||||
@@ -90,7 +91,7 @@ class SettingsOverlay(
|
||||
if (it()) return
|
||||
}
|
||||
super.dismiss()
|
||||
this@SettingsOverlay.context.config.writeConfig()
|
||||
this@RemoteOverlay.context.config.writeConfig()
|
||||
}
|
||||
}
|
||||
dialog.window?.apply {
|
||||
@@ -113,7 +114,7 @@ class SettingsOverlay(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
OverlayContent()
|
||||
OverlayContent(route)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
@@ -33,15 +34,11 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -467,7 +464,13 @@ class AlertDialogs(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChooseLocationDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) {
|
||||
fun ChooseLocationDialog(
|
||||
property: PropertyPair<*>,
|
||||
marker: MutableState<Marker?> = remember { mutableStateOf(null) },
|
||||
mapView: MutableState<MapView?> = remember { mutableStateOf(null) },
|
||||
saveCoordinates: (() -> Unit)? = null,
|
||||
dismiss: () -> Unit = {}
|
||||
) {
|
||||
val coordinates = remember {
|
||||
(property.value.get() as Pair<*, *>).let {
|
||||
it.first.toString().toDouble() to it.second.toString().toDouble()
|
||||
@@ -475,8 +478,7 @@ class AlertDialogs(
|
||||
}
|
||||
val context = LocalContext.current
|
||||
|
||||
var marker by remember { mutableStateOf<Marker?>(null) }
|
||||
val mapView = remember {
|
||||
mapView.value = remember {
|
||||
Configuration.getInstance().apply {
|
||||
osmdroidBasePath = File(context.cacheDir, "osmdroid")
|
||||
load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
|
||||
@@ -490,7 +492,7 @@ class AlertDialogs(
|
||||
controller.setZoom(10.0)
|
||||
controller.setCenter(startPoint)
|
||||
|
||||
marker = Marker(this).apply {
|
||||
marker.value = Marker(this).apply {
|
||||
isDraggable = true
|
||||
position = startPoint
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
@@ -498,29 +500,32 @@ class AlertDialogs(
|
||||
|
||||
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
|
||||
marker.value?.position = mapView.projection.fromPixels(e.x.toInt(), e.y.toInt()) as GeoPoint
|
||||
mapView.invalidate()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
overlays.add(marker)
|
||||
overlays.add(marker.value)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
mapView.onDetach()
|
||||
mapView.value?.onDetach()
|
||||
}
|
||||
}
|
||||
|
||||
var customCoordinatesDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.9f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds()
|
||||
.fillMaxHeight(fraction = 0.9f),
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { mapView }
|
||||
factory = { mapView.value!! },
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -530,8 +535,8 @@ class AlertDialogs(
|
||||
) {
|
||||
FilledIconButton(
|
||||
onClick = {
|
||||
val lat = marker?.position?.latitude ?: coordinates.first
|
||||
val lon = marker?.position?.longitude ?: coordinates.second
|
||||
val lat = marker.value?.position?.latitude ?: coordinates.first
|
||||
val lon = marker.value?.position?.longitude ?: coordinates.second
|
||||
property.value.setAny(lat to lon)
|
||||
dismiss()
|
||||
}) {
|
||||
@@ -543,6 +548,18 @@ class AlertDialogs(
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
saveCoordinates?.let {
|
||||
FilledIconButton(
|
||||
onClick = { it() }) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(60.dp)
|
||||
.padding(5.dp),
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FilledIconButton(
|
||||
onClick = {
|
||||
@@ -562,44 +579,48 @@ class AlertDialogs(
|
||||
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,
|
||||
Dialog(onDismissRequest = {
|
||||
customCoordinatesDialog = false
|
||||
}) {
|
||||
DefaultDialogCard(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Button(onClick = {
|
||||
customCoordinatesDialog = false
|
||||
}) {
|
||||
Text(text = translation["button.cancel"])
|
||||
}
|
||||
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"])
|
||||
Button(onClick = {
|
||||
marker.value?.position = GeoPoint(lat.value.toDouble(), lon.value.toDouble())
|
||||
mapView.value?.controller?.setCenter(marker.value?.position)
|
||||
mapView.value?.invalidate()
|
||||
customCoordinatesDialog = false
|
||||
}) {
|
||||
Text(text = translation["button.ok"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import me.rhunk.snapenhance.bridge.ConfigStateListener;
|
||||
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge;
|
||||
import me.rhunk.snapenhance.bridge.AccountStorage;
|
||||
import me.rhunk.snapenhance.bridge.storage.FileHandleManager;
|
||||
import me.rhunk.snapenhance.bridge.location.LocationManager;
|
||||
|
||||
interface BridgeInterface {
|
||||
/**
|
||||
@@ -82,11 +83,13 @@ interface BridgeInterface {
|
||||
|
||||
FileHandleManager getFileHandleManager();
|
||||
|
||||
LocationManager getLocationManager();
|
||||
|
||||
oneway void registerMessagingBridge(MessagingBridge bridge);
|
||||
|
||||
oneway void openSettingsOverlay();
|
||||
oneway void openOverlay(String type);
|
||||
|
||||
oneway void closeSettingsOverlay();
|
||||
oneway void closeOverlay();
|
||||
|
||||
oneway void registerConfigStateListener(in ConfigStateListener listener);
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.rhunk.snapenhance.bridge.location;
|
||||
|
||||
parcelable FriendLocation {
|
||||
String username;
|
||||
@nullable String displayName;
|
||||
@nullable String bitmojiId;
|
||||
@nullable String bitmojiSelfieId;
|
||||
double latitude;
|
||||
double longitude;
|
||||
long lastUpdated;
|
||||
String locality;
|
||||
List<String> localityPieces;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package me.rhunk.snapenhance.bridge.location;
|
||||
|
||||
|
||||
parcelable LocationCoordinates {
|
||||
int id;
|
||||
String name;
|
||||
double latitude;
|
||||
double longitude;
|
||||
double radius;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.rhunk.snapenhance.bridge.location;
|
||||
|
||||
import me.rhunk.snapenhance.bridge.location.FriendLocation;
|
||||
|
||||
interface LocationManager {
|
||||
void provideFriendsLocation(in List<FriendLocation> friendsLocation);
|
||||
}
|
||||
@@ -39,7 +39,8 @@
|
||||
"social": "Social",
|
||||
"manage_scope": "Manage Scope",
|
||||
"messaging_preview": "Preview",
|
||||
"scripts": "Scripts"
|
||||
"scripts": "Scripts",
|
||||
"better_location": "Better Location"
|
||||
},
|
||||
"sections": {
|
||||
"home": {
|
||||
@@ -141,6 +142,23 @@
|
||||
"file_imported": "File imported successfully",
|
||||
"file_delete_failed": "Failed to delete file",
|
||||
"no_files_hint": "Here you can import files for use in Snapchat. Press the button below to import a file."
|
||||
},
|
||||
"better_location": {
|
||||
"spoofed_coordinates_title": "Spoofed Coordinates\nLat {latitude}, Lng {longitude}",
|
||||
"save_coordinates_dialog_title": "Save Coordinates",
|
||||
"saved_name_dialog_hint": "Saved Name",
|
||||
"save_dialog_button": "Save",
|
||||
"choose_location_button": "Choose Location",
|
||||
"teleport_to_friend_button": "Teleport to Friend",
|
||||
"spoof_location_toggle": "Spoof Location",
|
||||
"saved_coordinates_title": "Saved Coordinates",
|
||||
"no_saved_coordinates_hint": "No saved coordinates",
|
||||
"delete_dialog_title": "Delete Saved Coordinate",
|
||||
"delete_dialog_message": "Are you sure you want to delete this saved coordinate?",
|
||||
"teleport_to_friend_title": "Teleport to Friend",
|
||||
"search_bar": "Search",
|
||||
"no_friends_map": "No friends on the map",
|
||||
"no_friends_found": "No friends found"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
|
||||
@@ -101,6 +101,12 @@ open class ConfigContainer(
|
||||
}
|
||||
}
|
||||
|
||||
fun getPropertyPair(key: String): PropertyPair<*> {
|
||||
val propertyKey = properties.keys.firstOrNull { it.name == key }
|
||||
?: throw IllegalArgumentException("Property $key not found")
|
||||
return PropertyPair(propertyKey, properties[propertyKey]!!)
|
||||
}
|
||||
|
||||
operator fun getValue(t: Any?, property: KProperty<*>) = this.globalState
|
||||
operator fun setValue(t: Any?, property: KProperty<*>, t1: Boolean?) { this.globalState = t1 }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package me.rhunk.snapenhance.common.ui
|
||||
|
||||
enum class OverlayType(
|
||||
val key: String
|
||||
) {
|
||||
SETTINGS("settings"),
|
||||
BETTER_LOCATION("better_location");
|
||||
|
||||
companion object {
|
||||
fun fromKey(key: String): OverlayType? {
|
||||
return entries.find { it.key == key }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ class SnapEnhance {
|
||||
}
|
||||
|
||||
hookMainActivity("onPause") {
|
||||
appContext.bridgeClient.closeSettingsOverlay()
|
||||
appContext.bridgeClient.closeOverlay()
|
||||
appContext.isMainActivityPaused = true
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import me.rhunk.snapenhance.bridge.*
|
||||
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
|
||||
import me.rhunk.snapenhance.bridge.location.LocationManager
|
||||
import me.rhunk.snapenhance.bridge.logger.LoggerInterface
|
||||
import me.rhunk.snapenhance.bridge.logger.TrackerInterface
|
||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||
@@ -25,6 +26,7 @@ import me.rhunk.snapenhance.common.data.MessagingFriendInfo
|
||||
import me.rhunk.snapenhance.common.data.MessagingGroupInfo
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.data.SocialScope
|
||||
import me.rhunk.snapenhance.common.ui.OverlayType
|
||||
import me.rhunk.snapenhance.common.util.toSerialized
|
||||
import me.rhunk.snapenhance.core.ModContext
|
||||
import java.util.concurrent.Executors
|
||||
@@ -236,10 +238,12 @@ class BridgeClient(
|
||||
|
||||
fun getFileHandlerManager(): FileHandleManager = safeServiceCall { service.fileHandleManager }
|
||||
|
||||
fun getLocationManager(): LocationManager = safeServiceCall { service.locationManager }
|
||||
|
||||
fun registerMessagingBridge(bridge: MessagingBridge) = safeServiceCall { service.registerMessagingBridge(bridge) }
|
||||
|
||||
fun openSettingsOverlay() = safeServiceCall { service.openSettingsOverlay() }
|
||||
fun closeSettingsOverlay() = safeServiceCall { service.closeSettingsOverlay() }
|
||||
fun openOverlay(type: OverlayType) = safeServiceCall { service.openOverlay(type.key) }
|
||||
fun closeOverlay() = safeServiceCall { service.closeOverlay() }
|
||||
|
||||
fun registerConfigStateListener(listener: ConfigStateListener) = safeServiceCall { service.registerConfigStateListener(listener) }
|
||||
|
||||
|
||||
@@ -2,87 +2,72 @@ package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
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.ui.Modifier
|
||||
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
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
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.RandomWalking
|
||||
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.mapper.impl.CallbackMapper
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
data class FriendLocation(
|
||||
val userId: String,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val lastUpdated: Long,
|
||||
val locality: String?,
|
||||
val localityPieces: List<String>
|
||||
)
|
||||
|
||||
class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
//Latitude ft / deg /Longitude ft /deg = 1.26301179736
|
||||
// 4ft/s * 1 degree/364000ft (latitude) * 1s/1000ms = .000000010989011 degrees/ms
|
||||
val max_speed = 4.0 / 364000.0 / 1000.0
|
||||
val pause_chance = .0023 // .23% chance to pause every second = after 5 minutes 50% chance of pause
|
||||
val pause_duration = 60000L //ms
|
||||
val pause_random = 30000L //ms
|
||||
private val locationHistory = mutableMapOf<String, FriendLocation>()
|
||||
|
||||
var pause_expire = 0L
|
||||
var current_x = 0.0
|
||||
var current_y = 0.0
|
||||
var target_x = 0.0
|
||||
var target_y = 0.0
|
||||
var last_update_time = 0L
|
||||
private fun updatePosition(){
|
||||
val now = System.currentTimeMillis()
|
||||
private val walkRadius by lazy {
|
||||
context.config.global.betterLocation.walkRadius.getNullable()
|
||||
}
|
||||
|
||||
if(current_x == target_x && current_y == target_y) {
|
||||
val config = context.config.global.betterLocation
|
||||
val walk_rad = if (config.walkRadius.get()
|
||||
.toDoubleOrNull() == null
|
||||
) 0.0 else (config.walkRadius.get().toDouble() / 364000.0) //Lat deg
|
||||
|
||||
if(last_update_time == 0L){ //Start at random position
|
||||
val radius1 = sqrt(Math.random()) * walk_rad
|
||||
val theta1 = Math.PI * 2.0 * Math.random()
|
||||
current_x = cos(theta1) * radius1 * 1.26301179736
|
||||
current_y = sin(theta1) * radius1
|
||||
}
|
||||
|
||||
val radius2 = sqrt(Math.random()) * walk_rad
|
||||
val theta2 = Math.PI * 2.0 * Math.random()
|
||||
target_x = cos(theta2) * radius2 * 1.26301179736
|
||||
target_y = sin(theta2) * radius2
|
||||
} else if (pause_expire < now) {
|
||||
val deltat = now - last_update_time
|
||||
if(Math.random() > (1.0 - pause_chance).pow(deltat / 1000.0)){
|
||||
pause_expire = now + pause_duration + (pause_random * Math.random()).toLong()
|
||||
} else {
|
||||
val max_dist = max_speed * deltat
|
||||
val dist = hypot(target_x - current_x, target_y - current_y)
|
||||
|
||||
if (dist <= max_dist) {
|
||||
current_x = target_x
|
||||
current_y = target_y
|
||||
} else {
|
||||
val norm_x = (target_x - current_x) / dist * max_dist
|
||||
val norm_y = (target_y - current_y) / dist * max_dist
|
||||
current_x += norm_x
|
||||
current_y += norm_y
|
||||
}
|
||||
}
|
||||
}
|
||||
last_update_time = now
|
||||
private val randomWalking by lazy {
|
||||
RandomWalking(walkRadius?.toDoubleOrNull())
|
||||
}
|
||||
|
||||
private fun getLat() : Double {
|
||||
updatePosition()
|
||||
return (context.config.global.betterLocation.coordinates.get().first + current_x)
|
||||
var spoofedLatitude = context.config.global.betterLocation.coordinates.get().first
|
||||
walkRadius?.let {
|
||||
spoofedLatitude += randomWalking.current_x
|
||||
}
|
||||
return spoofedLatitude
|
||||
}
|
||||
|
||||
private fun getLong() : Double {
|
||||
updatePosition()
|
||||
return (context.config.global.betterLocation.coordinates.get().second + current_y)
|
||||
var spoofedLongitude = context.config.global.betterLocation.coordinates.get().second
|
||||
walkRadius?.let {
|
||||
spoofedLongitude += randomWalking.current_y
|
||||
}
|
||||
return spoofedLongitude
|
||||
}
|
||||
|
||||
private fun editClientUpdate(editor: EditorContext) {
|
||||
val config = context.config.global.betterLocation
|
||||
|
||||
@@ -90,6 +75,7 @@ class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams
|
||||
// SCVSLocationUpdate
|
||||
edit(1) {
|
||||
if (config.spoofLocation.get()) {
|
||||
randomWalking.updatePosition()
|
||||
remove(1)
|
||||
remove(2)
|
||||
addFixed32(1, getLat().toFloat()) // lat
|
||||
@@ -136,6 +122,48 @@ class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocationEvent(protoReader: ProtoReader) {
|
||||
protoReader.eachBuffer(3, 1) {
|
||||
val userId = UUID(getFixed64(1, 1) ?: return@eachBuffer, getFixed64(1, 2) ?: return@eachBuffer).toString()
|
||||
val friendCluster = FriendLocation(
|
||||
userId = userId,
|
||||
latitude = Float.fromBits(getFixed32(4)).toDouble(),
|
||||
longitude = Float.fromBits(getFixed32(5)).toDouble(),
|
||||
lastUpdated = getVarInt(7, 2) ?: -1L,
|
||||
locality = getString(10),
|
||||
localityPieces = mutableListOf<String>().also {
|
||||
forEach { index, wire ->
|
||||
if (index != 11) return@forEach
|
||||
it.add((wire.value as ByteArray).toString(Charsets.UTF_8) )
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
locationHistory[userId] = friendCluster
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
it.bitmojiId = friendInfo.bitmojiAvatarId
|
||||
it.bitmojiSelfieId = friendInfo.bitmojiSelfieId
|
||||
it.latitude = locationHistory.latitude
|
||||
it.longitude = locationHistory.longitude
|
||||
it.lastUpdated = locationHistory.lastUpdated
|
||||
it.locality = locationHistory.locality
|
||||
it.localityPieces = locationHistory.localityPieces
|
||||
}
|
||||
}
|
||||
)
|
||||
context.bridgeClient.openOverlay(OverlayType.BETTER_LOCATION)
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (context.config.global.betterLocation.globalState != true) return
|
||||
|
||||
@@ -145,14 +173,39 @@ class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams
|
||||
hook("isProviderEnabledForUser", HookStage.BEFORE) { it.setResult(true) }
|
||||
}
|
||||
Location::class.java.apply {
|
||||
hook("getLatitude", HookStage.BEFORE) {
|
||||
it.setResult(getLat()) }
|
||||
hook("getLongitude", HookStage.BEFORE) {
|
||||
it.setResult(getLong())
|
||||
}
|
||||
hook("getLatitude", HookStage.BEFORE) { it.setResult(getLat()) }
|
||||
hook("getLongitude", HookStage.BEFORE) { it.setResult(getLong()) }
|
||||
}
|
||||
}
|
||||
|
||||
val mapFeaturesRootId = context.resources.getId("map_features_root")
|
||||
val mapLayerSelectorId = context.resources.getId("map_layer_selector")
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view.id != mapFeaturesRootId) return@subscribe
|
||||
val view = event.view as RelativeLayout
|
||||
|
||||
view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
view.addView(createComposeView(view.context) {
|
||||
FilledIconButton(
|
||||
modifier = Modifier.size(54.dp).padding(8.dp),
|
||||
onClick = { openManagementOverlay() }
|
||||
) {
|
||||
Icon(Icons.Default.EditLocation, contentDescription = null)
|
||||
}
|
||||
}.apply {
|
||||
layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
addRule(RelativeLayout.BELOW, mapLayerSelectorId)
|
||||
addRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) {}
|
||||
})
|
||||
}
|
||||
|
||||
context.event.subscribe(UnaryCallEvent::class) { event ->
|
||||
if (event.uri == "/snapchat.valis.Valis/SendClientUpdate") {
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
@@ -165,6 +218,16 @@ class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams
|
||||
}
|
||||
}
|
||||
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("ServerStreamingEventHandler")?.hook("onEvent", HookStage.BEFORE) { param ->
|
||||
val buffer = param.arg<ByteBuffer>(1).let {
|
||||
it.position(0)
|
||||
ByteArray(it.capacity()).also { buffer -> it.get(buffer); it.position(0) }
|
||||
}
|
||||
onLocationEvent(ProtoReader(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
findClass("com.snapchat.client.grpc.ClientStreamSendHandler\$CppProxy").hook("send", HookStage.BEFORE) { param ->
|
||||
val array = param.arg<ByteBuffer>(0).let {
|
||||
it.position(0)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import me.rhunk.snapenhance.common.ui.OverlayType
|
||||
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDrawable
|
||||
@@ -38,7 +39,7 @@ class SettingsGearInjector : AbstractMenu() {
|
||||
isClickable = true
|
||||
|
||||
setOnClickListener {
|
||||
this@SettingsGearInjector.context.bridgeClient.openSettingsOverlay()
|
||||
this@SettingsGearInjector.context.bridgeClient.openOverlay(OverlayType.SETTINGS)
|
||||
}
|
||||
|
||||
parent.setOnTouchListener { _, event ->
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package me.rhunk.snapenhance.core.util
|
||||
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class RandomWalking(
|
||||
private var walkRadius: Double?,
|
||||
) {
|
||||
var current_x = 0.0
|
||||
private set
|
||||
var current_y = 0.0
|
||||
private set
|
||||
private var last_update_time = 0L
|
||||
private var pause_expire = 0L
|
||||
private var target_x = 0.0
|
||||
private var target_y = 0.0
|
||||
|
||||
fun updatePosition() {
|
||||
//Latitude ft / deg /Longitude ft /deg = 1.26301179736
|
||||
// 4ft/s * 1 degree/364000ft (latitude) * 1s/1000ms = .000000010989011 degrees/ms
|
||||
val max_speed = 4.0 / 364000.0 / 1000.0
|
||||
val pause_chance = .0023 // .23% chance to pause every second = after 5 minutes 50% chance of pause
|
||||
val pause_duration = 60000L //ms
|
||||
val pause_random = 30000L //ms
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
if(current_x == target_x && current_y == target_y) {
|
||||
val walk_rad = if (walkRadius == null
|
||||
) 0.0 else (walkRadius!! / 364000.0) //Lat deg
|
||||
|
||||
if(last_update_time == 0L){ //Start at random position
|
||||
val radius1 = sqrt(Math.random()) * walk_rad
|
||||
val theta1 = Math.PI * 2.0 * Math.random()
|
||||
current_x = cos(theta1) * radius1 * 1.26301179736
|
||||
current_y = sin(theta1) * radius1
|
||||
}
|
||||
|
||||
val radius2 = sqrt(Math.random()) * walk_rad
|
||||
val theta2 = Math.PI * 2.0 * Math.random()
|
||||
target_x = cos(theta2) * radius2 * 1.26301179736
|
||||
target_y = sin(theta2) * radius2
|
||||
} else if (pause_expire < now) {
|
||||
val deltat = now - last_update_time
|
||||
if(Math.random() > (1.0 - pause_chance).pow(deltat / 1000.0)){
|
||||
pause_expire = now + pause_duration + (pause_random * Math.random()).toLong()
|
||||
} else {
|
||||
val max_dist = max_speed * deltat
|
||||
val dist = hypot(target_x - current_x, target_y - current_y)
|
||||
|
||||
if (dist <= max_dist) {
|
||||
current_x = target_x
|
||||
current_y = target_y
|
||||
} else {
|
||||
current_x += (target_x - current_x) / dist * max_dist
|
||||
current_y += (target_y - current_y) / dist * max_dist
|
||||
}
|
||||
}
|
||||
}
|
||||
last_update_time = now
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class CallbackMapper : AbstractClassMapper("Callbacks") {
|
||||
if (clazz.superclass == null) return@filter false
|
||||
|
||||
val superclassName = clazz.getSuperClassName()!!
|
||||
if ((!superclassName.endsWith("Callback") && !superclassName.endsWith("Delegate"))
|
||||
if ((!superclassName.endsWith("Callback") && !superclassName.endsWith("Delegate") && !superclassName.endsWith("EventHandler"))
|
||||
|| superclassName.endsWith("\$Callback")) return@filter false
|
||||
|
||||
if (clazz.getClassName().endsWith("\$CppProxy")) return@filter false
|
||||
|
||||
Reference in New Issue
Block a user