diff --git a/app/build.gradle b/app/build.gradle index 969629d..dc21096 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,9 @@ plugins { } ext.versionMajor = 1 -ext.versionMinor = 1 -ext.versionPatch = 6 -ext.grindrVersion = '8.23.0' +ext.versionMinor = 2 +ext.versionPatch = 4 +ext.grindrVersion = '9.7.0' private String genVersionName() { String versionName = "${ext.versionMajor}.${ext.versionMinor}.${ext.versionPatch}" @@ -18,12 +18,12 @@ private String getVersionNameSuffix() { } android { - compileSdk 32 + compileSdk 33 defaultConfig { applicationId 'com.eljaviluki.grindrplus' minSdk 21 - targetSdk 32 + targetSdk 33 versionCode 14 versionName genVersionName() versionNameSuffix getVersionNameSuffix() @@ -42,6 +42,7 @@ android { kotlinOptions { jvmTarget = '1.8' } + namespace 'com.eljaviluki.grindrplus' } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2a581f..c7776e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + + //val profile = Profile(it) + addProfileFieldUi("Profile ID", profileId, 0).also { view -> view.setOnLongClickListener { - val clipboard = Hooker.appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Profile ID", profile.profileId) + val clipboard = + Hooker.appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Profile ID", profileId) clipboard.setPrimaryClip(clip) - Toast.makeText(Hooker.appContext, "Profile ID copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText( + Hooker.appContext, + "Profile ID copied to clipboard", + Toast.LENGTH_SHORT + ).show() true } } - addProfileFieldUi("Last Seen", if (profile.seen != 0L) Utils.toReadableDate(profile.seen) else "N/A", 1) + /*addProfileFieldUi( + "Last Seen", + if (profile.seen != 0L) Utils.toReadableDate(profile.seen) else "N/A", + 1 + ) if (profile.weight != 0.0 && profile.height != 0.0) - addProfileFieldUi("Body Mass Index", Utils.getBmiDescription(profile.weight, profile.height), 2) + addProfileFieldUi( + "Body Mass Index", + Utils.getBmiDescription(profile.weight, profile.height), + 2 + )*/ } //.setVisibility() of param.thisObject to always VISIBLE (otherwise if the profile has no fields, the additional ones will not be shown) @@ -141,8 +170,15 @@ object Hooks { } //By default, the views are added to the end of the list. - private fun addProfileFieldUi(label: CharSequence, value: CharSequence, where: Int = -1) : FrameLayout { - val hooked = XposedBridge.hookMethod(checkNotNullParameterMethod, XC_MethodReplacement.DO_NOTHING) + private fun addProfileFieldUi( + label: CharSequence, + value: CharSequence, + where: Int = -1 + ): FrameLayout { + val hooked = XposedBridge.hookMethod( + checkNotNullParameterMethod, + XC_MethodReplacement.DO_NOTHING + ) val extendedProfileFieldView = newInstance(class_ExtendedProfileFieldView, context, null as AttributeSet?) hooked.unhook() @@ -379,14 +415,14 @@ object Hooks { "android.location.Location", Hooker.pkgParam.classLoader ) - + findAndHookMethod( class_Location, "isFromMockProvider", RETURN_FALSE ) - if(Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= 31) { findAndHookMethod( class_Location, "isMock", @@ -413,9 +449,13 @@ object Hooks { * * @author ElJaviLuki */ - fun hookOnlineIndicatorDuration(duration : Duration){ + fun hookOnlineIndicatorDuration(duration: Duration) { val class_ProfileUtils = findClass(GApp.utils.ProfileUtils, Hooker.pkgParam.classLoader) - setStaticLongField(class_ProfileUtils, GApp.utils.ProfileUtils_.onlineIndicatorDuration, duration.inWholeMilliseconds) + setStaticLongField( + class_ProfileUtils, + GApp.utils.ProfileUtils_.onlineIndicatorDuration, + duration.inWholeMilliseconds + ) } /** @@ -425,9 +465,13 @@ object Hooks { */ fun unlimitedTaps() { val class_TapsAnimLayout = findClass(GApp.view.TapsAnimLayout, Hooker.pkgParam.classLoader) - val class_ChatMessage = findClass(GApp.persistence.model.ChatMessage, Hooker.pkgParam.classLoader) + val class_ChatMessage = + findClass(GApp.persistence.model.ChatMessage, Hooker.pkgParam.classLoader) - val tapTypeToHook = getStaticObjectField(class_ChatMessage, GApp.persistence.model.ChatMessage_.TAP_TYPE_NONE) + val tapTypeToHook = getStaticObjectField( + class_ChatMessage, + GApp.persistence.model.ChatMessage_.TAP_TYPE_NONE + ) //Reset the tap value to allow multitapping. findAndHookMethod( @@ -435,7 +479,7 @@ object Hooks { GApp.view.TapsAnimLayout_.setTapType, String::class.java, Boolean::class.javaPrimitiveType, - object : XC_MethodHook(){ + object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { setObjectField( param.thisObject, @@ -467,7 +511,8 @@ object Hooks { * @author ElJaviLuki */ fun removeExpirationOnExpiringPhotos() { - val class_ExpiringImageBody = findClass(GApp.model.ExpiringImageBody, Hooker.pkgParam.classLoader) + val class_ExpiringImageBody = + findClass(GApp.model.ExpiringImageBody, Hooker.pkgParam.classLoader) findAndHookMethod( class_ExpiringImageBody, GApp.model.ExpiringImageBody_.getDuration, @@ -475,29 +520,33 @@ object Hooks { ) } - fun preventRecordProfileViews(){ - val class_Continuation = findClass( - "kotlin.coroutines.Continuation", - Hooker.pkgParam.classLoader - ) - - val class_GrindrRestService = findClass(GApp.api.GrindrRestService, Hooker.pkgParam.classLoader) + fun preventRecordProfileViews() { findAndHookMethod( - class_GrindrRestService, - GApp.api.GrindrRestService_.recordProfileViews, + GApp.ui.profileV2.ProfilesViewModel, + Hooker.pkgParam.classLoader, + GApp.ui.profileV2.ProfilesViewModel_.recordProfileViewsForViewedMeService, List::class.java, - class_Continuation, XC_MethodReplacement.DO_NOTHING ) + + findAndHookMethod( + GApp.persistence.repository.ProfileRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ProfileRepo_.recordProfileView, + String::class.java, + "kotlin.coroutines.Continuation", + RETURN_UNIT + ) } - fun makeMessagesAlwaysRemovable(){ + fun makeMessagesAlwaysRemovable() { val class_ChatBaseFragmentV2 = findClass( GApp.ui.chat.ChatBaseFragmentV2, Hooker.pkgParam.classLoader ) - val class_ChatMessage = findClass(GApp.persistence.model.ChatMessage, Hooker.pkgParam.classLoader) + val class_ChatMessage = + findClass(GApp.persistence.model.ChatMessage, Hooker.pkgParam.classLoader) findAndHookMethod( class_ChatBaseFragmentV2, GApp.ui.chat.ChatBaseFragmentV2_._canBeUnsent, @@ -506,13 +555,19 @@ object Hooks { ) } + /* fun notifyBlockStatusViaToast() { - val class_BlockByHelper = findClass( - GApp.persistence.cache.BlockByHelper, + val class_BlockedByHelper = findClass( + GApp.persistence.cache.BlockedByHelper, Hooker.pkgParam.classLoader ) - findAndHookMethod(class_BlockByHelper, GApp.persistence.cache.BlockByHelper_.addBlockByProfile, String::class.java, object : XC_MethodHook(){ + val class_Continuation = findClass( + "kotlin.coroutines.Continuation", + Hooker.pkgParam.classLoader + ) + + findAndHookMethod(class_BlockedByHelper, GApp.persistence.cache.BlockByHelper_.addBlockByProfile, String::class.java, class_Continuation, object : XC_MethodHook(){ override fun beforeHookedMethod(param: MethodHookParam?) { val profileId: String = param!!.args[0] as String ContextCompat.getMainExecutor(Hooker.appContext).execute { @@ -521,7 +576,7 @@ object Hooks { } }) - findAndHookMethod(class_BlockByHelper, GApp.persistence.cache.BlockByHelper_.removeBlockByProfile, String::class.java, object : XC_MethodHook(){ + findAndHookMethod(class_BlockedByHelper, GApp.persistence.cache.BlockByHelper_.removeBlockByProfile, String::class.java, class_Continuation, object : XC_MethodHook(){ override fun beforeHookedMethod(param: MethodHookParam?) { val profileId: String = param!!.args[0] as String ContextCompat.getMainExecutor(Hooker.appContext).execute { @@ -530,4 +585,643 @@ object Hooks { } }) } + */ + + fun showBlocksInChat() { + val receiveChatMessage = findMethodExact( + GApp.xmpp.ChatMessageManager, + Hooker.pkgParam.classLoader, + GApp.xmpp.ChatMessageManager_.handleIncomingChatMessage, + GApp.persistence.model.ChatMessage, + Boolean::class.javaPrimitiveType, + Boolean::class.javaPrimitiveType, + ) + + XposedBridge.hookMethod(receiveChatMessage, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val chatMessage = param.args[0] + val type = callMethod( + chatMessage, + GApp.persistence.model.ChatMessage_.getType + ) as String + val syntheticMessage = when (type) { + "block" -> "[You have been blocked.]" + "unblock" -> "[You have been unblocked.]" + else -> null + } + if (syntheticMessage != null) { + val clone = + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.clone) + callMethod(clone, GApp.persistence.model.ChatMessage_.setType, "text") + callMethod( + clone, + GApp.persistence.model.ChatMessage_.setBody, + syntheticMessage + ) + receiveChatMessage.invoke( + param.thisObject, + clone, + param.args[1], + param.args[2] + ) + } + } + }) + + + val Constructor_ChatMessage = findConstructorExact( + GApp.persistence.model.ChatMessage, + Hooker.pkgParam.classLoader + ) + + var ownProfileId: String? = null + + findAndHookMethod( + GApp.storage.UserSession, + Hooker.pkgParam.classLoader, + GApp.storage.IUserSession_.getProfileId, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + ownProfileId = param.result as String + } + } + ) + + var chatMessageManager: Any? = null + + XposedBridge.hookAllConstructors( + findClass( + GApp.xmpp.ChatMessageManager, + Hooker.pkgParam.classLoader + ), + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + chatMessageManager = param.thisObject + } + } + ) + + fun logChatMessage(from: String, text: String) { + val chatMessage = Constructor_ChatMessage.newInstance() + callMethod( + chatMessage, + GApp.persistence.model.ChatMessage_.setMessageId, + UUID.randomUUID().toString() + ) + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setSender, ownProfileId) + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setRecipient, from) + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setStanzaId, from) + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setConversationId, from) + callMethod( + chatMessage, + GApp.persistence.model.ChatMessage_.setTimestamp, + System.currentTimeMillis() + ) + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setType, "text") + callMethod(chatMessage, GApp.persistence.model.ChatMessage_.setBody, text) + callMethod( + chatMessageManager, + GApp.xmpp.ChatMessageManager_.handleIncomingChatMessage, + chatMessage, + false, + false + ) + } + + findAndHookMethod( + GApp.persistence.repository.BlockRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.BlockRepo_.add, + GApp.persistence.model.BlockedProfile, + "kotlin.coroutines.Continuation", + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val otherProfileId = callMethod( + param.args[0], + GApp.persistence.model.BlockedProfile_.getProfileId + ) as String + logChatMessage(otherProfileId, "[You have blocked this profile.]") + } + } + ) + + findAndHookMethod( + GApp.persistence.repository.BlockRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.BlockRepo_.delete, + String::class.java, + "kotlin.coroutines.Continuation", + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val otherProfileId = param.args[0] as? String + if (otherProfileId != null) { + logChatMessage(otherProfileId, "[You have unblocked this profile.]") + } + } + } + ) + } + + fun keepChatsOfBlockedProfiles() { + val ignoreIfBlockInteractor = object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any { + //We still want to allow deleting chats etc., + //so only ignore if BlockInteractor is calling + val isBlockInteractor = + Thread.currentThread().stackTrace.any { + it.className.contains(GApp.manager.BlockInteractor) || + it.className.contains(GApp.ui.chat.BlockViewModel) + } + if (isBlockInteractor) { + return Unit + } + return XposedBridge.invokeOriginalMethod( + param.method, + param.thisObject, + param.args + ) + } + } + + findAndHookMethod( + GApp.persistence.repository.ProfileRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ProfileRepo_.delete, + String::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.ProfileRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ProfileRepo_.delete, + List::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.ConversationRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ConversationRepo_.deleteConversation, + String::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.ConversationRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ConversationRepo_.deleteConversations, + List::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.ChatRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ChatRepo_.deleteChatMessageFromConversationId, + String::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.ChatRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ChatRepo_.deleteChatMessageListFromConversationId, + List::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.persistence.repository.IncomingChatMarkerRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.IncomingChatMarkerRepo_.deleteIncomingChatMarker, + String::class.java, + "kotlin.coroutines.Continuation", + ignoreIfBlockInteractor + ) + + findAndHookMethod( + GApp.ui.chat.individual.ChatIndividualFragment, + Hooker.pkgParam.classLoader, + GApp.ui.chat.individual.ChatIndividualFragment_.showBlockDialog, + Boolean::class.javaPrimitiveType, + XC_MethodReplacement.DO_NOTHING + ) + + val queries = mapOf( + "\n" + + " SELECT * FROM conversation \n" + + " LEFT JOIN blocks ON blocks.profileId = conversation_id\n" + + " LEFT JOIN banned ON banned.profileId = conversation_id\n" + + " WHERE blocks.profileId is NULL AND banned.profileId is NULL\n" + + " ORDER BY conversation.pin DESC, conversation.last_message_timestamp DESC, conversation.conversation_id DESC\n" + + " " + to "\n" + + " SELECT * FROM conversation \n" + + " LEFT JOIN blocks ON blocks.profileId = conversation_id\n" + + " LEFT JOIN banned ON banned.profileId = conversation_id\n" + + " WHERE banned.profileId is NULL\n" + + " ORDER BY conversation.pin DESC, conversation.last_message_timestamp DESC, conversation.conversation_id DESC\n" + + " ", + "\n" + + " SELECT * FROM conversation\n" + + " LEFT JOIN profile ON profile.profile_id = conversation.conversation_id\n" + + " LEFT JOIN blocks ON blocks.profileId = conversation_id\n" + + " LEFT JOIN banned ON banned.profileId = conversation_id\n" + + " WHERE blocks.profileId is NULL AND banned.profileId is NULL AND unread >= :minUnreadCount AND is_group_chat in (:isGroupChat)\n" + + " AND (:minLastSeen = 0 OR seen > :minLastSeen)\n" + + " AND (1 IN (:isFavorite) AND 0 IN (:isFavorite) OR is_favorite in (:isFavorite))\n" + + " ORDER BY conversation.pin DESC, conversation.last_message_timestamp DESC, conversation.conversation_id DESC\n" + + " " + to "\n" + + " SELECT * FROM conversation\n" + + " LEFT JOIN profile ON profile.profile_id = conversation.conversation_id\n" + + " LEFT JOIN blocks ON blocks.profileId = conversation_id\n" + + " LEFT JOIN banned ON banned.profileId = conversation_id\n" + + " WHERE banned.profileId is NULL AND unread >= :minUnreadCount AND is_group_chat in (:isGroupChat)\n" + + " AND (:minLastSeen = 0 OR seen > :minLastSeen)\n" + + " AND (1 IN (:isFavorite) AND 0 IN (:isFavorite) OR is_favorite in (:isFavorite))\n" + + " ORDER BY conversation.pin DESC, conversation.last_message_timestamp DESC, conversation.conversation_id DESC\n" + + " " + ) + + findAndHookMethod("androidx.room.RoomSQLiteQuery", + Hooker.pkgParam.classLoader, + "acquire", + String::class.java, + Int::class.javaPrimitiveType, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val query = param.args[0] + param.args[0] = queries.getOrDefault(query, query) + } + }) + } + + fun localSavedPhrases() { + val class_ChatRestService = + findClass(GApp.api.ChatRestService, Hooker.pkgParam.classLoader) + + val class_PhrasesRestService = + findClass(GApp.api.PhrasesRestService, Hooker.pkgParam.classLoader) + + val createSuccessResult = findMethodExact( + GApp.network.either.ResultHelper, + Hooker.pkgParam.classLoader, + GApp.network.either.ResultHelper_.createSuccess, + Any::class.java + ) + + val constructor_AddSavedPhraseResponse = findConstructorExact( + GApp.model.AddSavedPhraseResponse, + Hooker.pkgParam.classLoader, + String::class.java + ) + + val constructor_PhrasesResponse = findConstructorExact( + GApp.model.PhrasesResponse, + Hooker.pkgParam.classLoader, + Map::class.java + ) + + val constructor_Phrase = findConstructorExact( + GApp.persistence.model.Phrase, + Hooker.pkgParam.classLoader, + String::class.java, + String::class.java, + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + + fun hookChatRestService(service: Any): Any { + val invocationHandler = Proxy.getInvocationHandler(service) + return Proxy.newProxyInstance( + Hooker.pkgParam.classLoader, + arrayOf(class_ChatRestService) + ) { proxy, method, args -> + when (method.name) { + GApp.api.ChatRestService_.addSavedPhrase -> { + val phrase = + getObjectField(args[0], "phrase") as String + val id = Hooker.sharedPref.getInt("id_counter", 0) + 1 + val currentPhrases = + Hooker.sharedPref.getStringSet("phrases", emptySet())!! + Hooker.sharedPref.edit() + .putInt("id_counter", id) + .putStringSet("phrases", currentPhrases + id.toString()) + .putString("phrase_${id}_text", phrase) + .putInt("phrase_${id}_frequency", 0) + .putLong("phrase_${id}_timestamp", 0) + .apply() + val response = + constructor_AddSavedPhraseResponse.newInstance(id.toString()) + createSuccessResult.invoke(null, response) + } + GApp.api.ChatRestService_.deleteSavedPhrase -> { + val id = args[0] as String + val currentPhrases = + Hooker.sharedPref.getStringSet("phrases", emptySet())!! + Hooker.sharedPref.edit() + .putStringSet("phrases", currentPhrases - id) + .remove("phrase_${id}_text") + .remove("phrase_${id}_frequency") + .remove("phrase_${id}_timestamp") + .apply() + createSuccessResult.invoke(null, Unit) + } + GApp.api.ChatRestService_.increaseSavedPhraseClickCount -> { + val id = args[0] as String + val currentFrequency = + Hooker.sharedPref.getInt("phrase_${id}_frequency", 0) + Hooker.sharedPref.edit() + .putInt("phrase_${id}_frequency", currentFrequency + 1) + .apply() + createSuccessResult.invoke(null, Unit) + } + else -> invocationHandler.invoke(proxy, method, args) + } + } + } + + fun hookPhrasesRestService(service: Any): Any { + val invocationHandler = Proxy.getInvocationHandler(service) + return Proxy.newProxyInstance( + Hooker.pkgParam.classLoader, + arrayOf(class_PhrasesRestService) + ) { proxy, method, args -> + when (method.name) { + GApp.api.PhrasesRestService_.getSavedPhrases -> { + val phrases = + Hooker.sharedPref.getStringSet("phrases", emptySet())!! + .map { id -> + val text = Hooker.sharedPref.getString( + "phrase_${id}_text", + "" + ) + val timestamp = Hooker.sharedPref.getLong( + "phrase_${id}_timestamp", + 0 + ) + val frequency = Hooker.sharedPref.getInt( + "phrase_${id}_frequency", + 0 + ) + id to constructor_Phrase.newInstance( + id, + text, + timestamp, + frequency + ) + } + .toMap() + val phrasesResponse = + constructor_PhrasesResponse.newInstance(phrases) + createSuccessResult.invoke(null, phrasesResponse) + } + else -> invocationHandler.invoke(proxy, method, args) + } + } + } + + findAndHookMethod( + "retrofit2.Retrofit", + Hooker.pkgParam.classLoader, + "create", + Class::class.java, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val service = param.result + param.result = when { + class_ChatRestService.isInstance(service) -> hookChatRestService(service) + class_PhrasesRestService.isInstance(service) -> hookPhrasesRestService( + service + ) + else -> service + } + } + } + ) + } + + fun disableAnalytics() { + val class_AnalyticsRestService = + findClass(GApp.api.AnalyticsRestService, Hooker.pkgParam.classLoader) + + val createSuccessResult = findMethodExact( + GApp.network.either.ResultHelper, + Hooker.pkgParam.classLoader, + GApp.network.either.ResultHelper_.createSuccess, + Any::class.java + ) + + findAndHookMethod( + "retrofit2.Retrofit", + Hooker.pkgParam.classLoader, + "create", + Class::class.java, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val service = param.result + param.result = when { + class_AnalyticsRestService.isInstance(service) -> { + Proxy.newProxyInstance( + Hooker.pkgParam.classLoader, + arrayOf(class_AnalyticsRestService) + ) { proxy, method, args -> + //Just block all methods for now, + //in the future we might need to differentiate if they change the service interface. + createSuccessResult(Unit) + } + } + else -> service + } + } + } + ) + } + + fun useThreeColumnLayoutForFavorites() { + val R_id = findClass( + GApp.R.id, + Hooker.pkgParam.classLoader + ) + + val recyclerViewId = getStaticIntField( + R_id, + GApp.R.id_.fragment_favorite_recycler_view + ) + val profileDistanceId = getStaticIntField( + R_id, + GApp.R.id_.profile_distance + ) + val profileOnlineNowIconId = getStaticIntField( + R_id, + GApp.R.id_.profile_online_now_icon + ) + val profileLastSeenId = getStaticIntField( + R_id, + GApp.R.id_.profile_last_seen + ) + val profileNoteIconId = getStaticIntField( + R_id, + GApp.R.id_.profile_note_icon + ) + val profileDisplayNameId = getStaticIntField( + R_id, + GApp.R.id_.profile_display_name + ) + + val Constructor_LayoutParamsRecyclerView = findConstructorExact( + "androidx.recyclerview.widget.RecyclerView\$LayoutParams", + Hooker.pkgParam.classLoader, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + + findAndHookMethod( + GApp.favorites.FavoritesFragment, + Hooker.pkgParam.classLoader, + "onViewCreated", + View::class.java, + Bundle::class.java, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val view = param.args[0] as View + val recyclerView = view.findViewById(recyclerViewId) + val gridLayoutManager = callMethod(recyclerView, "getLayoutManager") + callMethod(gridLayoutManager, "setSpanCount", 3) + + val adapter = callMethod(recyclerView, "getAdapter") + + findAndHookMethod( + adapter::class.java, + "onBindViewHolder", + "androidx.recyclerview.widget.RecyclerView\$ViewHolder", + Int::class.javaPrimitiveType, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + //Adjust grid item size + val size = + Hooker.appContext.resources.displayMetrics.widthPixels / 3 + val rootLayoutParams = + Constructor_LayoutParamsRecyclerView.newInstance( + size, + size + ) as LayoutParams + + val viewHolder = param.args[0] + val itemView = getObjectField(viewHolder, "itemView") as View + + itemView.layoutParams = rootLayoutParams + val distanceTextView = + itemView.findViewById(profileDistanceId) + + //Make online status and distance appear below each other + //because theres not enough space anymore to show them in a single row + val linearLayout = distanceTextView.parent as LinearLayout + linearLayout.orientation = LinearLayout.VERTICAL + + //Adjust layout params because of different orientation of LinearLayout + linearLayout.children.forEach { child -> + child.layoutParams = LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ) + } + + //Align distance TextView left now that it's displayed in its own row + distanceTextView.gravity = Gravity.START + + //Remove ugly margin before last seen text when online indicator is invisible + + val profileOnlineNowIcon = + itemView.findViewById(profileOnlineNowIconId) + val profileLastSeen = + itemView.findViewById(profileLastSeenId) + val lastSeenLayoutParams = + profileLastSeen.layoutParams as LinearLayout.LayoutParams + if (profileOnlineNowIcon.visibility == View.GONE) { + lastSeenLayoutParams.marginStart = 0 + } else { + lastSeenLayoutParams.marginStart = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 5f, + profileLastSeen.resources.displayMetrics + ).roundToInt() + } + profileLastSeen.layoutParams = lastSeenLayoutParams + + //Remove ugly margin before display name when note icon is invisible + + val profileNoteIcon = + itemView.findViewById(profileNoteIconId) + val profileDisplayName = + itemView.findViewById(profileDisplayNameId) + val displayNameLayoutParams = + profileDisplayName.layoutParams as LinearLayout.LayoutParams + if (profileNoteIcon.visibility == View.GONE) { + displayNameLayoutParams.marginStart = 0 + } else { + displayNameLayoutParams.marginStart = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 4f, + profileLastSeen.resources.displayMetrics + ).roundToInt() + } + profileDisplayName.layoutParams = displayNameLayoutParams + } + } + ) + } + } + ) + } + + fun disableAutomaticMessageDeletion() { + findAndHookMethod( + GApp.persistence.repository.ChatRepo, + Hooker.pkgParam.classLoader, + GApp.persistence.repository.ChatRepo_.deleteChatMessageFromLessThanOrEqualToTimestamp, + Long::class.java, + "kotlin.coroutines.Continuation", + RETURN_UNIT + ) + } + + fun dontSendChatMarkers() { + findAndHookMethod( + GApp.xmpp.ChatMarkersManager, + Hooker.pkgParam.classLoader, + GApp.xmpp.ChatMarkersManager_.addDisplayedExtension, + "org.jivesoftware.smack.chat2.Chat", + "org.jivesoftware.smack.packet.Message", + XC_MethodReplacement.DO_NOTHING + ) + findAndHookMethod( + GApp.xmpp.ChatMarkersManager, + Hooker.pkgParam.classLoader, + GApp.xmpp.ChatMarkersManager_.addReceivedExtension, + "org.jivesoftware.smack.chat2.Chat", + "org.jivesoftware.smack.packet.Message", + XC_MethodReplacement.DO_NOTHING + ) + } + + fun dontSendTypingIndicator() { + findAndHookMethod( + "org.jivesoftware.smackx.chatstates.ChatStateManager", + Hooker.pkgParam.classLoader, + "setCurrentState", + "org.jivesoftware.smackx.chatstates.ChatState", + "org.jivesoftware.smack.chat2.Chat", + XC_MethodReplacement.DO_NOTHING + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/eljaviluki/grindrplus/Obfuscation.kt b/app/src/main/java/com/eljaviluki/grindrplus/Obfuscation.kt index 6c5aea9..a959dc6 100644 --- a/app/src/main/java/com/eljaviluki/grindrplus/Obfuscation.kt +++ b/app/src/main/java/com/eljaviluki/grindrplus/Obfuscation.kt @@ -5,10 +5,28 @@ object Obfuscation { object api { private const val _api = Constants.GRINDR_PKG + ".api" - const val GrindrRestService = "$_api.GrindrRestService" - object GrindrRestService_ { - const val recordProfileViews = "W" + const val ChatRestService = "$_api.ChatRestService" + + object ChatRestService_ { + //Annotated with @POST("v3/me/prefs/phrases") + const val addSavedPhrase = "a" + + //Annotated with @DELETE("v3/me/prefs/phrases/{id}") + const val deleteSavedPhrase = "r" + + //Annotated with @POST("v4/phrases/frequency/{id}") + const val increaseSavedPhraseClickCount = "G" } + + const val PhrasesRestService = "$_api.n" + + object PhrasesRestService_ { + //Annotated with @GET("v3/me/prefs"), returns PhrasesResponse + const val getSavedPhrases = "a" + } + + //Contains @POST("/v3/logging/mobile/logs") + const val AnalyticsRestService = "$_api.c" } object base { @@ -17,82 +35,153 @@ object Obfuscation { object Experiment { private const val _experiment = "$_base.experiment" - const val IExperimentsManager = "$_experiment.c" + const val IExperimentsManager = "$_experiment.a" } } object experiment { private const val _experiment = Constants.GRINDR_PKG + ".experiment" - const val Experiments = "$_experiment.b" + const val Experiments = "$_experiment.f" + object Experiments_ { const val uncheckedIsEnabled_expMgr = "c" } } + object favorites { + private const val _favorites = Constants.GRINDR_PKG + ".favorites" + + const val FavoritesFragment = "$_favorites.FavoritesFragment" + } + + object manager { + private const val _manager = Constants.GRINDR_PKG + ".manager" + const val BlockInteractor = "$_manager.o" + } + object model { private const val _model = Constants.GRINDR_PKG + ".model" const val ExpiringImageBody = "$_model.ExpiringImageBody" + object ExpiringImageBody_ { const val getDuration = "getDuration" } const val ExpiringPhotoStatusResponse = "$_model.ExpiringPhotoStatusResponse" + object ExpiringPhotoStatusResponse_ { const val getTotal = "getTotal" const val getAvailable = "getAvailable" } const val Feature = "$_model.Feature" + object Feature_ { const val isGranted = "isGranted" } const val UpsellsV8 = "$_model.UpsellsV8" + object UpsellsV8_ { const val getMpuFree = "getMpuFree" const val getMpuXtra = "getMpuXtra" } const val Inserts = "$_model.Inserts" + object Inserts_ { const val getMpuFree = "getMpuFree" const val getMpuXtra = "getMpuXtra" } + + const val AddSavedPhraseRequest = "$_model.AddSavedPhraseRequest" + const val AddSavedPhraseResponse = "$_model.AddSavedPhraseResponse" + const val PhrasesResponse = "$_model.PhrasesResponse" } - object persistence { - private const val _persistence = Constants.GRINDR_PKG + ".persistence" + object network { + private const val _network = Constants.GRINDR_PKG + ".network" - object cache { - private const val _cache = "$_persistence.cache" + object either { + private const val _either = "$_network.either" - const val BlockByHelper = "$_cache.BlockByHelper" - object BlockByHelper_ { - const val addBlockByProfile = "addBlockByProfile" - const val removeBlockByProfile = "removeBlockByProfile" - } + const val ResultHelper = "$_either.b" + object ResultHelper_ { + const val createSuccess = "b" + } } + } + + object persistence { + private const val _persistence = Constants.GRINDR_PKG + ".persistence" object model { private const val _model = "$_persistence.model" const val ChatMessage = "$_model.ChatMessage" + object ChatMessage_ { const val TAP_TYPE_NONE = "TAP_TYPE_NONE" + const val getType = "getType" + const val setType = "setType" + const val setMessageId = "setMessageId" + const val setSender = "setSender" + const val setRecipient = "setRecipient" + const val setStanzaId = "setStanzaId" + const val setConversationId = "setConversationId" + const val setTimestamp = "setTimestamp" + const val setBody = "setBody" + + const val clone = "clone" + } + + const val BlockedProfile = "$_model.BlockedProfile" + + object BlockedProfile_ { + const val getProfileId = "getProfileId" } const val Profile = "$_model.Profile" + const val Phrase = "$_model.Phrase" } object repository { private const val _repository = "$_persistence.repository" const val ChatRepo = "$_repository.ChatRepo" + object ChatRepo_ { const val checkMessageForVideoCall = "checkMessageForVideoCall" + const val deleteChatMessageFromLessThanOrEqualToTimestamp = "deleteChatMessageFromLessThanOrEqualToTimestamp" + const val deleteChatMessageFromConversationId = "deleteChatMessageFromConversationId" + const val deleteChatMessageListFromConversationId = "deleteChatMessageListFromConversationId" + } + + const val ProfileRepo = "$_repository.ProfileRepo" + + object ProfileRepo_ { + const val delete = "delete" + const val recordProfileView = "recordProfileView" + } + + const val ConversationRepo = "$_repository.ConversationRepo" + object ConversationRepo_ { + const val deleteConversation = "deleteConversation" + const val deleteConversations = "deleteConversations" + } + + const val IncomingChatMarkerRepo = "$_repository.IncomingChatMarkerRepo" + object IncomingChatMarkerRepo_ { + const val deleteIncomingChatMarker = "deleteIncomingChatMarker" + } + + const val BlockRepo = "$_repository.BlockRepo" + object BlockRepo_ { + const val add = "add" + const val delete = "delete" } } } @@ -101,24 +190,38 @@ object Obfuscation { private const val _R = Constants.GRINDR_PKG const val color = "$_R.m0" + object color_ { - const val grindr_gold_star_gay = "D" - const val grindr_pure_white = "T" + const val grindr_gold_star_gay = "G" + const val grindr_pure_white = "W" + } + + const val id = "$_R.q0" + + object id_ { + const val fragment_favorite_recycler_view = "Ib" + const val profile_distance = "rk" + const val profile_online_now_icon = "pl" + const val profile_last_seen = "Tk" + const val profile_note_icon = "nl" + const val profile_display_name = "nk" } } object storage { private const val _storage = Constants.GRINDR_PKG + ".storage" - const val UserSession = "$_storage.f1" + const val UserSession = "$_storage.x0" const val IUserSession = "$_storage.UserSession" + object IUserSession_ { const val hasFeature_feature = "a" - const val isFree = "k" - const val isNoXtraUpsell = "r" - const val isXtra = "g" - const val isUnlimited = "s" + const val isFree = "r" + const val isNoXtraUpsell = "g" + const val isXtra = "o" + const val isUnlimited = "x" + const val getProfileId = "e" } } @@ -129,40 +232,74 @@ object Obfuscation { private const val _profileV2 = "$_ui.profileV2" const val ProfileFieldsView = "$_profileV2.ProfileFieldsView" + object ProfileFieldsView_ { - const val setProfile = "h" + const val setProfile = "setProfile" + } + + const val ProfilesViewModel = "$_profileV2.ProfilesViewModel" + + object ProfilesViewModel_ { + const val recordProfileViewsForViewedMeService = "X1" + } + + object model { + private const val _model = "$_profileV2.model" + + const val Profile = "$_model.h" + + object Profile_ { + const val getProfileId = "Z" + } } } object chat { private const val _chat = "$_ui.chat" + const val BlockViewModel = "$_chat.BlockViewModel" + const val ChatBaseFragmentV2 = "$_chat.ChatBaseFragmentV2" + object ChatBaseFragmentV2_ { const val _canBeUnsent = "X1" } + + object individual { + private const val _individual = "$_chat.individual" + + const val ChatIndividualFragment = "$_individual.ChatIndividualFragment" + + object ChatIndividualFragment_ { + const val showBlockDialog = "A3" + } + } } } object utils { private const val _utils = Constants.GRINDR_PKG + ".utils" - const val ProfileUtils = "$_utils.v0" + const val ProfileUtils = "$_utils.ProfileUtilsV2" + object ProfileUtils_ { - const val onlineIndicatorDuration = "b" + //Look for value of 600000 + const val onlineIndicatorDuration = "g" } } object view { private const val _view = Constants.GRINDR_PKG + ".view" - const val ExtendedProfileFieldView = "$_view.v4" + const val ExtendedProfileFieldView = "$_view.y4" + object ExtendedProfileFieldView_ { const val setLabel = "l" const val setValue = "n" } const val TapsAnimLayout = "$_view.TapsAnimLayout" + object TapsAnimLayout_ { const val tapType = "i" @@ -171,5 +308,22 @@ object Obfuscation { const val setTapType = "S" } } + + object xmpp { + private const val _xmpp = Constants.GRINDR_PKG + ".xmpp" + + const val ChatMessageManager = "$_xmpp.ChatMessageManager" + + object ChatMessageManager_ { + const val handleIncomingChatMessage = "p" + } + + const val ChatMarkersManager = "$_xmpp.i" + + object ChatMarkersManager_ { + const val addDisplayedExtension = "d" + const val addReceivedExtension = "f" + } + } } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 081a28e..10511fd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.1.0' apply false - id 'com.android.library' version '7.1.0' apply false + id 'com.android.application' version '7.4.2' apply false + id 'com.android.library' version '7.4.2' apply false id 'org.jetbrains.kotlin.android' version '1.6.21' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 25dfd63..2f0e7a3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Sep 17 23:10:56 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME