commit 9dd7422b9fc905230f52e7fe1dbef2219228b19d
parent c4b5856ba67868b6767630dbab300de8d55d70b7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun,  2 Jun 2024 23:28:42 +0200

feat: better location overlay

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>

Diffstat:
Aapp/src/main/kotlin/me/rhunk/snapenhance/RemoteLocationManager.kt | 16++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 5+++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 20++++++++++++++------
Mapp/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt | 9++++++++-
Aapp/src/main/kotlin/me/rhunk/snapenhance/storage/Location.kt | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt | 4++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/BetterLocationRoot.kt | 389+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/RemoteOverlay.kt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/SettingsOverlay.kt | 126-------------------------------------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt | 127++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcommon/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 7+++++--
Acommon/src/main/aidl/me/rhunk/snapenhance/bridge/location/FriendLocation.aidl | 13+++++++++++++
Acommon/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationCoordinates.aidl | 11+++++++++++
Acommon/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationManager.aidl | 8++++++++
Mcommon/src/main/assets/lang/en_US.json | 20+++++++++++++++++++-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/ConfigContainer.kt | 6++++++
Acommon/src/main/kotlin/me/rhunk/snapenhance/common/ui/OverlayType.kt | 15+++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt | 8++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt | 195++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt | 3++-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/RandomWalking.kt | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt | 2+-
24 files changed, 979 insertions(+), 263 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteLocationManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteLocationManager.kt @@ -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 } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -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 { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -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) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt @@ -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", + ), )) } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/Location.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/Location.kt @@ -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())) + } +} + + diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt @@ -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 -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt @@ -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 diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/BetterLocationRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/BetterLocationRoot.kt @@ -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") + } + } + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/RemoteOverlay.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/RemoteOverlay.kt @@ -0,0 +1,126 @@ +package me.rhunk.snapenhance.ui.overlay + +import android.app.Dialog +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.provider.Settings +import android.view.WindowManager +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.rememberNavController +import com.arthenica.ffmpegkit.Packages.getPackageName +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 RemoteOverlay( + private val context: RemoteSideContext +) { + private lateinit var dialog: Dialog + private var dismissCallback: (() -> Boolean)? = null + + private fun checkForPermissions(): Boolean { + if (!Settings.canDrawOverlays(context.androidContext)) { + val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) + myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + myIntent.setData(Uri.parse("package:" + getPackageName())) + context.androidContext.startActivity(myIntent) + return false + } + return true + } + + @Composable + private fun OverlayContent(startRoute: (Routes) -> Routes.Route) { + val navHostController = rememberNavController() + + LaunchedEffect(Unit) { + dismissCallback = { navHostController.popBackStack() } + } + + val navigation = remember { Navigation(context, navHostController) } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + topBar = { navigation.TopBar() } + ) { innerPadding -> + navigation.Content( + innerPadding, + startDestination = remember { startRoute(navigation.routes).routeInfo.id } + ) + } + } + + fun close() { + if (!::dialog.isInitialized || !dialog.isShowing) return + dismissCallback = null + context.androidContext.mainExecutor.execute { + dialog.dismiss() + } + } + + fun show(route: (Routes) -> Routes.Route) { + if (!checkForPermissions()) { + return + } + + if (::dialog.isInitialized && dialog.isShowing) { + return + } + + context.androidContext.mainExecutor.execute { + dialog = object: Dialog(context.androidContext, R.style.FullscreenOverlayDialog) { + override fun dismiss() { + dismissCallback?.also { + if (it()) return + } + super.dismiss() + this@RemoteOverlay.context.config.writeConfig() + } + } + dialog.window?.apply { + setBackgroundDrawable(ColorDrawable(Color.Transparent.value.toInt())) + setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + ) + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) + } + + dialog.setContentView( + createComposeView(context.androidContext) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 12.dp, end = 12.dp, top = 10.dp, bottom = 20.dp) + .clip(shape = MaterialTheme.shapes.large), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + OverlayContent(route) + } + } + ) + + dialog.setCancelable(true) + dialog.show() + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/SettingsOverlay.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/overlay/SettingsOverlay.kt @@ -1,125 +0,0 @@ -package me.rhunk.snapenhance.ui.overlay - -import android.app.Dialog -import android.content.Intent -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.provider.Settings -import android.view.WindowManager -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.navigation.compose.rememberNavController -import com.arthenica.ffmpegkit.Packages.getPackageName -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.common.ui.createComposeView -import me.rhunk.snapenhance.ui.manager.Navigation - - -class SettingsOverlay( - private val context: RemoteSideContext -) { - private lateinit var dialog: Dialog - private var dismissCallback: (() -> Boolean)? = null - - private fun checkForPermissions(): Boolean { - if (!Settings.canDrawOverlays(context.androidContext)) { - val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) - myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - myIntent.setData(Uri.parse("package:" + getPackageName())) - context.androidContext.startActivity(myIntent) - return false - } - return true - } - - @Composable - private fun OverlayContent() { - val navHostController = rememberNavController() - - LaunchedEffect(Unit) { - dismissCallback = { navHostController.popBackStack() } - } - - val navigation = remember { Navigation(context, navHostController) } - - Scaffold( - containerColor = MaterialTheme.colorScheme.background, - topBar = { navigation.TopBar() } - ) { innerPadding -> - navigation.Content( - innerPadding, - startDestination = navigation.routes.features.routeInfo.id - ) - } - } - - fun close() { - if (!::dialog.isInitialized || !dialog.isShowing) return - dismissCallback = null - context.androidContext.mainExecutor.execute { - dialog.dismiss() - } - } - - fun show() { - if (!checkForPermissions()) { - return - } - - if (::dialog.isInitialized && dialog.isShowing) { - return - } - - context.androidContext.mainExecutor.execute { - dialog = object: Dialog(context.androidContext, R.style.FullscreenOverlayDialog) { - override fun dismiss() { - dismissCallback?.also { - if (it()) return - } - super.dismiss() - this@SettingsOverlay.context.config.writeConfig() - } - } - dialog.window?.apply { - setBackgroundDrawable(ColorDrawable(Color.Transparent.value.toInt())) - setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT, - ) - clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) - setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) - } - - dialog.setContentView( - createComposeView(context.androidContext) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = 12.dp, end = 12.dp, top = 10.dp, bottom = 20.dp) - .clip(shape = MaterialTheme.shapes.large), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - OverlayContent() - } - } - ) - - dialog.setCancelable(true) - dialog.show() - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt @@ -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"]) + } } } } diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -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); diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/FriendLocation.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/FriendLocation.aidl @@ -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; +} diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationCoordinates.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationCoordinates.aidl @@ -0,0 +1,10 @@ +package me.rhunk.snapenhance.bridge.location; + + +parcelable LocationCoordinates { + int id; + String name; + double latitude; + double longitude; + double radius; +}+ \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationManager.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/location/LocationManager.aidl @@ -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); +}+ \ No newline at end of file diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -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": { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ConfigContainer.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ConfigContainer.kt @@ -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 } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/OverlayType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/OverlayType.kt @@ -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 } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -119,7 +119,7 @@ class SnapEnhance { } hookMainActivity("onPause") { - appContext.bridgeClient.closeSettingsOverlay() + appContext.bridgeClient.closeOverlay() appContext.isMainActivityPaused = true } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -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) } 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 @@ -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 - - 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() - - 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 - } + private val locationHistory = mutableMapOf<String, FriendLocation>() - 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 walkRadius by lazy { + context.config.global.betterLocation.walkRadius.getNullable() + } + + 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) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt @@ -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 -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/RandomWalking.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/RandomWalking.kt @@ -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 + } +}+ \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt @@ -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