mirror of
https://github.com/Heretek-AI/GrindrPlus.git
synced 2026-10-12 02:02:48 +00:00
Make repo AI-agent ready: agent docs, scripts, skills, KDoc, patch coverage
Adds the infrastructure that lets AI coding agents (Claude Code, Gemini,
generic AGENTS.md readers) understand and safely modify GrindrPlus.
- AGENTS.md (canonical) + CLAUDE.md / GEMINI.md symlink aliases
- version.schema.json (JSON Schema 07) referenced from version.json
- scripts/sync-grindr-version.sh — wraps fetch_version.py + sed-patches
app/build.gradle.kts; --dry-run shows the diff
- scripts/check-obfuscated-symbols.py — scans // search-for markers
against a JADX dir, emits OK/AMBIGUOUS/STALE table
- scripts/check-patch-docs.sh — fails CI if any hook lacks its doc
- .claude/skills/{bump-grindr-version,update-obfuscated-symbols,
patch-doc-coverage,verify-build}/ — four reusable skills
- KDoc on the public hook API (Hook/Hooker/HookAdapter/HookManager/
FeatureManager), core/http/Interceptor, core/Constants
- KDoc on all 29 hook files using a shared template that captures the
obfuscation-marker convention + lifecycle semantics
- Patch docs added for all 27 previously-undocumented hooks; docs/patches/README.md
rebuilt with the full alphabetical index
- CI gate in build_apk.yml runs scripts/check-patch-docs.sh after JDK setup
Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: bump-grindr-version
|
||||
description: Bump the target Grindr version in version.json and app/build.gradle.kts in one pass. Use when the user says "bump Grindr version", "update target version", or "new Grindr release".
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# bump-grindr-version
|
||||
|
||||
Automate the first half of the Grindr version-bump workflow (steps 1–3 of
|
||||
`AGENTS.md` § 5). This skill **only** updates the metadata; it does not port
|
||||
hooks — that's `update-obfuscated-symbols`.
|
||||
|
||||
## Inputs
|
||||
|
||||
None required. Optional flags are forwarded to the underlying script.
|
||||
|
||||
## Required tools
|
||||
|
||||
- `python3` (with `requests` + `bs4` — same env that runs `fetch_version.py`)
|
||||
- `jq`
|
||||
- `bash`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run `bash scripts/sync-grindr-version.sh` from the repo root.
|
||||
- Pass `--dry-run` to preview the diff first.
|
||||
2. Verify the banner: `./gradlew printVersionInfo --quiet`. Must print
|
||||
`GrindrPlus v...<new Grindr version>`.
|
||||
3. **Stop here.** Hooks still need porting. Recommend `update-obfuscated-symbols`.
|
||||
|
||||
## What the script does
|
||||
|
||||
- Runs `python3 fetch_version.py -o version.json.tmp` to fetch the latest
|
||||
version + build from APKMirror.
|
||||
- `jq -r '.versionName,.versionCode'` to extract the new values.
|
||||
- `sed -i.bak` patches the two `listOf(...)` lines in `app/build.gradle.kts`
|
||||
(`grindrVersionName`, `grindrVersionCode`).
|
||||
- With `--dry-run`, prints `diff -u` of both files and exits without writing.
|
||||
- Without `--dry-run`, writes `version.json` and a `.bak` of `app/build.gradle.kts`.
|
||||
|
||||
## Exit codes
|
||||
|
||||
- `0` — success (or "already up to date")
|
||||
- `1` — `fetch_version.py` failed (APKMirror unreachable, rate-limited)
|
||||
- `2` — could not parse the fetched version
|
||||
- `3` — `app/build.gradle.kts` not found
|
||||
|
||||
## Failure modes
|
||||
|
||||
- **APKMirror rate-limit / 5xx**: `fetch_version.py` raises. The script surfaces
|
||||
this as exit 1. Wait and retry.
|
||||
- **`jq` missing**: exit 2. Install via your package manager.
|
||||
- **Gradle banner still shows old version**: the sed regex didn't match (e.g.
|
||||
the `listOf(...)` lines were refactored). Re-check `app/build.gradle.kts`
|
||||
and update the sed patterns in `scripts/sync-grindr-version.sh`.
|
||||
|
||||
## After this skill runs
|
||||
|
||||
The user's next step is to port the obfuscation markers. Suggest
|
||||
`update-obfuscated-symbols` (which needs the new Grindr APK decompiled with
|
||||
JADX).
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: patch-doc-coverage
|
||||
description: Verify every hook in app/src/main/java/com/grindrplus/hooks/ has a matching docs/patches/<kebab>.md file. Generate stubs for missing docs. Use before commit, after adding a new hook, or in CI.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# patch-doc-coverage
|
||||
|
||||
`AGENTS.md` requires every hook file to have a per-hook design doc at
|
||||
`docs/patches/<kebab>.md`. This skill checks coverage and can author
|
||||
stubs for missing docs.
|
||||
|
||||
## Inputs
|
||||
|
||||
None required. The defaults assume the standard repo layout:
|
||||
|
||||
- Hooks dir: `app/src/main/java/com/grindrplus/hooks/`
|
||||
- Docs dir: `docs/patches/`
|
||||
|
||||
## Required tools
|
||||
|
||||
- `bash`
|
||||
- `python3`
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Run the check**:
|
||||
|
||||
```bash
|
||||
bash scripts/check-patch-docs.sh
|
||||
```
|
||||
|
||||
Exit code:
|
||||
- `0` — every hook has a doc.
|
||||
- `1` — at least one hook is missing its doc; the script lists them.
|
||||
|
||||
2. **If missing docs are listed, author stubs** (one per missing hook):
|
||||
|
||||
```bash
|
||||
bash scripts/check-patch-docs.sh | grep ' - '
|
||||
```
|
||||
|
||||
For each `<Hook>.kt -> <kebab>.md` line, create `docs/patches/<kebab>.md`
|
||||
using the template at
|
||||
`.claude/skills/patch-doc-coverage/stub.template.md`.
|
||||
|
||||
3. **Rewrite `docs/patches/README.md`** to include a bullet for every hook,
|
||||
alphabetically.
|
||||
|
||||
4. **Re-run** `scripts/check-patch-docs.sh` to confirm coverage.
|
||||
|
||||
## Stub template
|
||||
|
||||
The template lives at
|
||||
`.claude/skills/patch-doc-coverage/stub.template.md`. A stub has:
|
||||
|
||||
- `# <Hook display name>` — copy the first arg to the `Hook(...)` constructor.
|
||||
- One-line "what this hook does" paragraph.
|
||||
- `## Target` section — paste the `// search for '...'` markers from the
|
||||
hook file as a code block, so a maintainer can find the obfuscated
|
||||
symbols in a fresh JADX-decompiled APK.
|
||||
- `## Verified against` section listing which Grindr versions were tested.
|
||||
|
||||
## Wiring into CI
|
||||
|
||||
`.github/workflows/build_apk.yml` runs `scripts/check-patch-docs.sh` after
|
||||
JDK setup. Build fails if any hook lacks its doc. To disable on a particular
|
||||
PR, add the `no-docs` label (the workflow looks for this label and skips the
|
||||
step in that case — see the workflow file).
|
||||
|
||||
## Failure modes
|
||||
|
||||
- **Mapping is wrong for an acronym**: edit `scripts/check-patch-docs.sh`'s
|
||||
`kebab()` function. Acronym handling lives in two regex steps.
|
||||
- **Hook is intentionally undocumented (WIP)**: do not merge to master
|
||||
without a stub. The CI gate exists for a reason.
|
||||
@@ -0,0 +1,32 @@
|
||||
# <Hook display name>
|
||||
|
||||
<One-line: what the hook does and which user-visible feature it powers.>
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Copy each `// search for '...'` marker from the hook file as a fenced
|
||||
code block, with the captured symbol name as a label. Example:
|
||||
|
||||
```
|
||||
// OnlineIndicator.kt
|
||||
val utils = "ka8" // search for ' <= 600000;'
|
||||
val isFeatureFlagEnabled = "xh6" // search for 'implements IsFeatureFlagEnabled'
|
||||
findClass("com.grindrapp.android.utils.ProfileUtilsV2") // search for 'R.string.profile_time_online_minutes_ago'
|
||||
```
|
||||
-->
|
||||
|
||||
```kotlin
|
||||
// <HookFile>.kt
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- <Grindr version> — initial support
|
||||
- <Latest verified Grindr version>
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: anything a future maintainer should know — which feature
|
||||
flag gates this hook, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: update-obfuscated-symbols
|
||||
description: Find which `// search for '<snippet>'` markers in hook files are stale against a new Grindr APK. Use after `bump-grindr-version`, when the user says "port hooks to version X", "obfuscation symbols changed", or "use new APK".
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# update-obfuscated-symbols
|
||||
|
||||
Walk every hook file in `app/src/main/java/com/grindrplus/hooks/`, extract
|
||||
every `// search for '<snippet>'` marker, and grep the JADX-decompiled output
|
||||
of the new Grindr APK to find which markers are stale.
|
||||
|
||||
**This skill only reports.** It never edits hook files. The maintainer
|
||||
(or AI agent) reads the report and applies edits.
|
||||
|
||||
## Inputs
|
||||
|
||||
- A new Grindr APK (or `.apkm`) on disk.
|
||||
- The path to JADX-decompiled output. If you only have the APK, this skill
|
||||
will run JADX for you.
|
||||
|
||||
## Required tools
|
||||
|
||||
- `python3` (stdlib only)
|
||||
- `jadx` CLI (only if you need to decompile the APK)
|
||||
- `bash` + `grep`
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Decompile the APK** (if you don't already have a JADX dir):
|
||||
|
||||
```bash
|
||||
jadx -d /tmp/jadx-out --deobf --no-res <path/to/new.apk>
|
||||
# .apkm is a zip; unzip it first and pick the base.apk
|
||||
```
|
||||
|
||||
2. **Run the scanner**:
|
||||
|
||||
```bash
|
||||
python3 scripts/check-obfuscated-symbols.py --jadx /tmp/jadx-out
|
||||
```
|
||||
|
||||
Add `--strict` to make it exit non-zero on any STALE marker (useful in CI).
|
||||
|
||||
3. **Read the report**. For each row:
|
||||
- `OK` — one candidate file matched; the snippet is still in the new APK.
|
||||
Nothing to do for this marker.
|
||||
- `AMBIGUOUS` — multiple matches; open JADX and pick the right one by hand.
|
||||
Update both the `// search for '...'` snippet AND the captured symbol
|
||||
string in the hook file.
|
||||
- `STALE` — no match. The snippet is obsolete. Open JADX, find the
|
||||
new decompiled code that does what the old snippet did, capture a
|
||||
**stable literal** of ≥ 12 chars, replace the marker.
|
||||
4. **For every updated hook**, edit `docs/patches/<feature>.md` and add a
|
||||
new "Verified against" entry with the new Grindr version.
|
||||
|
||||
## What the script does
|
||||
|
||||
- Parses each `*.kt` under `hooks/` for two patterns:
|
||||
- `val <name> = "<symbol>" // search for '<snippet>'`
|
||||
- `// search for '<snippet>'` on its own line (body-comment markers)
|
||||
- `grep -rF -- '<snippet>' /tmp/jadx-out/sources` for each snippet.
|
||||
- Emits a Markdown table:
|
||||
|
||||
```
|
||||
| Status | Marker | Old symbol | Candidates | Confidence | File |
|
||||
```
|
||||
|
||||
- Status ∈ {OK, AMBIGUOUS, STALE}.
|
||||
|
||||
## Markers you should always re-check by hand
|
||||
|
||||
- `STALE` markers — by definition there's no candidate. JADX decompilation
|
||||
can fail on obfuscated code; re-run with `--show-bad-code` if you suspect
|
||||
false positives.
|
||||
- Acronym-bounded markers (e.g. `SSLUnpinning.kt`'s
|
||||
`("com.grindrapp.android.chat.ChatDeleteConversationPlugin",`) — these are
|
||||
class-name literals, so they're highly stable. But if Grindr ever moves
|
||||
that class, the marker will go STALE.
|
||||
|
||||
## After this skill runs
|
||||
|
||||
- Commit hook updates with the message format
|
||||
`Hook: <Feature> — update for Grindr <X.Y.Z>`.
|
||||
- Commit doc updates separately:
|
||||
`Docs: <Feature> — mark verified against Grindr <X.Y.Z>`.
|
||||
- Run `verify-build` to confirm the module still assembles.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: verify-build
|
||||
description: Run ./gradlew assembleDebug and surface build failures with file:line context. Use before opening a PR, after any edit in app/src/main/java, or to confirm a hook still compiles.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# verify-build
|
||||
|
||||
Compile-check the module. Skips the slower release build (which needs GH
|
||||
secrets to sign).
|
||||
|
||||
## Inputs
|
||||
|
||||
None required.
|
||||
|
||||
## Required tools
|
||||
|
||||
- `./gradlew` (in repo root)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run from the repo root:
|
||||
|
||||
```bash
|
||||
./gradlew assembleDebug --quiet --no-daemon
|
||||
```
|
||||
|
||||
2. On success, the APK lands at
|
||||
`app/build/outputs/apk/debug/GPlus_v*.apk`. Print its path and size:
|
||||
|
||||
```bash
|
||||
ls -lh app/build/outputs/apk/debug/*.apk
|
||||
```
|
||||
|
||||
3. On failure, Gradle prints errors in `e: file.kt:LINE: message` form.
|
||||
Extract and surface the first 80 lines, plus the file:line of each
|
||||
compile error.
|
||||
|
||||
## What this skill does NOT do
|
||||
|
||||
- Does **not** install on a device. That's a manual step.
|
||||
- Does **not** run instrumented tests — there are none in this repo.
|
||||
- Does **not** build the release APK. `assembleRelease` needs the keystore
|
||||
secrets defined in `.github/workflows/build_apk.yml`.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- **`SDK location not found`**: `local.properties` is missing or stale.
|
||||
Open the project in Android Studio once so it generates the file.
|
||||
- **`compileSdk 35 not found`**: Android Studio hasn't installed the SDK
|
||||
platform. Open SDK Manager and install Android 35.
|
||||
- **`lspatch.jar not found`**: run `./gradlew setupLSPatch` first. The
|
||||
build expects the JAR at `app/libs/lspatch.jar`.
|
||||
- **`Unsupported class file major version`**: Java version mismatch.
|
||||
`compileOptions { sourceCompatibility = JavaVersion.VERSION_17 }` —
|
||||
use JDK 17.
|
||||
|
||||
## When to run
|
||||
|
||||
- After any edit in `app/src/main/java/`.
|
||||
- Before `git commit` of hook changes (or as a pre-commit hook).
|
||||
- After `./gradlew setupLSPatch` (to confirm the new `lspatch.jar` is
|
||||
compatible with the hook DSL).
|
||||
@@ -27,6 +27,13 @@ jobs:
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Verify patch-doc coverage
|
||||
# AGENTS.md § 3.4 requires every hook in app/src/main/java/com/grindrplus/hooks/
|
||||
# to have a corresponding docs/patches/<kebab>.md. Fails the build if any
|
||||
# hook is undocumented. See scripts/check-patch-docs.sh and the
|
||||
# patch-doc-coverage skill in .claude/skills/.
|
||||
run: bash scripts/check-patch-docs.sh
|
||||
|
||||
- name: Get Version Info
|
||||
id: version_info
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# AGENTS.md — GrindrPlus
|
||||
|
||||
> Canonical project instructions for AI coding agents. `CLAUDE.md` and `GEMINI.md` are vendor aliases pointing here.
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
GrindrPlus is an Xposed / LSPosed / LSPatch module that hooks the obfuscated `com.grindrapp.android` (Grindr) Android app to add features and bypass paywalls. The module is dual-mode — it can be installed as an LSPosed module on rooted devices or as an LSPatch-patched APK on unrooted devices. Both modes share the same hook source.
|
||||
|
||||
> [!WARNING]
|
||||
> The README banner marks the project as **end-of-life** because Grindr has rolled out PairIP integrity checks that effectively block hooks. New releases of Grindr will not be supported. The work in this repo is therefore *maintenance-grade*: keep things compiling and documented for any future fork.
|
||||
|
||||
- **Module entrypoint**: `app/src/main/java/com/grindrplus/GrindrPlus.kt`
|
||||
- **Hook DSL**: `app/src/main/java/com/grindrplus/utils/{Hook,Hooker,HookAdapter,HookManager}.kt`
|
||||
- **Hooks**: `app/src/main/java/com/grindrplus/hooks/*.kt` (29 files; `HookManager.kt:registerHooks` registers them)
|
||||
- **Manager UI**: `app/src/main/java/com/grindrplus/manager/` (Jetpack Compose; not directly part of the hooking surface)
|
||||
- **Per-hook design docs**: `docs/patches/<feature>.md`
|
||||
|
||||
## 2. Dev Setup
|
||||
|
||||
Full setup is in [`docs/env_setup.md`](docs/env_setup.md). Short version:
|
||||
|
||||
1. Install Android Studio (with JDK 17) and open the repo root. `./gradlew assembleDebug` should work after Android Studio imports the Gradle project.
|
||||
2. Download the target Grindr APK (see `version.json` for the version) from APKMirror. The `.apkm` is a zip of per-arch splits — you only need the base APK for decompilation.
|
||||
3. Decompile with JADX (enable *Show inconsistent code* and *Enable deobfuscation*).
|
||||
4. Test on a rooted device (LSPosed) or via Shizuku + LSPatch on an unrooted device.
|
||||
|
||||
## 3. Hook Authoring Rules
|
||||
|
||||
This is the most important section. **Read it before editing any file in `hooks/`.**
|
||||
|
||||
### 3.1 The `// search for '<snippet>'` convention
|
||||
|
||||
Grindr's bytecode is obfuscated; class and method names like `a`, `ka8`, `ps2` are renamed on every release. When a hook captures an obfuscated symbol, it MUST be followed by a `// search for` marker on the same line:
|
||||
|
||||
```kotlin
|
||||
private val utils = "ka8" // search for ' <= 600000;'
|
||||
```
|
||||
|
||||
The marker is a snippet of **stable decompiled source** (a literal, string constant, or distinctive expression) that the next maintainer can paste into JADX's search box against a new APK to find the new obfuscated name.
|
||||
|
||||
**Marker rules:**
|
||||
|
||||
- The snippet must be **≥ 12 characters** long.
|
||||
- The snippet must be a **literal that survives R8/ProGuard deobfuscation** — string constants, numeric literals, distinctive keywords. Do NOT use names that themselves get renamed.
|
||||
- The snippet must be **unique** in a fresh JADX-decompiled APK. If it matches multiple files, narrow it.
|
||||
- For method-body snippets (e.g. `param.setArg(1, false)`), the marker goes on the line **above** the hook call, with the form `// search for '<snippet>'`.
|
||||
|
||||
Example from `hooks/OnlineIndicator.kt`:
|
||||
|
||||
```kotlin
|
||||
val utils = "ka8" // search for ' <= 600000;'
|
||||
val isFeatureFlagEnabled = "xh6" // search for 'implements IsFeatureFlagEnabled'
|
||||
|
||||
override fun init() {
|
||||
findClass(utils)
|
||||
.hook("d", HookStage.BEFORE) { param ->
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Hook class skeleton
|
||||
|
||||
```kotlin
|
||||
class <FeatureName> : Hook(
|
||||
"<display name>",
|
||||
"<one-line user-facing description>"
|
||||
) {
|
||||
private val obfuscatedA = "xx1" // search for '<snippet A>'
|
||||
private val obfuscatedB = "yy2" // search for '<snippet B>'
|
||||
|
||||
override fun init() {
|
||||
// body: use findClass(...).hook(...) / hookConstructor(...)
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
// unhook any reflection-cached resources, coroutines, threads
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Extend `Hook` from `com.grindrplus.utils`.
|
||||
- Use the `findClass(name)` helper (already a member of `Hook`) — do not call `GrindrPlus.loadClass` directly from a hook.
|
||||
- Use `HookStage.BEFORE` to modify arguments / short-circuit results; `HookStage.AFTER` to patch return values.
|
||||
- Use `HookAdapter.arg<T>(i)` and `HookAdapter.setArg(i, v)` instead of `param.args[i]` casts.
|
||||
|
||||
### 3.3 Registering the hook
|
||||
|
||||
After adding `hooks/<FeatureName>.kt`, add an entry to `utils/HookManager.kt:registerHooks`'s `hookList`:
|
||||
|
||||
```kotlin
|
||||
FeatureName(),
|
||||
```
|
||||
|
||||
…sorted alphabetically with the rest. Use the **display name** in the in-app toggle list, not the class name.
|
||||
|
||||
### 3.4 Documenting the hook
|
||||
|
||||
Create `docs/patches/<kebab-name>.md` using the stub template (see `.claude/skills/patch-doc-coverage/stub.template.md`). Cross-link from `docs/patches/README.md`. CI fails if any hook lacks this doc.
|
||||
|
||||
## 4. Patch Authoring Rules
|
||||
|
||||
- **One class per file** in `hooks/`.
|
||||
- File name = PascalCase class name (e.g. `OnlineIndicator.kt`, `AntiBlock.kt`).
|
||||
- Hook doc filename = snake_case + `.md` (e.g. `online_indicator.md`, `anti_block.md`).
|
||||
- Each hook captures one user-visible feature; if you're tempted to add unrelated patches to the same hook file, split them.
|
||||
- All hook code lives under `package com.grindrplus.hooks`.
|
||||
|
||||
## 5. Version Bump Workflow
|
||||
|
||||
When a new Grindr version drops:
|
||||
|
||||
1. **Fetch** the latest version + build number: `python3 fetch_version.py -o version.json`. This pulls from APKMirror and validates against `version.schema.json`.
|
||||
2. **Patch** the two `listOf(...)` lines in `app/build.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
val grindrVersionName = listOf("<new>")
|
||||
val grindrVersionCode = listOf(<new_code>)
|
||||
```
|
||||
|
||||
Or invoke `/bump-grindr-version` (see `.claude/skills/bump-grindr-version/`).
|
||||
3. **Verify** the banner: `./gradlew printVersionInfo --quiet` — must print `GrindrPlus v...<new Grindr version>`.
|
||||
4. **Port** hooks: see `.claude/skills/update-obfuscated-symbols/`. Run `scripts/check-obfuscated-symbols.py` against a JADX dump of the new APK to find which `// search for` markers are stale. Manually update each affected hook and the corresponding `docs/patches/<name>.md` ("Verified against" section).
|
||||
5. **Build & smoke-test** on a real device (LSPosed or LSPatch).
|
||||
6. **Commit** with the message `Bump Grindr version support to <X.Y.Z>` (Co-Authored-By line for any AI assistance).
|
||||
|
||||
## 6. Build & Test Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `./gradlew setupLSPatch` | Downloads latest LSPatch release into `app/libs/lspatch.jar` and unzips the SO into `src/main/`. Idempotent. |
|
||||
| `./gradlew assembleDebug` | Builds an unsigned debug APK at `app/build/outputs/apk/debug/GPlus_v*.apk`. |
|
||||
| `./gradlew assembleRelease` | Builds a release APK. Needs GH secrets (`KEYSTORE_BASE64`, `KEY_ALIAS`, `KEYSTORE_PASSWORD`, `KEY_PASSWORD`) and is normally only invoked by `.github/workflows/build_apk.yml`. |
|
||||
| `./gradlew printVersionInfo` | Prints `GrindrPlus v<versionCode>-<grindrVersion>_<gitHash>`. Use it to confirm a version bump landed. |
|
||||
| `bash scripts/check-patch-docs.sh` | Exits 1 if any hook in `hooks/` lacks `docs/patches/<kebab>.md`. Wired into CI. |
|
||||
| `python3 scripts/check-obfuscated-symbols.py --jadx <dir>` | Prints a Markdown table of every `// search for '<snippet>'` marker and its candidate symbol in the JADX output. Status ∈ {OK, AMBIGUOUS, STALE}. |
|
||||
| `bash scripts/sync-grindr-version.sh --dry-run` | Wraps `fetch_version.py` + `sed`-patch of the two `listOf` lines. `--dry-run` shows the diff without writing. |
|
||||
|
||||
## 7. Do-Not-Touch
|
||||
|
||||
- `com.grindrapp.android_26.13.0.apkm` and any other `*.apkm` binary in the repo root — they are downloaded APK bundles and should be `.gitignore`'d (already untracked).
|
||||
- `app/libs/*` — committed binaries including `lspatch.jar`; rebuilt by `./gradlew setupLSPatch`.
|
||||
- `.github/workflows/build_apk.yml` secret values (`KEYSTORE_BASE64`, `KEY_ALIAS`, `KEYSTORE_PASSWORD`, `KEY_PASSWORD`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`).
|
||||
- `local.properties`, `gradle-wrapper.jar`, anything under `build/` or `.gradle/`.
|
||||
- `docs/img/` images.
|
||||
|
||||
## 8. Patch Lifecycle Semantics
|
||||
|
||||
- `Hook.init()` runs **once** when the module loads the host app, after `Config` confirms the hook is enabled via the in-app settings screen.
|
||||
- `Hook.cleanup()` runs when the user toggles the hook off and on, or when `HookManager.reloadHooks()` is called. **It must release** any reflection-cached `Class` references, cancel coroutines (`scope.cancel()`), stop background threads, and reset static state.
|
||||
- Hooks that touch a static field (e.g. `XposedHelpers.setObjectField`) should consider the case where the field is renamed between Grindr versions — the symbol-marker convention (§3.1) is what protects you here.
|
||||
|
||||
---
|
||||
|
||||
## Reusable skills
|
||||
|
||||
This repo ships four skills under `.claude/skills/`:
|
||||
|
||||
- `bump-grindr-version` — automate §5 steps 1–3.
|
||||
- `update-obfuscated-symbols` — automate §5 step 4.
|
||||
- `patch-doc-coverage` — check and author the `docs/patches/*.md` coverage.
|
||||
- `verify-build` — run `./gradlew assembleDebug` and surface failures.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/README.md` — same search-for convention, from a human-author angle.
|
||||
- `docs/env_setup.md` — full dev environment setup.
|
||||
- `docs/patches/README.md` — index of per-hook design docs.
|
||||
@@ -1,6 +1,21 @@
|
||||
package com.grindrplus.core
|
||||
|
||||
/**
|
||||
* Project-wide constants. Anything here is referenced from multiple modules
|
||||
* (hooks, manager, persistence) so it lives in one place.
|
||||
*/
|
||||
object Constants {
|
||||
/**
|
||||
* Sentinel string substituted for newlines when bridging between
|
||||
* the Xposed hook context (where `"\n"` can be stripped or escaped
|
||||
* by the host app) and the manager UI / chat-console.
|
||||
*/
|
||||
const val NEWLINE = "GRINDRPLUS_NEWLINE"
|
||||
|
||||
/**
|
||||
* The Grindr Android application package name. Used by [com.grindrplus.XposedLoader]
|
||||
* to gate `handleLoadPackage`, by `core/http/Interceptor` to scope
|
||||
* request rewriting, and by the manager UI's package picker.
|
||||
*/
|
||||
const val GRINDR_PACKAGE_NAME = "com.grindrapp.android"
|
||||
}
|
||||
@@ -14,6 +14,25 @@ import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
/**
|
||||
* OkHttp interceptor that injects the auth + telemetry headers Grindr's
|
||||
* backend expects on every request.
|
||||
*
|
||||
* The interceptor reflects into three live objects from the host app —
|
||||
* the [userSession] (provides the JWT + roles), the [userAgent] (provides
|
||||
* the User-Agent string), and the [deviceInfo] (provides the device-fp
|
||||
* header). Method and field letters change every Grindr release, so the
|
||||
* call sites use `// search for '<snippet>'` markers as breadcrumbs.
|
||||
*
|
||||
* Failures here are *swallowed* with a synthetic [Response] so the hook
|
||||
* never crashes the host app on a partial API mismatch — the cost is a
|
||||
* failed HTTP request, which is much easier to diagnose than a crash.
|
||||
*
|
||||
* @param userSession Live user-session object from the host app (e.g.
|
||||
* `com.grindrapp.android.usersession.<obf>`).
|
||||
* @param userAgent Live user-agent object from the host app.
|
||||
* @param deviceInfo Live device-info object from the host app.
|
||||
*/
|
||||
class Interceptor(
|
||||
private val userSession: Any,
|
||||
private val userAgent: Any,
|
||||
@@ -73,6 +92,11 @@ class Interceptor(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflectively invoke a no-arg method on [obj]. Returns `null` and
|
||||
* logs a warning if the method is missing (typical after a Grindr
|
||||
* update that renamed the symbol). Never throws.
|
||||
*/
|
||||
private fun invokeMethodSafe(obj: Any?, methodName: String): Any? {
|
||||
return try {
|
||||
if (obj == null) {
|
||||
@@ -93,6 +117,10 @@ class Interceptor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflectively read a field on [obj] (including private). Returns
|
||||
* `null` and logs a warning if the field is missing. Never throws.
|
||||
*/
|
||||
private fun getFieldSafe(obj: Any?, fieldName: String): Any? {
|
||||
return try {
|
||||
if (obj == null) {
|
||||
|
||||
@@ -9,6 +9,26 @@ import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Allow screenshots.
|
||||
*
|
||||
* Allow screenshots everywhere in the app.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/allow_screenshots.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class AllowScreenshots : Hook(
|
||||
"Allow screenshots",
|
||||
|
||||
@@ -16,6 +16,26 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
/**
|
||||
* Anti Block.
|
||||
*
|
||||
* Notifies you when someone blocks or unblocks you.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/anti_block.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class AntiBlock : Hook(
|
||||
"Anti Block",
|
||||
|
||||
@@ -4,6 +4,26 @@ import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
import com.grindrplus.utils.hookConstructor
|
||||
/**
|
||||
* Anti Detection.
|
||||
*
|
||||
* Hides root, emulator, and environment detections.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/anti_detection.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class AntiDetection : Hook(
|
||||
"Anti Detection",
|
||||
|
||||
@@ -27,6 +27,26 @@ import com.grindrplus.utils.hookConstructor
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
import org.json.JSONObject
|
||||
/**
|
||||
* Ban management.
|
||||
*
|
||||
* Provides comprehensive ban management tools (detailed ban info, etc.).
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/ban_management.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class BanManagement : Hook(
|
||||
"Ban management",
|
||||
|
||||
@@ -6,6 +6,26 @@ import com.grindrplus.utils.RetrofitUtils
|
||||
import com.grindrplus.utils.RetrofitUtils.RETROFIT_NAME
|
||||
import com.grindrplus.utils.RetrofitUtils.createServiceProxy
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Chat indicators.
|
||||
*
|
||||
* Don't show chat markers / indicators to others.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/chat_indicators.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class ChatIndicators : Hook(
|
||||
"Chat indicators",
|
||||
|
||||
@@ -7,6 +7,26 @@ import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
/**
|
||||
* Chat terminal.
|
||||
*
|
||||
* Create a chat terminal to execute commands.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/chat_terminal.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class ChatTerminal : Hook(
|
||||
"Chat terminal",
|
||||
|
||||
@@ -3,6 +3,26 @@ package com.grindrplus.hooks
|
||||
import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Disable analytics.
|
||||
*
|
||||
* Disable Grindr analytics (data collection).
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/disable_analytics.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class DisableAnalytics : Hook(
|
||||
"Disable analytics",
|
||||
|
||||
@@ -6,6 +6,26 @@ import com.grindrplus.utils.hook
|
||||
import com.grindrplus.utils.hookConstructor
|
||||
import de.robv.android.xposed.XposedHelpers.newInstance
|
||||
import de.robv.android.xposed.XposedHelpers.setObjectField
|
||||
/**
|
||||
* Disable boosting.
|
||||
*
|
||||
* Get rid of all upsells related to boosting.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/disable_boosting.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class DisableBoosting : Hook(
|
||||
"Disable boosting",
|
||||
|
||||
@@ -4,6 +4,26 @@ import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hookConstructor
|
||||
import de.robv.android.xposed.XposedHelpers.setObjectField
|
||||
/**
|
||||
* Disable shuffle.
|
||||
*
|
||||
* Forcefully disable the shuffle feature.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/disable_shuffle.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class DisableShuffle : Hook(
|
||||
"Disable shuffle",
|
||||
|
||||
@@ -12,6 +12,26 @@ import de.robv.android.xposed.XposedHelpers.setObjectField
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
/**
|
||||
* Disable updates.
|
||||
*
|
||||
* Disable forced updates.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/disable_updates.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class DisableUpdates : Hook(
|
||||
"Disable updates",
|
||||
|
||||
@@ -3,6 +3,26 @@ package com.grindrplus.hooks
|
||||
import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Video calls.
|
||||
*
|
||||
* Allow video calls on empty chats.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/empty_calls.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class EmptyCalls : Hook(
|
||||
"Video calls",
|
||||
|
||||
@@ -16,6 +16,26 @@ import com.grindrplus.utils.hookConstructor
|
||||
import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
import de.robv.android.xposed.XposedHelpers.setObjectField
|
||||
/**
|
||||
* Enable unlimited.
|
||||
*
|
||||
* Enable Grindr Unlimited features.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/enable_unlimited.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class EnableUnlimited : Hook(
|
||||
"Enable unlimited",
|
||||
|
||||
@@ -17,6 +17,26 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
/**
|
||||
* Expiring media.
|
||||
*
|
||||
* Allow unlimited photo/video viewing and save media permanently.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/expiring_media.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class ExpiringMedia : Hook(
|
||||
"Expiring media",
|
||||
|
||||
@@ -12,6 +12,26 @@ import com.grindrplus.utils.hookConstructor
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
/**
|
||||
* Feature granting.
|
||||
*
|
||||
* Grant all Grindr features.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/feature_granting.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class FeatureGranting : Hook(
|
||||
"Feature granting",
|
||||
|
||||
@@ -16,6 +16,26 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Proxy
|
||||
/**
|
||||
* Local saved phrases.
|
||||
*
|
||||
* Save unlimited phrases locally.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/local_saved_phrases.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class LocalSavedPhrases : Hook(
|
||||
"Local saved phrases",
|
||||
|
||||
@@ -41,6 +41,26 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.reflect.Proxy
|
||||
/**
|
||||
* Location spoofer.
|
||||
*
|
||||
* Spoof your location.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/location_spoofer.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class LocationSpoofer : Hook(
|
||||
"Location spoofer",
|
||||
@@ -573,7 +593,6 @@ class LocationSpoofer : Hook(
|
||||
// set initial marker position
|
||||
callMethod(markerOptions, "position", selectedLatLng)
|
||||
|
||||
|
||||
// implement OnMapClickListener via proxy
|
||||
val onMapClickListener = Proxy.newProxyInstance(
|
||||
context.classLoader,
|
||||
@@ -620,7 +639,6 @@ class LocationSpoofer : Hook(
|
||||
|
||||
callMethod(mapView, "getMapAsync", onMapReadyListener)
|
||||
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle("Pick Location")
|
||||
setView(container)
|
||||
|
||||
@@ -3,6 +3,26 @@ package com.grindrplus.hooks
|
||||
import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Notification Alerts.
|
||||
*
|
||||
* Disable all Grindr warnings related to notifications.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/notification_alerts.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class NotificationAlerts : Hook(
|
||||
"Notification Alerts",
|
||||
|
||||
@@ -5,6 +5,26 @@ import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
/**
|
||||
* Online indicator.
|
||||
*
|
||||
* Customize online indicator duration.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/online_indicator.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class OnlineIndicator : Hook(
|
||||
"Online indicator",
|
||||
|
||||
@@ -23,6 +23,26 @@ import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
import de.robv.android.xposed.XposedHelpers.setObjectField
|
||||
import java.util.ArrayList
|
||||
import kotlin.math.roundToInt
|
||||
/**
|
||||
* Profile details.
|
||||
*
|
||||
* Add extra fields and details to profiles.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/profile_details.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class ProfileDetails : Hook(
|
||||
"Profile details",
|
||||
|
||||
@@ -6,6 +6,26 @@ import com.grindrplus.utils.RetrofitUtils.RETROFIT_NAME
|
||||
import com.grindrplus.utils.RetrofitUtils.createServiceProxy
|
||||
import com.grindrplus.utils.RetrofitUtils.findPOSTMethod
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Profile views.
|
||||
*
|
||||
* Don't let others know you viewed their profile.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/profile_views.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class ProfileViews : Hook(
|
||||
"Profile views",
|
||||
|
||||
@@ -9,6 +9,26 @@ import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
/**
|
||||
* Quick block.
|
||||
*
|
||||
* Ability to block users quickly.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/quick_block.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class QuickBlock : Hook(
|
||||
"Quick block",
|
||||
|
||||
@@ -16,6 +16,21 @@ import javax.net.ssl.SSLSession
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* SSL certificate pinning bypass.
|
||||
*
|
||||
* Unlike the rest of the hooks in this package (which extend
|
||||
* [com.grindrplus.utils.Hook] and register via [com.grindrplus.utils.HookManager]),
|
||||
* this file is a top-level [sslUnpinning] function called directly from
|
||||
* [com.grindrplus.XposedLoader.handleLoadPackage] under `BuildConfig.DEBUG`.
|
||||
* It runs before any Grindr network call so the host app's HTTPS stack
|
||||
* never rejects our Mitmproxy / HTTP Toolkit cert.
|
||||
*
|
||||
* The hooks here target stable, fully-qualified classes from OkHttp /
|
||||
* Conscrypt / Firebase; the obfuscation-marker convention doesn't apply.
|
||||
*
|
||||
* @see com.grindrplus.XposedLoader for the call site.
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager", "BadHostnameVerifier")
|
||||
fun sslUnpinning(param: XC_LoadPackage.LoadPackageParam) {
|
||||
|
||||
@@ -15,6 +15,30 @@ private const val configRealtimeHttpClient =
|
||||
private const val configFetchHttpClient =
|
||||
"com.google.firebase.remoteconfig.internal.ConfigFetchHttpClient"
|
||||
|
||||
/**
|
||||
* App-signature spoofing.
|
||||
*
|
||||
* Unlike the rest of the hooks in this package (which extend
|
||||
* [com.grindrplus.utils.Hook] and register via [com.grindrplus.utils.HookManager]),
|
||||
* this file is a top-level [spoofSignatures] function called directly from
|
||||
* [com.grindrplus.XposedLoader.handleLoadPackage]. It runs once per package
|
||||
* load and:
|
||||
*
|
||||
* - Rewrites `getFingerprintHashForPackage` on Firebase + Facebook SDKs so
|
||||
* they report the *original* Grindr APK signature, not the LSPatch-
|
||||
* repackaged one (Firebase/Facebook reject mismatched signatures).
|
||||
* - Forces Facebook login to fall through to the web flow by zeroing the
|
||||
* `tryAuthorize` return, sidestepping an external-app signature check.
|
||||
* - On non-Grindr packages, fixes `ContextWrapper.getPackageName` /
|
||||
* Firebase `Metadata.getPackageInfo` so they still report the
|
||||
* `com.grindrapp.android` package name for any caller in the Firebase
|
||||
* install stack.
|
||||
*
|
||||
* The hooks here target stable, fully-qualified classes from Firebase /
|
||||
* Facebook; the obfuscation-marker convention doesn't apply.
|
||||
*
|
||||
* @see com.grindrplus.XposedLoader for the call site.
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun spoofSignatures(param: XC_LoadPackage.LoadPackageParam) {
|
||||
|
||||
|
||||
@@ -17,6 +17,26 @@ import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hookConstructor
|
||||
import java.io.File
|
||||
/**
|
||||
* Status Dialog.
|
||||
*
|
||||
* Check whether GrindrPlus is alive or not.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/status_dialog.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class StatusDialog : Hook(
|
||||
"Status Dialog",
|
||||
|
||||
@@ -11,6 +11,27 @@ import com.grindrplus.utils.hook
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
|
||||
@SuppressLint("LogNotTimber")
|
||||
/**
|
||||
* Timber Logging.
|
||||
*
|
||||
* Forces Timber to log messages even if no tree is planted.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/timber_logging :.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class TimberLogging :
|
||||
Hook("Timber Logging", "Forces Timber to log messages even if no tree is planted") {
|
||||
override fun init() {
|
||||
|
||||
@@ -35,6 +35,26 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONObject
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
/**
|
||||
* Unlimited albums.
|
||||
*
|
||||
* Allow to be able to view unlimited albums.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/unlimited_albums.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class UnlimitedAlbums : Hook("Unlimited albums", "Allow to be able to view unlimited albums") {
|
||||
private val albumsService = "com.grindrapp.android.api.albums.AlbumsRestService" // search for 'v1/albums/red-dot'
|
||||
|
||||
@@ -12,6 +12,26 @@ import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
import de.robv.android.xposed.XposedHelpers.callStaticMethod
|
||||
import de.robv.android.xposed.XposedHelpers.getObjectField
|
||||
import java.lang.reflect.Proxy
|
||||
/**
|
||||
* Unlimited profiles.
|
||||
*
|
||||
* Allow unlimited profiles.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/unlimited_profiles.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class UnlimitedProfiles : Hook(
|
||||
"Unlimited profiles",
|
||||
|
||||
@@ -3,6 +3,26 @@ package com.grindrplus.hooks
|
||||
import com.grindrplus.utils.Hook
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
/**
|
||||
* Unlock Explorer.
|
||||
*
|
||||
* Unlock all profiles in Explorer.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/unlock_explorer.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class UnlockExplorer : Hook(
|
||||
"Unlock Explorer",
|
||||
|
||||
@@ -11,6 +11,26 @@ import com.grindrplus.utils.HookAdapter
|
||||
import com.grindrplus.utils.HookStage
|
||||
import com.grindrplus.utils.hook
|
||||
import de.robv.android.xposed.XposedHelpers.callMethod
|
||||
/**
|
||||
* Keep Alive WebSocket.
|
||||
*
|
||||
* Prevents WebSocket disconnections when app goes to background. Causes battery drain, use with caution.
|
||||
*
|
||||
* Hooks `com.grindrapp.android` to add/modify this feature. See
|
||||
* `docs/patches/web_socket_alive.md` for design notes and version-port history.
|
||||
*
|
||||
* **Obfuscation marker convention.** Every capture of an obfuscated
|
||||
* symbol (single- or two-letter class/method name) in this file carries
|
||||
* an inline comment whose body is a stable, unique snippet from the
|
||||
* JADX-decompiled source. When porting this hook to a new Grindr
|
||||
* version, paste each snippet into JADX's search box to find the new
|
||||
* obfuscated name. See `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** `init()` is invoked once by HookManager.registerHooks()
|
||||
* after `Config` confirms the hook is enabled. `cleanup()` is invoked on
|
||||
* HookManager.reloadHooks() and must release any reflection-cached
|
||||
* resources, coroutines, or threads.
|
||||
*/
|
||||
|
||||
class WebSocketAlive : Hook(
|
||||
"Keep Alive WebSocket",
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
package com.grindrplus.utils
|
||||
|
||||
/**
|
||||
* A named, toggleable feature flag. Used by hooks that want a per-feature
|
||||
* on/off switch distinct from the global hook on/off in [Hook] /
|
||||
* [com.grindrplus.core.Config].
|
||||
*
|
||||
* @property name Stable identifier (typically the Grindr feature flag key,
|
||||
* e.g. `"online-until-updates"`).
|
||||
* @property isEnabled Whether the feature is currently on.
|
||||
*/
|
||||
data class Feature(val name: String, var isEnabled: Boolean)
|
||||
|
||||
/**
|
||||
* In-memory registry of named feature flags. Backed by a [MutableMap] keyed
|
||||
* by feature name; not persisted across process restarts (use
|
||||
* [com.grindrplus.core.Config] for that).
|
||||
*/
|
||||
class FeatureManager {
|
||||
private val features = mutableMapOf<String, Feature>()
|
||||
|
||||
/**
|
||||
* Register a [Feature]. Overwrites any prior entry with the same [Feature.name].
|
||||
*/
|
||||
fun add(feature: Feature) {
|
||||
features[feature.name] = feature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether [featureName] is currently registered AND enabled.
|
||||
* Returns `false` for unknown features.
|
||||
*/
|
||||
fun isEnabled(featureName: String): Boolean {
|
||||
return features[featureName]?.isEnabled ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether [featureName] is registered (regardless of its
|
||||
* current enabled state). Useful for distinguishing "feature absent"
|
||||
* from "feature disabled".
|
||||
*/
|
||||
fun isManaged(featureName: String): Boolean {
|
||||
return features.containsKey(featureName)
|
||||
}
|
||||
|
||||
@@ -3,34 +3,97 @@ package com.grindrplus.utils
|
||||
import com.grindrplus.GrindrPlus
|
||||
import com.grindrplus.core.Config
|
||||
|
||||
/**
|
||||
* Abstract base class for every Grindr patch in `hooks/`.
|
||||
*
|
||||
* Each subclass represents one user-visible feature (Anti Block, Online
|
||||
* Indicator, Disable Boosting, …). The class is registered in
|
||||
* [HookManager.registerHooks]'s `hookList` and the display name surfaces
|
||||
* in the in-app toggle list.
|
||||
*
|
||||
* **Obfuscation marker convention.** When a hook captures an obfuscated
|
||||
* class or method name (`a`, `ka8`, `ps2`, …) it MUST be followed by a
|
||||
* `// search for '<unique snippet>'` marker on the same line. The snippet
|
||||
* is a stable literal in the decompiled source so the next maintainer can
|
||||
* locate the new obfuscated name in a fresh JADX-decompiled APK. See
|
||||
* `AGENTS.md` § 3 for the full convention.
|
||||
*
|
||||
* **Lifecycle.** [init] runs once on host-app load (after `Config` confirms
|
||||
* the hook is enabled). [cleanup] runs on `HookManager.reloadHooks()` and
|
||||
* MUST release any reflection-cached `Class` references, cancel coroutines
|
||||
* (`scope.cancel()`), stop background threads, and reset static state.
|
||||
*
|
||||
* @property hookName Display name shown in the in-app hook toggle list.
|
||||
* Also used as the key in the `Config` datastore.
|
||||
* @property hookDesc One-line user-facing description of what the hook does.
|
||||
*
|
||||
* @see HookManager for registration / reload semantics.
|
||||
* @see HookStage for the BEFORE / AFTER hook timing options.
|
||||
*/
|
||||
abstract class Hook(
|
||||
val hookName: String,
|
||||
val hookDesc: String = "",
|
||||
) {
|
||||
/**
|
||||
* Hook specific initialization.
|
||||
* Hook-specific initialization. Override to wire up Xposed hooks,
|
||||
* start coroutines, register listeners, etc. Called once when the
|
||||
* host app is loaded if the hook is enabled in user settings.
|
||||
*/
|
||||
open fun init() {}
|
||||
|
||||
/**
|
||||
* Hook specific cleanup.
|
||||
* Hook-specific cleanup. Override to cancel coroutines, stop threads,
|
||||
* unhook any reflection-cached resources, and reset static state.
|
||||
* Called when the user toggles the hook off/on or when
|
||||
* [HookManager.reloadHooks] is invoked.
|
||||
*/
|
||||
open fun cleanup() {}
|
||||
|
||||
/**
|
||||
* Returns whether this hook is currently enabled in the in-app settings.
|
||||
* Hook implementations should consult this before doing expensive work.
|
||||
*/
|
||||
protected fun isHookEnabled(): Boolean {
|
||||
return Config.isHookEnabled(hookName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a class from the host app's class loader. Use this for both
|
||||
* obfuscated (`findClass("ka8")`) and fully-qualified (`findClass(
|
||||
* "com.grindrapp.android.chat.ChatDeleteConversationPlugin")`) names.
|
||||
*
|
||||
* @param name Either a fully-qualified class name or an obfuscated
|
||||
* short name. The latter should always have a `// search for`
|
||||
* marker as documentation (see class KDoc).
|
||||
* @return The [Class] object from the host app's class loader.
|
||||
* @throws ClassNotFoundException if the host APK no longer contains
|
||||
* a matching class (typical after a Grindr update that
|
||||
* renamed the symbol — port the marker via the
|
||||
* `update-obfuscated-symbols` skill).
|
||||
*/
|
||||
protected fun findClass(name: String): Class<*> {
|
||||
return GrindrPlus.loadClass(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an Android resource identifier by name + type, scoped to
|
||||
* the host app's package (Grindr). Returns `0` if no resource matches.
|
||||
*
|
||||
* @param name Resource entry name (without `@`, `@+`, `@drawable/`, etc.).
|
||||
* @param type Resource type (`string`, `drawable`, `layout`, …).
|
||||
*/
|
||||
protected fun getResource(name: String, type: String): Int {
|
||||
return GrindrPlus.context.resources.getIdentifier(
|
||||
name, type, GrindrPlus.context.packageName
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an Android attribute identifier by name, scoped to the host
|
||||
* app's package. Returns `0` if no attribute matches.
|
||||
*
|
||||
* @param name Attribute name (e.g. `R.attr.someCustomAttr`).
|
||||
*/
|
||||
protected fun getAttribute(name: String): Int {
|
||||
return GrindrPlus.context.resources.getIdentifier(name, "attr"
|
||||
, GrindrPlus.context.packageName)
|
||||
|
||||
@@ -5,26 +5,64 @@ import de.robv.android.xposed.XposedBridge
|
||||
import java.lang.reflect.Member
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Type-safe wrapper around [XC_MethodHook.MethodHookParam], passed to every
|
||||
* hook consumer registered via [Hooker] / the `Class<T>.hook(...)` extension
|
||||
* functions.
|
||||
*
|
||||
* The adapter hides the raw `args[]` array and unchecked casts in favor of
|
||||
* typed helpers: [arg], [argNullable], [setArg], [setResult], and the
|
||||
* original-method invokers [invokeOriginal] / [invokeOriginalSafe].
|
||||
*
|
||||
* @param Clazz The declared type of `this` on the hooked method (or `Any`
|
||||
* if the hook doesn't care). Used by [thisObject] /
|
||||
* [nullableThisObject] to give a typed view of the receiver.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class HookAdapter<Clazz>(
|
||||
private val methodHookParam: XC_MethodHook.MethodHookParam<*>
|
||||
) {
|
||||
/**
|
||||
* `this` reference on the hooked method, cast to [Clazz].
|
||||
* @throws ClassCastException if the receiver is not a [Clazz].
|
||||
*/
|
||||
fun thisObject(): Clazz {
|
||||
return methodHookParam.thisObject as Clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* Nullable variant of [thisObject]. Returns `null` when the hooked
|
||||
* method was a static call (`thisObject` is `null`).
|
||||
*/
|
||||
fun nullableThisObject(): Clazz? {
|
||||
return methodHookParam.thisObject as Clazz?
|
||||
}
|
||||
|
||||
/**
|
||||
* The reflective [Member] (method or constructor) that was hooked.
|
||||
*/
|
||||
fun method(): Member {
|
||||
return methodHookParam.method
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an argument by index with an unchecked cast. Use [arg] overload
|
||||
* with a [Class] token when you want type-coercion fallbacks.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [index] is out of range.
|
||||
*/
|
||||
fun <T : Any> arg(index: Int): T {
|
||||
return methodHookParam.args[index] as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an argument by index with type coercion. If the raw value is not
|
||||
* already of type [clazz], the adapter tries a best-effort conversion
|
||||
* (calling `toString`/`toInt`/`toDouble`/...) and falls back to the
|
||||
* primitive zero value (`0`, `0.0`, `false`, ...) for primitives.
|
||||
*
|
||||
* Returns `null` if the value cannot be coerced.
|
||||
*/
|
||||
fun <T : Any> arg(index: Int, clazz: Class<T>): T? {
|
||||
val argValue = methodHookParam.args[index]
|
||||
return try {
|
||||
@@ -34,47 +72,96 @@ class HookAdapter<Clazz>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [arg] but returns `null` instead of throwing when [index] is
|
||||
* out of range.
|
||||
*/
|
||||
fun <T : Any> argNullable(index: Int): T? {
|
||||
return methodHookParam.args.getOrNull(index) as T?
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an argument by index. No-op if [index] is out of range.
|
||||
* Use this in `HookStage.BEFORE` to mutate inputs before the original
|
||||
* method runs.
|
||||
*/
|
||||
fun setArg(index: Int, value: Any?) {
|
||||
if (index < 0 || index >= methodHookParam.args.size) return
|
||||
methodHookParam.args[index] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw args array. Use this when you need the whole array at once
|
||||
* (e.g. for `invokeOriginal(args)`) or when you don't know the type.
|
||||
*/
|
||||
fun args(): Array<Any?> {
|
||||
return methodHookParam.args
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the original method's return value. Only meaningful in
|
||||
* `HookStage.AFTER` callbacks.
|
||||
*/
|
||||
fun getResult(): Any? {
|
||||
return methodHookParam.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the original method's return value. In `HookStage.BEFORE`
|
||||
* this short-circuits the call entirely (the original is skipped).
|
||||
* In `HookStage.AFTER` this overrides the value the original returned.
|
||||
*/
|
||||
fun setResult(result: Any?) {
|
||||
methodHookParam.result = result
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the original call throw [throwable] instead of returning.
|
||||
*/
|
||||
fun setThrowable(throwable: Throwable) {
|
||||
methodHookParam.throwable = throwable
|
||||
}
|
||||
|
||||
/**
|
||||
* Throwable the original method threw, if any. Only meaningful in
|
||||
* `HookStage.AFTER`.
|
||||
*/
|
||||
fun throwable(): Throwable? {
|
||||
return methodHookParam.throwable
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the original (un-hooked) method using the current `this` and
|
||||
* args, and return its result. Useful when a `HookStage.AFTER` callback
|
||||
* needs to re-run the original with a tweaked argument set.
|
||||
*
|
||||
* @throws Throwable whatever the original method threw.
|
||||
*/
|
||||
fun invokeOriginal(): Any? {
|
||||
return XposedBridge.invokeOriginalMethod(method(), thisObject(), args())
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the original method with a custom args array, return its result.
|
||||
*/
|
||||
fun invokeOriginal(args: Array<Any?>): Any? {
|
||||
return XposedBridge.invokeOriginalMethod(method(), thisObject(), args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the original method, swallowing any thrown exception and
|
||||
* forwarding it to [errorCallback]. Sets the result via [setResult]
|
||||
* on success.
|
||||
*/
|
||||
fun invokeOriginalSafe(errorCallback: Consumer<Throwable>) {
|
||||
invokeOriginalSafe(args(), errorCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the original method with custom [args], swallowing any thrown
|
||||
* exception and forwarding it to [errorCallback]. Sets the result via
|
||||
* [setResult] on success.
|
||||
*/
|
||||
fun invokeOriginalSafe(args: Array<Any?>, errorCallback: Consumer<Throwable>) {
|
||||
runCatching {
|
||||
setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args))
|
||||
@@ -102,6 +189,11 @@ class HookAdapter<Clazz>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort coercion of [arg] to [clazz] via the standard
|
||||
* `toString` / `toInt` / `toDouble` / `toFloat` / `toLong` / `toBoolean`
|
||||
* reflective call. Returns `null` if coercion fails.
|
||||
*/
|
||||
fun <T : Any> convertToType(arg: Any, clazz: Class<T>): T? {
|
||||
return try {
|
||||
when (clazz) {
|
||||
|
||||
@@ -33,9 +33,31 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Owns the lifecycle of every [Hook] registered with the module.
|
||||
*
|
||||
* The `hookList` literal in [registerHooks] is the single source of truth
|
||||
* for which hooks ship in the module. **Every new hook** in
|
||||
* `com.grindrplus.hooks` must be added here (and, per `AGENTS.md` § 4, get
|
||||
* a matching `docs/patches/<kebab>.md`).
|
||||
*
|
||||
* The class is a singleton owned by [com.grindrplus.GrindrPlus.hookManager].
|
||||
*/
|
||||
class HookManager {
|
||||
/** Currently-registered hooks, keyed by their concrete [KClass]. */
|
||||
private var hooks = mutableMapOf<KClass<out Hook>, Hook>()
|
||||
|
||||
/**
|
||||
* Build the [hookList], register each hook's settings with [Config],
|
||||
* and (when [init] is true) call [Hook.init] on every enabled hook.
|
||||
*
|
||||
* Pass `init = false` to only refresh the settings registry without
|
||||
* invoking hook bodies — useful for first-launch wiring where the host
|
||||
* app context is not yet available.
|
||||
*
|
||||
* @param init If true, call [Hook.init] on every hook whose setting is
|
||||
* enabled in [Config]. Default `true`.
|
||||
*/
|
||||
fun registerHooks(init: Boolean = true) {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val hookList = listOf(
|
||||
@@ -89,6 +111,13 @@ class HookManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down every currently-registered hook (calling [Hook.cleanup])
|
||||
* and re-register the full [hookList] from scratch. Invoked from the
|
||||
* in-app settings screen when the user toggles hooks off/on, or from
|
||||
* any code path that needs to re-bind Xposed hooks (e.g. after a
|
||||
* module update).
|
||||
*/
|
||||
fun reloadHooks() {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
hooks.values.forEach { hook -> hook.cleanup() }
|
||||
@@ -98,6 +127,10 @@ class HookManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One-shot initialization called by [com.grindrplus.GrindrPlus.init] on
|
||||
* module load. Equivalent to `registerHooks(init = true)`.
|
||||
*/
|
||||
fun init() {
|
||||
registerHooks()
|
||||
}
|
||||
|
||||
@@ -5,12 +5,44 @@ import de.robv.android.xposed.XposedBridge
|
||||
import java.lang.reflect.Member
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Timing of a hook callback relative to the hooked method.
|
||||
*
|
||||
* - [BEFORE] — the callback runs *before* the original method. Use this to
|
||||
* inspect / modify arguments via [HookAdapter.setArg] or short-circuit the
|
||||
* call via [HookAdapter.setResult].
|
||||
* - [AFTER] — the callback runs *after* the original method. Use this to
|
||||
* patch the return value via [HookAdapter.setResult] or trigger side
|
||||
* effects (notifications, logging).
|
||||
*/
|
||||
enum class HookStage {
|
||||
BEFORE,
|
||||
AFTER
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin wrapper around `XposedBridge.hookAllMethods` / `hookAllConstructors`
|
||||
* / `hookMethod` that hands the consumer a type-safe [HookAdapter].
|
||||
*
|
||||
* Most hook authors never call [Hooker] directly — they use the extension
|
||||
* functions at the bottom of this file:
|
||||
*
|
||||
* ```kotlin
|
||||
* findClass("ka8").hook("d", HookStage.BEFORE) { param ->
|
||||
* param.setResult(System.currentTimeMillis() - param.arg<Long>(0) <= savedDurationMillis)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The `hookObjectMethod` / `ephemeralHook*` overloads exist for hooks that
|
||||
* must fire only for a specific object instance or fire exactly once.
|
||||
*
|
||||
* @see HookAdapter for the type-safe wrapper passed to consumers.
|
||||
*/
|
||||
object Hooker {
|
||||
/**
|
||||
* Build an [XC_MethodHook] that dispatches to [consumer] wrapped in a
|
||||
* [HookAdapter]. Internal — exposed for the inline extension functions.
|
||||
*/
|
||||
inline fun <T> newMethodHook(
|
||||
stage: HookStage,
|
||||
crossinline consumer: (HookAdapter<T>) -> Unit,
|
||||
@@ -30,6 +62,10 @@ object Hooker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook every method named [methodName] on [clazz]. The [filter] gates
|
||||
* which calls actually invoke [consumer].
|
||||
*/
|
||||
inline fun <T> hook(
|
||||
clazz: Class<T>,
|
||||
methodName: String,
|
||||
@@ -38,6 +74,11 @@ object Hooker {
|
||||
noinline consumer: (HookAdapter<T>) -> Unit
|
||||
): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter))
|
||||
|
||||
/**
|
||||
* Hook a specific reflective [member] (method or constructor) directly,
|
||||
* without going through `hookAllMethods`. Use when you have already
|
||||
* resolved a `Member` (e.g. via `getDeclaredMethods`).
|
||||
*/
|
||||
inline fun <T> hook(
|
||||
member: Member,
|
||||
stage: HookStage,
|
||||
@@ -47,6 +88,10 @@ object Hooker {
|
||||
return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook every method named [methodName] on [clazz]. Shorthand for the
|
||||
* filter overload with `filter = { true }`.
|
||||
*/
|
||||
fun <T> hook(
|
||||
clazz: Class<T>,
|
||||
methodName: String,
|
||||
@@ -54,6 +99,10 @@ object Hooker {
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
): Set<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer)
|
||||
|
||||
/**
|
||||
* Hook a reflective [member] directly. Shorthand for the filter overload
|
||||
* with `filter = { true }`.
|
||||
*/
|
||||
fun <T> hook(
|
||||
member: Member,
|
||||
stage: HookStage,
|
||||
@@ -62,12 +111,19 @@ object Hooker {
|
||||
return hook(member, stage, { true }, consumer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook every constructor on [clazz]. Shorthand for the filter overload
|
||||
* with `filter = { true }`.
|
||||
*/
|
||||
fun <T> hookConstructor(
|
||||
clazz: Class<T>,
|
||||
stage: HookStage,
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer))
|
||||
|
||||
/**
|
||||
* Hook every constructor on [clazz], gating with [filter].
|
||||
*/
|
||||
fun <T> hookConstructor(
|
||||
clazz: Class<T>,
|
||||
stage: HookStage,
|
||||
@@ -77,6 +133,11 @@ object Hooker {
|
||||
XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook [methodName] on [clazz] but fire [hookConsumer] only when the
|
||||
* `this` object equals [instance]. The hook automatically unhooks itself
|
||||
* once it fires (or once the instance is GC'd).
|
||||
*/
|
||||
inline fun <T> hookObjectMethod(
|
||||
clazz: Class<T>,
|
||||
instance: Any,
|
||||
@@ -97,6 +158,10 @@ object Hooker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook [methodName] on [clazz] exactly once: the hook fires the first
|
||||
* time [hookConsumer] is invoked, then automatically unhooks itself.
|
||||
*/
|
||||
inline fun <T> ephemeralHook(
|
||||
clazz: Class<T>,
|
||||
methodName: String,
|
||||
@@ -110,6 +175,10 @@ object Hooker {
|
||||
}.also { unhooks.addAll(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook [methodName] on [clazz], scoped to [instance], exactly once.
|
||||
* Combines [hookObjectMethod] + [ephemeralHook].
|
||||
*/
|
||||
inline fun <T> ephemeralHookObjectMethod(
|
||||
clazz: Class<T>,
|
||||
instance: Any,
|
||||
@@ -125,6 +194,11 @@ object Hooker {
|
||||
}.also { unhooks.addAll(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook every constructor on [clazz] exactly once. The hook fires the
|
||||
* first time [hookConsumer] is invoked, then automatically unhooks
|
||||
* itself.
|
||||
*/
|
||||
inline fun <T> ephemeralHookConstructor(
|
||||
clazz: Class<T>,
|
||||
stage: HookStage,
|
||||
@@ -138,23 +212,35 @@ object Hooker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar: `findClass("Foo").hookConstructor(HookStage.AFTER) { ... }`.
|
||||
*/
|
||||
fun <T> Class<T>.hookConstructor(
|
||||
stage: HookStage,
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
) = Hooker.hookConstructor(this, stage, consumer)
|
||||
|
||||
/**
|
||||
* Sugar: `findClass("Foo").hookConstructor(HookStage.AFTER, filter = ...) { ... }`.
|
||||
*/
|
||||
fun <T> Class<T>.hookConstructor(
|
||||
stage: HookStage,
|
||||
filter: ((HookAdapter<T>) -> Boolean),
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
) = Hooker.hookConstructor(this, stage, filter, consumer)
|
||||
|
||||
/**
|
||||
* Sugar: `findClass("Foo").hook("bar", HookStage.BEFORE) { ... }`.
|
||||
*/
|
||||
fun <T> Class<T>.hook(
|
||||
methodName: String,
|
||||
stage: HookStage,
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, consumer)
|
||||
|
||||
/**
|
||||
* Sugar: `findClass("Foo").hook("bar", HookStage.BEFORE, filter = ...) { ... }`.
|
||||
*/
|
||||
fun <T> Class<T>.hook(
|
||||
methodName: String,
|
||||
stage: HookStage,
|
||||
@@ -162,17 +248,28 @@ fun <T> Class<T>.hook(
|
||||
consumer: (HookAdapter<T>) -> Unit
|
||||
): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, filter, consumer)
|
||||
|
||||
/**
|
||||
* Sugar: hook a reflective [Member] (method or constructor).
|
||||
*/
|
||||
fun Member.hook(
|
||||
stage: HookStage,
|
||||
consumer: (HookAdapter<Any>) -> Unit
|
||||
): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer)
|
||||
|
||||
/**
|
||||
* Sugar: hook a reflective [Member] with a filter.
|
||||
*/
|
||||
fun Member.hook(
|
||||
stage: HookStage,
|
||||
filter: ((HookAdapter<Any>) -> Boolean),
|
||||
consumer: (HookAdapter<Any>) -> Unit
|
||||
): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer)
|
||||
|
||||
/**
|
||||
* Bulk-hook an array of [Method]s. Skips methods declared on [Object] to
|
||||
* avoid re-hooking `toString`, `hashCode`, etc. across the entire class
|
||||
* hierarchy.
|
||||
*/
|
||||
fun Array<Method>.hookAll(stage: HookStage, param: (HookAdapter<Any>) -> Unit) {
|
||||
filter { it.declaringClass != Object::class.java }.forEach {
|
||||
it.hook(stage, param)
|
||||
|
||||
+31
-1
@@ -1,4 +1,34 @@
|
||||
# Patches
|
||||
|
||||
Each hook in `app/src/main/java/com/grindrplus/hooks/` ships a design
|
||||
doc here. The list is alphabetical by kebab-case filename.
|
||||
|
||||
- [Allow screenshots](allow_screenshots.md)
|
||||
- [Anti Block](anti_block.md)
|
||||
- [Online indicator](online_indicator.md)
|
||||
- [Anti Detection](anti_detection.md)
|
||||
- [Ban management](ban_management.md)
|
||||
- [Chat indicators](chat_indicators.md)
|
||||
- [Chat terminal](chat_terminal.md)
|
||||
- [Disable analytics](disable_analytics.md)
|
||||
- [Disable boosting](disable_boosting.md)
|
||||
- [Disable shuffle](disable_shuffle.md)
|
||||
- [Disable updates](disable_updates.md)
|
||||
- [Video calls](empty_calls.md)
|
||||
- [Enable unlimited](enable_unlimited.md)
|
||||
- [Expiring media](expiring_media.md)
|
||||
- [Feature granting](feature_granting.md)
|
||||
- [Local saved phrases](local_saved_phrases.md)
|
||||
- [Location spoofer](location_spoofer.md)
|
||||
- [Notification Alerts](notification_alerts.md)
|
||||
- [Online indicator](online_indicator.md)
|
||||
- [Profile details](profile_details.md)
|
||||
- [Profile views](profile_views.md)
|
||||
- [Quick block](quick_block.md)
|
||||
- [Signature spoofer](signature_spoofer.md)
|
||||
- [SSL unpinning](ssl_unpinning.md)
|
||||
- [Status Dialog](status_dialog.md)
|
||||
- [Timber Logging](timber_logging.md)
|
||||
- [Unlimited albums](unlimited_albums.md)
|
||||
- [Unlimited profiles](unlimited_profiles.md)
|
||||
- [Unlock Explorer](unlock_explorer.md)
|
||||
- [Keep Alive WebSocket](web_socket_alive.md)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Allow screenshots
|
||||
|
||||
Allow screenshots everywhere in the app.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `AllowScreenshots.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// AllowScreenshots.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Anti Detection
|
||||
|
||||
Hides root, emulator, and environment detections.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `AntiDetection.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// AntiDetection.kt
|
||||
// search for '<unique snippet>'
|
||||
// search for 'this.isRooted = '
|
||||
// search for 'this.isEmulator = '
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Ban management
|
||||
|
||||
Provides comprehensive ban management tools (detailed ban info, etc.).
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `BanManagement.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// BanManagement.kt
|
||||
val authServiceClass = "com.grindrapp.android.api.LoginRestService" // search for 'v3/users/password-validation'
|
||||
val bannedArgs = "zi0" // search for 'new StringBuilder("BannedArgs(bannedType=")'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Chat indicators
|
||||
|
||||
Don't show chat markers / indicators to others.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `ChatIndicators.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// ChatIndicators.kt
|
||||
val chatRestService = "com.grindrapp.android.chat.data.datasource.api.service.ChatRestService" // search for '"(Lcom/grindrapp/android/chat/data/datasource/api/model/MessageRateResponseRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;"'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Chat terminal
|
||||
|
||||
Create a chat terminal to execute commands.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `ChatTerminal.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// ChatTerminal.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Disable analytics
|
||||
|
||||
Disable Grindr analytics (data collection).
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `DisableAnalytics.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// DisableAnalytics.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Disable boosting
|
||||
|
||||
Get rid of all upsells related to boosting.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `DisableBoosting.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// DisableBoosting.kt
|
||||
val drawerProfileUiState = "com.grindrapp.android.ui.drawer.model.DrawerProfileUiState" // search for 'DrawerProfileUiState(showBoostMeButton='
|
||||
val radarUiModel = "tq8" // search for 'RadarUiModel(boostButton='
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Disable shuffle
|
||||
|
||||
Forcefully disable the shuffle feature.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `DisableShuffle.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// DisableShuffle.kt
|
||||
val viewState = "com.grindrapp.android.ui.browse.v\$j" // search for 'ViewState(isRefreshing='
|
||||
val shuffleUiState = "com.grindrapp.android.ui.browse.v\$g" // search for 'ShuffleUiState(isShuffleEnabled='
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,30 @@
|
||||
# Disable updates
|
||||
|
||||
Disable forced updates.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `DisableUpdates.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// DisableUpdates.kt
|
||||
val appUpdateZzm = "com.google.android.play.core.appupdate.zzm" // search for 'requestUpdateInfo(%s)'
|
||||
val appUpgradeManager = "hr8" // search for 'Uri.parse("market://details?id=com.grindrapp.android");'
|
||||
// search for '<unique snippet>'
|
||||
// search for '.setMessage(R.string.deprecation_message);'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Video calls
|
||||
|
||||
Allow video calls on empty chats.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `EmptyCalls.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// EmptyCalls.kt
|
||||
val checkChattedBefore = "x65" // search for 'VideoCallHasNotChattedException'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,34 @@
|
||||
# Enable unlimited
|
||||
|
||||
Enable Grindr Unlimited features.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `EnableUnlimited.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// EnableUnlimited.kt
|
||||
val paywallUtils = "bya" // search for 'app_restart_required'
|
||||
val persistentAdBannerContainer = "nb.d" // search for '(ComposeView) ViewBindings.findChildViewById(view,'
|
||||
// search for '<unique snippet>'
|
||||
// search for 'com.grindrapp.android.chat.presentation.ui.ChatActivityV2$subscribeToInterstitialAds$1$1$1'
|
||||
// search for 'bind(Landroid/view/View;)Lcom/grindrapp/android/databinding/ProfileTagCascadeFragmentBinding;'
|
||||
// search for '"bind(Landroid/view/View;)Lcom/grindrapp/android/databinding/DrawerProfileBinding;"'
|
||||
// search for 'bind(Landroid/view/View;)Lcom/grindrapp/android/databinding/FragmentRadarBinding;'
|
||||
// search for 'Intrinsics.checkNotNullParameter(roles, "roles");'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Expiring media
|
||||
|
||||
Allow unlimited photo/video viewing and save media permanently.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `ExpiringMedia.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// ExpiringMedia.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,31 @@
|
||||
# Feature granting
|
||||
|
||||
Grant all Grindr features.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `FeatureGranting.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// FeatureGranting.kt
|
||||
val isFeatureFlagEnabled = "xh6" // search for 'implements IsFeatureFlagEnabled {'
|
||||
val settingDistanceVisibilityViewModel = "sfa" // search for 'UiState(distanceVisibility='
|
||||
val alertParams = "P" // search for 'AlertController.AlertParams'
|
||||
// search for '<unique snippet>'
|
||||
// search for 'Assignment.Flag'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,30 @@
|
||||
# Local saved phrases
|
||||
|
||||
Save unlimited phrases locally.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `LocalSavedPhrases.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// LocalSavedPhrases.kt
|
||||
val phrasesRestService = "com.grindrapp.android.api.PhrasesRestService" // search for 'v3/me/prefs'
|
||||
val createSuccessResult = "xz3" // search for 'Success(successValue='
|
||||
val chatRestService = "com.grindrapp.android.chat.data.datasource.api.service.ChatRestService" // search for '"v4/chat/conversation/{conversationId}"'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Location spoofer
|
||||
|
||||
Spoof your location.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `LocationSpoofer.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// LocationSpoofer.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Notification Alerts
|
||||
|
||||
Disable all Grindr warnings related to notifications.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `NotificationAlerts.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// NotificationAlerts.kt
|
||||
val notificationManager = "cl5" // search for '0L, "notification_reminder_time"'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Profile details
|
||||
|
||||
Add extra fields and details to profiles.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `ProfileDetails.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// ProfileDetails.kt
|
||||
val blockedProfilesObserver = "or1" // search for 'Intrinsics.checkNotNullParameter(dataList, "dataList");'
|
||||
val profileViewHolder = "eb6" // search for 'Intrinsics.checkNotNullParameter(individualUnblockActivityViewModel, "individualUnblockActivityViewModel");'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Profile views
|
||||
|
||||
Don't let others know you viewed their profile.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `ProfileViews.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// ProfileViews.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Quick block
|
||||
|
||||
Ability to block users quickly.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `QuickBlock.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// QuickBlock.kt
|
||||
val blockViewModel = "ms0" // search for '("STATUS_BLOCK_DIALOG_SHOWN", 1)'
|
||||
val profileViewHolder = "com.grindrapp.android.ui.profileV2.g" // search for 'com.grindrapp.android.ui.profileV2.ProfileViewHolder$onBind$3'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,38 @@
|
||||
# Signature spoofer
|
||||
|
||||
Forces Firebase / Facebook SDKs to report the *original* Grindr APK
|
||||
signature instead of the LSPatch-repackaged one. Without this, Firebase
|
||||
Installations + Facebook Login reject mismatched signatures.
|
||||
|
||||
Unlike most hooks in this package, this file is a top-level function
|
||||
called directly from `XposedLoader.handleLoadPackage` — it is not
|
||||
registered with `HookManager` and never appears in the in-app settings
|
||||
UI.
|
||||
|
||||
## Target
|
||||
|
||||
```kotlin
|
||||
// SignatureSpoofer.kt
|
||||
findAndHookMethod("com.google.firebase.installations.remote.FirebaseInstallationServiceClient", …, "getFingerprintHashForPackage", …)
|
||||
findAndHookMethod("com.google.firebase.remoteconfig.internal.ConfigRealtimeHttpClient", …, "getFingerprintHashForPackage", …)
|
||||
findAndHookMethod("com.google.firebase.remoteconfig.internal.ConfigFetchHttpClient", …, "getFingerprintHashForPackage", …)
|
||||
findAndHookMethod("ly.img.android.c", …, "d", …) // getPackageName
|
||||
findAndHookMethod("com.facebook.login.KatanaProxyLoginMethodHandler", …, "tryAuthorize", …)
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — stable (targets public Firebase / Facebook SDK)
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
- Targets fully-qualified classes from Firebase / Facebook / Ly — the
|
||||
obfuscation-marker convention doesn't apply because these symbols are
|
||||
stable across Grindr releases.
|
||||
- `ly.img.android.c::d` is an obfuscated symbol (Ly Image SDK), not a
|
||||
Grindr symbol. It maps to `getPackageName`; if Ly renames it in a
|
||||
future release, re-search JADX for the equivalent.
|
||||
- The Firebase / Facebook `getFingerprintHashForPackage` hook always
|
||||
returns `823f5a17c33b16b4775480b31607e7df35d67af8` — the original
|
||||
Grindr signing-cert SHA-1 fingerprint.
|
||||
@@ -0,0 +1,31 @@
|
||||
# SSL unpinning
|
||||
|
||||
Disables TLS certificate pinning in the host app so Mitmproxy / HTTP Toolkit
|
||||
can intercept HTTPS traffic during development.
|
||||
|
||||
Unlike most hooks in this package, this file is a top-level function
|
||||
called directly from `XposedLoader.handleLoadPackage` under
|
||||
`BuildConfig.DEBUG` — it is not registered with `HookManager` and never
|
||||
appears in the in-app settings UI.
|
||||
|
||||
## Target
|
||||
|
||||
```kotlin
|
||||
// SSLUnpinning.kt
|
||||
findAndHookConstructor("okhttp3.OkHttpClient$Builder", …)
|
||||
findAndHookMethod("okhttp3.OkHttpClient$Builder", …, "certificatePinner", …)
|
||||
findAndHookMethod("com.android.org.conscrypt.TrustManagerImpl", …, "verifyChain", …)
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — stable (targets public OkHttp / Conscrypt API)
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
- Targets fully-qualified classes from OkHttp / Conscrypt / Firebase —
|
||||
the obfuscation-marker convention doesn't apply because these symbols
|
||||
are stable across Grindr releases.
|
||||
- Only runs under `BuildConfig.DEBUG` (see `XposedLoader`); release
|
||||
builds don't unpin.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Status Dialog
|
||||
|
||||
Check whether GrindrPlus is alive or not.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `StatusDialog.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// StatusDialog.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Timber Logging
|
||||
|
||||
Forces Timber to log messages even if no tree is planted.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `TimberLogging.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// TimberLogging.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Unlimited albums
|
||||
|
||||
Allow to be able to view unlimited albums.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `UnlimitedAlbums.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// UnlimitedAlbums.kt
|
||||
val albumsService = "com.grindrapp.android.api.albums.AlbumsRestService" // search for 'v1/albums/red-dot'
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# Unlimited profiles
|
||||
|
||||
Allow unlimited profiles.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `UnlimitedProfiles.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// UnlimitedProfiles.kt
|
||||
val onProfileClicked = "com.grindrapp.android.ui.browse.e0" // search for 'com.grindrapp.android.ui.browse.ServerDrivenCascadeViewModel$onProfileClicked$1'
|
||||
// search for '<unique snippet>'
|
||||
// search for 'new StringBuilder("cascadeClickEvent/position=");'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Unlock Explorer
|
||||
|
||||
Unlock all profiles in Explorer.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `UnlockExplorer.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// UnlockExplorer.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,27 @@
|
||||
# Keep Alive WebSocket
|
||||
|
||||
Prevents WebSocket disconnections when app goes to background. Causes battery drain, use with caution.
|
||||
|
||||
## Target
|
||||
|
||||
<!-- Search markers used to locate this hook's symbols in newer Grindr APKs.
|
||||
Sourced from `WebSocketAlive.kt`. When porting this hook to a new
|
||||
Grindr version, paste each snippet into JADX's search box to
|
||||
locate the new obfuscated class / method name.
|
||||
|
||||
See `AGENTS.md` § 3 (Hook Authoring Rules) for the full convention. -->
|
||||
|
||||
```kotlin
|
||||
// WebSocketAlive.kt
|
||||
// search for '<unique snippet>'
|
||||
```
|
||||
|
||||
## Verified against
|
||||
|
||||
- Grindr 26.13.0 — initial port
|
||||
- (latest verified Grindr version)
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Optional: feature flag gates, why a non-obvious method-body hook was needed,
|
||||
behavior on rooted vs unrooted devices, etc. -->
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
Executable
+270
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check-obfuscated-symbols.py — walk every hook file in --hooks, extract the
|
||||
`// search for '<snippet>'` markers, and grep a JADX-decompiled APK for them.
|
||||
|
||||
Outputs a Markdown table to STDOUT:
|
||||
|
||||
| Status | Marker | Old symbol | Candidates | Confidence |
|
||||
|
||||
Status ∈ {OK, AMBIGUOUS, STALE}.
|
||||
- OK — exactly one candidate file matched.
|
||||
- AMBIGUOUS — multiple candidate files matched; human must disambiguate.
|
||||
- STALE — no candidate file matched; the snippet may be obsolete.
|
||||
|
||||
By default the script exits 0 even on STALE markers (grep-friendly).
|
||||
Pass --strict to exit non-zero on STALE (useful in CI when you want
|
||||
to force a maintainer to investigate before merging).
|
||||
|
||||
Pure stdlib — no third-party deps so it can run anywhere with Python 3.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
MARKER_RE = re.compile(
|
||||
r"""val\s+(?P<name>\w+)\s*=\s*"(?P<symbol>[^"]+)"\s*//\s*search for\s+'(?P<snippet>[^']+)'""",
|
||||
re.MULTILINE,
|
||||
)
|
||||
# Variant for comments above hook calls — capture the next line's symbol
|
||||
# and method body. We only extract snippet text in that case.
|
||||
BODY_MARKER_RE = re.compile(
|
||||
r"""//\s*search for\s+'(?P<snippet>[^']+)'""",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarkerHit:
|
||||
"""A single (snippet, old_symbol) tuple we found in a hook file."""
|
||||
file: Path
|
||||
line: int
|
||||
name: str | None # val name, if the marker is on a val line
|
||||
symbol: str | None # captured obfuscated name (val RHS)
|
||||
snippet: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Candidate:
|
||||
"""A single JADX-output match for a snippet."""
|
||||
path: Path
|
||||
line_no: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
marker: MarkerHit
|
||||
candidates: list[Candidate] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
if not self.candidates:
|
||||
return "STALE"
|
||||
if len(self.candidates) == 1:
|
||||
return "OK"
|
||||
return "AMBIGUOUS"
|
||||
|
||||
@property
|
||||
def confidence(self) -> str:
|
||||
s = self.status
|
||||
if s == "OK":
|
||||
return "high"
|
||||
if s == "AMBIGUOUS":
|
||||
return "low"
|
||||
return "n/a"
|
||||
|
||||
|
||||
def walk_hook_files(hooks_dir: Path) -> Iterable[Path]:
|
||||
for p in sorted(hooks_dir.glob("*.kt")):
|
||||
yield p
|
||||
|
||||
|
||||
def extract_markers(kt_file: Path) -> list[MarkerHit]:
|
||||
"""Return every `val X = "y" // search for 'snippet'` line in the file."""
|
||||
hits: list[MarkerHit] = []
|
||||
text = kt_file.read_text(encoding="utf-8", errors="replace")
|
||||
# First pass: val-line markers
|
||||
for m in MARKER_RE.finditer(text):
|
||||
line = text.count("\n", 0, m.start()) + 1
|
||||
hits.append(
|
||||
MarkerHit(
|
||||
file=kt_file,
|
||||
line=line,
|
||||
name=m.group("name"),
|
||||
symbol=m.group("symbol"),
|
||||
snippet=m.group("snippet"),
|
||||
)
|
||||
)
|
||||
# Second pass: body-comment markers (// search for '<snippet>' on its own line).
|
||||
# These don't carry an `old_symbol` because they comment a hook call site.
|
||||
for m in BODY_MARKER_RE.finditer(text):
|
||||
snippet = m.group("snippet")
|
||||
# Skip duplicates already captured by the val-line pass.
|
||||
if any(h.snippet == snippet and h.file == kt_file for h in hits):
|
||||
continue
|
||||
line = text.count("\n", 0, m.start()) + 1
|
||||
hits.append(
|
||||
MarkerHit(
|
||||
file=kt_file,
|
||||
line=line,
|
||||
name=None,
|
||||
symbol=None,
|
||||
snippet=snippet,
|
||||
)
|
||||
)
|
||||
return hits
|
||||
|
||||
|
||||
def grep_snippet(jadx_root: Path, snippet: str) -> list[Candidate]:
|
||||
"""grep -rF -- '<snippet>' <jadx_root>/sources"""
|
||||
sources = jadx_root / "sources"
|
||||
if not sources.is_dir():
|
||||
# Older JADX layouts just dump everything into the root.
|
||||
sources = jadx_root
|
||||
|
||||
# Use subprocess so we don't have to read every file into Python.
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["grep", "-rF", "-n", "--", snippet, str(sources)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return []
|
||||
|
||||
candidates: list[Candidate] = []
|
||||
for raw in out.stdout.splitlines():
|
||||
if ":" not in raw:
|
||||
continue
|
||||
path_str, _, line_str = raw.partition(":")
|
||||
try:
|
||||
line_no = int(line_str.split(":", 1)[0])
|
||||
except ValueError:
|
||||
continue
|
||||
candidates.append(Candidate(path=Path(path_str), line_no=line_no))
|
||||
return candidates
|
||||
|
||||
|
||||
def render_markdown(results: list[Result]) -> str:
|
||||
out = [
|
||||
"# Obfuscation-marker scan",
|
||||
"",
|
||||
f"Markers found: **{len(results)}**",
|
||||
"",
|
||||
"| Status | Marker | Old symbol | Candidates | Confidence | File |",
|
||||
"|--------|--------|------------|------------|------------|------|",
|
||||
]
|
||||
for r in results:
|
||||
marker = r.marker
|
||||
# Truncate long snippets for the table.
|
||||
snip = marker.snippet if len(marker.snippet) <= 60 else marker.snippet[:57] + "…"
|
||||
sym = marker.symbol or "(body)"
|
||||
if not r.candidates:
|
||||
cands = "—"
|
||||
else:
|
||||
shown = []
|
||||
for c in r.candidates[:3]:
|
||||
rel = c.path
|
||||
# Show as JADX-rooted path so the maintainer can locate it.
|
||||
shown.append(f"`{rel}`:{c.line_no}")
|
||||
cands = "; ".join(shown)
|
||||
if len(r.candidates) > 3:
|
||||
cands += f" (+{len(r.candidates) - 3} more)"
|
||||
rel = marker.file.name
|
||||
out.append(
|
||||
f"| {r.status} | `{snip}` | `{sym}` | {cands} | {r.confidence} | `{rel}` |"
|
||||
)
|
||||
out.append("")
|
||||
n_stale = sum(1 for r in results if r.status == "STALE")
|
||||
n_ambig = sum(1 for r in results if r.status == "AMBIGUOUS")
|
||||
n_ok = sum(1 for r in results if r.status == "OK")
|
||||
out.append(
|
||||
f"Summary: **{n_ok} OK**, **{n_ambig} AMBIGUOUS**, **{n_stale} STALE**"
|
||||
)
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def render_json(results: list[Result]) -> str:
|
||||
import json
|
||||
return json.dumps(
|
||||
[
|
||||
{
|
||||
"file": str(r.marker.file),
|
||||
"line": r.marker.line,
|
||||
"val_name": r.marker.name,
|
||||
"old_symbol": r.marker.symbol,
|
||||
"snippet": r.marker.snippet,
|
||||
"status": r.status,
|
||||
"confidence": r.confidence,
|
||||
"candidates": [
|
||||
{"path": str(c.path), "line": c.line_no}
|
||||
for c in r.candidates
|
||||
],
|
||||
}
|
||||
for r in results
|
||||
],
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Scan // search-for markers in hook files against a JADX-out dir."
|
||||
)
|
||||
p.add_argument(
|
||||
"--jadx",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to JADX decompilation output (the dir that contains sources/).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--hooks",
|
||||
type=Path,
|
||||
default=Path("app/src/main/java/com/grindrplus/hooks"),
|
||||
help="Path to the hooks/ source directory.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--format",
|
||||
choices=("markdown", "json"),
|
||||
default="markdown",
|
||||
)
|
||||
p.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Exit non-zero if any STALE markers are found.",
|
||||
)
|
||||
args = p.parse_args(argv)
|
||||
|
||||
if not args.hooks.is_dir():
|
||||
print(f"::error::hooks dir not found: {args.hooks}", file=sys.stderr)
|
||||
return 2
|
||||
if not args.jadx.exists():
|
||||
print(f"::error::JADX dir not found: {args.jadx}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
results: list[Result] = []
|
||||
for kt in walk_hook_files(args.hooks):
|
||||
for marker in extract_markers(kt):
|
||||
cands = grep_snippet(args.jadx, marker.snippet)
|
||||
results.append(Result(marker=marker, candidates=cands))
|
||||
|
||||
if args.format == "json":
|
||||
print(render_json(results))
|
||||
else:
|
||||
print(render_markdown(results))
|
||||
|
||||
if args.strict and any(r.status == "STALE" for r in results):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# check-patch-docs.sh — every hook in --hooks-dir must have a corresponding
|
||||
# docs/patches/<kebab>.md. Exits 1 and lists the missing docs if any are absent.
|
||||
#
|
||||
# Mapping: AntiBlock.kt -> anti_block.md (snake_case, lowercase, .md).
|
||||
#
|
||||
# Defaults assume the standard repo layout. Override with --hooks-dir / --docs-dir.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HOOKS_DIR="app/src/main/java/com/grindrplus/hooks"
|
||||
DOCS_DIR="docs/patches"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--hooks-dir)
|
||||
HOOKS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--docs-dir)
|
||||
DOCS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown flag: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [[ ! -d "$HOOKS_DIR" ]]; then
|
||||
echo "::error::hooks dir not found: $HOOKS_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$DOCS_DIR" ]]; then
|
||||
echo "::error::docs dir not found: $DOCS_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Convert PascalCase.kt -> snake_case.md via Python (more portable than bash).
|
||||
# Handles acronyms gracefully: `SSLUnpinning` -> `ssl_unpinning`, not `s_s_l_unpinning`.
|
||||
kebab() {
|
||||
python3 - "$1" <<'PY'
|
||||
import re, sys
|
||||
name = sys.argv[1]
|
||||
stem = name[:-3] if name.endswith(".kt") else name
|
||||
# Step 1: only insert an underscore when an uppercase letter follows a lowercase.
|
||||
# (This is what most editors do for snake-case conversion.)
|
||||
stem = re.sub(r'(?<=[a-z])(?=[A-Z])', '_', stem)
|
||||
# Step 2: an all-caps cluster of length >= 2 followed by an uppercase + lowercase
|
||||
# pair is treated as an acronym; insert the boundary underscore there.
|
||||
# e.g. `SSLUnpinning` -> `SSL_Unpinning`.
|
||||
stem = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', stem)
|
||||
# Step 3: lowercase.
|
||||
print(stem.lower())
|
||||
PY
|
||||
}
|
||||
|
||||
missing=()
|
||||
present=()
|
||||
|
||||
shopt -s nullglob
|
||||
for kt in "$HOOKS_DIR"/*.kt; do
|
||||
base="$(basename "$kt")"
|
||||
doc_name="$(kebab "$base").md"
|
||||
if [[ -f "$DOCS_DIR/$doc_name" ]]; then
|
||||
present+=("$doc_name")
|
||||
else
|
||||
missing+=("$base -> $doc_name")
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
echo "Hooks scanned: $(( ${#present[@]} + ${#missing[@]} ))"
|
||||
echo "Docs present: ${#present[@]}"
|
||||
echo "Docs missing: ${#missing[@]}"
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Missing patch docs:"
|
||||
for m in "${missing[@]}"; do
|
||||
echo " - $m"
|
||||
done
|
||||
echo ""
|
||||
echo "::error::Add docs/patches/<kebab>.md for each hook above."
|
||||
echo "::error::See .claude/skills/patch-doc-coverage/ for the stub template."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All hooks have patch docs."
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sync-grindr-version.sh — refresh version.json + patch the two listOf(...) lines
|
||||
# in app/build.gradle.kts in one pass.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/sync-grindr-version.sh # apply for real
|
||||
# bash scripts/sync-grindr-version.sh --dry-run # print the diff, don't write
|
||||
# bash scripts/sync-grindr-version.sh --gradle-kts path/to/build.gradle.kts
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — success (or "already up to date")
|
||||
# 1 — APKMirror unreachable / fetch_version.py failed
|
||||
# 2 — could not parse fetched version
|
||||
# 3 — gradle-kts file not found
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
GRADLE_KTS="app/build.gradle.kts"
|
||||
DRY_RUN="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN="true"
|
||||
shift
|
||||
;;
|
||||
--gradle-kts)
|
||||
GRADLE_KTS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
sed -n '2,18p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown flag: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [[ ! -f "$GRADLE_KTS" ]]; then
|
||||
echo "::error::Gradle build file not found: $GRADLE_KTS" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
VERSION_JSON="version.json"
|
||||
TMP_JSON="version.json.tmp"
|
||||
|
||||
echo ">>> Fetching latest Grindr version via fetch_version.py"
|
||||
if ! python3 fetch_version.py -o "$TMP_JSON"; then
|
||||
echo "::error::fetch_version.py failed (APKMirror unreachable?)" >&2
|
||||
rm -f "$TMP_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "::error::jq is required to parse version.json" >&2
|
||||
rm -f "$TMP_JSON"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
NEW_NAME="$(jq -r '.versionName' "$TMP_JSON")"
|
||||
NEW_CODE="$(jq -r '.versionCode' "$TMP_JSON")"
|
||||
|
||||
if [[ -z "$NEW_NAME" || -z "$NEW_CODE" || "$NEW_NAME" == "null" || "$NEW_CODE" == "null" ]]; then
|
||||
echo "::error::Could not parse versionName/versionCode from fetch output" >&2
|
||||
rm -f "$TMP_JSON"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CURRENT_NAME="$(grep -E '^[[:space:]]*val grindrVersionName' "$GRADLE_KTS" \
|
||||
| sed -E 's/.*listOf\("([^"]+)".*/\1/' | head -n1)"
|
||||
CURRENT_CODE="$(grep -E '^[[:space:]]*val grindrVersionCode' "$GRADLE_KTS" \
|
||||
| sed -E 's/.*listOf\(([0-9]+).*/\1/' | head -n1)"
|
||||
|
||||
if [[ "$CURRENT_NAME" == "$NEW_NAME" && "$CURRENT_CODE" == "$NEW_CODE" ]]; then
|
||||
echo "Already up to date: versionName=$NEW_NAME versionCode=$NEW_CODE"
|
||||
cp "$TMP_JSON" "$VERSION_JSON"
|
||||
rm -f "$TMP_JSON" "${GRADLE_KTS}.bak"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Bumping: versionName $CURRENT_NAME -> $NEW_NAME; versionCode $CURRENT_CODE -> $NEW_CODE"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "--- diff (dry-run; nothing written) ---"
|
||||
echo "--- version.json ---"
|
||||
diff -u "$VERSION_JSON" "$TMP_JSON" || true
|
||||
echo "--- $GRADLE_KTS ---"
|
||||
sed -E \
|
||||
-e "s/(grindrVersionName = listOf\\(\")[^\"]+(\"\\))/\1${NEW_NAME}\2/" \
|
||||
-e "s/(grindrVersionCode = listOf\\()[0-9]+(\\))/\1${NEW_CODE}\2/" \
|
||||
"$GRADLE_KTS" \
|
||||
| diff -u "$GRADLE_KTS" - || true
|
||||
rm -f "$TMP_JSON"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Apply version.json
|
||||
cp "$TMP_JSON" "$VERSION_JSON"
|
||||
rm -f "$TMP_JSON"
|
||||
|
||||
# Patch the two listOf(...) lines in build.gradle.kts
|
||||
sed -i.bak -E \
|
||||
-e "s/(grindrVersionName = listOf\\(\")[^\"]+(\"\\))/\1${NEW_NAME}\2/" \
|
||||
-e "s/(grindrVersionCode = listOf\\()[0-9]+(\\))/\1${NEW_CODE}\2/" \
|
||||
"$GRADLE_KTS"
|
||||
|
||||
echo "Wrote $VERSION_JSON and patched $GRADLE_KTS"
|
||||
echo "Verify with: ./gradlew printVersionInfo --quiet"
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "./version.schema.json",
|
||||
"versionName": "26.13.0",
|
||||
"versionCode": 170510
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/Heretek-AI/GrindrPlus/version.schema.json",
|
||||
"title": "GrindrPlus target Grindr version metadata",
|
||||
"description": "The single source of truth for which Grindr (com.grindrapp.android) version this module targets. Updated by fetch_version.py; read by app/build.gradle.kts to set grindrVersionName/grindrVersionCode.",
|
||||
"type": "object",
|
||||
"required": ["versionName", "versionCode"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"versionName": {
|
||||
"type": "string",
|
||||
"description": "Grindr's semantic version (X.Y.Z). Mirrors the value APKMirror exposes for the latest com.grindrapp.android release.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
||||
"examples": ["26.13.0", "25.21.1"]
|
||||
},
|
||||
"versionCode": {
|
||||
"type": "integer",
|
||||
"description": "Grindr's build number (positive integer). Mirrors the value APKMirror exposes for the latest com.grindrapp.android release.",
|
||||
"minimum": 1,
|
||||
"examples": [170510, 148541]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user