# android-reverse-engineering

Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP API endpoints (Retrofit, OkHttp, Volley), and trace call flows from UI to network layer. Use when the user wants to decompile, analyze, or reverse engineer Android packages, find API endpoints, or follow call flows. 中文触发词：反编译APK、安卓逆向、提取API、分析安卓应用、反编译安卓、逆向工程、追踪调用链、提取接口

- **Kind:** skill
- **Source:** https://github.com/SimoneAvogadro/android-reverse-engineering-skill
- **Page:** https://forefy.com/skills/d9072cdd-6593-4161-82e1-1d7a45d2efa0
- **API (JSON + files):** https://forefy.com/api/asr/d9072cdd-6593-4161-82e1-1d7a45d2efa0

---

## SKILL.md

---
name: android-reverse-engineering
description: Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP API endpoints (Retrofit, OkHttp, Volley), and trace call flows from UI to network layer. Use when the user wants to decompile, analyze, or reverse engineer Android packages, find API endpoints, or follow call flows. 中文触发词：反编译APK、安卓逆向、提取API、分析安卓应用、反编译安卓、逆向工程、追踪调用链、提取接口
trigger: decompile APK|decompile XAPK|reverse engineer Android|extract API|analyze Android|jadx|fernflower|vineflower|follow call flow|decompile JAR|decompile AAR|Android reverse engineering|find API endpoints|反编译APK|安卓逆向|提取API|分析安卓应用
---

# Android Reverse Engineering

Decompile Android APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower, trace call flows through application code and libraries, and produce structured documentation of extracted APIs. Two decompiler engines are supported — jadx for broad Android coverage and Fernflower for higher-quality output on complex Java code — and can be used together for comparison.

## Prerequisites

This skill requires **Java JDK 17+** and **jadx** to be installed. **Fernflower/Vineflower** and **dex2jar** are optional but recommended for better decompilation quality. Run the dependency checker to verify:

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh
```

On Windows (PowerShell):
```powershell
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1"
```

If anything is missing, follow the installation instructions in `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/setup-guide.md`.

## Workflow

### Phase 0: Fingerprint the App (recommended before anything else)

Before installing tools or decompiling, run a fast triage to determine what
kind of app you are looking at. **Decompiling Java is mostly useless for
Flutter, React Native, Cordova/Capacitor, and Xamarin apps** — the real code
lives elsewhere. The fingerprint script tells you which.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/fingerprint.sh <file.apk|file.xapk>
```

It prints, in one screen:

- **Mobile framework** (Flutter / React Native / Cordova / Xamarin / Native Kotlin / etc.) with the file marker that triggered the verdict.
- **HTTP stack** (Retrofit, OkHttp, Ktor, Apollo, Volley) detected via DEX string scan — works even when class names are obfuscated.
- **DI / serialization** signals (Hilt, Dagger, Koin, kotlinx.serialization, Moshi, Gson, Jackson).
- **Obfuscation level** estimate based on root-level short-named packages.
- **Notable third-party SDKs** (AppsFlyer, Datadog, Sentry, Firebase, payment SDKs, support/chat SDKs, etc.).
- **Consolidated native libraries** across the base APK and all splits — XAPK split bundles often place `.so` files in `config.<abi>.apk`, not in `base.apk`.
- **Recommended next step**, which differs by framework (e.g. for Flutter the script suggests `blutter` / `strings libapp.so` rather than jadx).

If the fingerprint says the app is Flutter / RN / Cordova / Xamarin, **stop**
and switch to the framework-appropriate tooling. Phases 1–5 below assume a
native (Java/Kotlin) Android app.

### Phase 1: Verify and Install Dependencies

Before decompiling, confirm that the required tools are available — and install any that are missing.

**Action**: Run the dependency check script.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh
```

On Windows (PowerShell):
```powershell
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1"
```

The output contains machine-readable lines:
- `INSTALL_REQUIRED:<dep>` — must be installed before proceeding
- `INSTALL_OPTIONAL:<dep>` — recommended but not blocking

**If required dependencies are missing** (exit code 1), install them automatically:

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
```

On Windows (PowerShell):
```powershell
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.ps1" <dep>
```

The install script detects the OS and package manager, then:
- Installs without sudo when possible (downloads to `~/.local/share/`, symlinks in `~/.local/bin/`)
- Uses sudo and the system package manager when necessary (apt, dnf, pacman)
- If sudo is needed but unavailable or the user declines, it prints the exact manual command and exits with code 2 — show these instructions to the user

**Windows notes**: The PowerShell install script uses `winget`, `scoop`, or `choco` (in that order). If none are available, it downloads directly to `%USERPROFILE%\.local\share\` and adds the directory to the user's PATH. After running `install-dep.ps1`, the PATH is persisted but the current terminal session may not see it. The `check-deps.ps1` and `decompile.ps1` scripts automatically refresh PATH from the user environment, so re-running them will find newly installed tools without restarting the terminal.

**For optional dependencies**, ask the user if they want to install them. Vineflower and dex2jar are recommended for best results.

After installation, re-run `check-deps.sh` to confirm everything is in place. Do not proceed to Phase 2 until all required dependencies are OK.

### Phase 2: Decompile

Use the decompile wrapper script to process the target file. The script supports three engines: `jadx`, `fernflower`, and `both`.

**Action**: Choose the engine and run the decompile script. The script handles APK, XAPK, JAR, and AAR files.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh [OPTIONS] <file>
```

On Windows (PowerShell):
```powershell
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.ps1" [OPTIONS] <file>
```

For **XAPK** files (ZIP bundles containing multiple APKs, used by APKPure and similar stores): the script automatically extracts the archive, identifies all APK files inside (base + split APKs), and decompiles each one into a separate subdirectory. The XAPK manifest is copied to the output for reference.

**Split/bundled APK detection**: Some APKs are actually bundle wrappers — the outer APK contains `base.apk` plus `split_config.*.apk` files inside its resources directory. When this happens, jadx will decompile the thin wrapper and produce very few Java files. The decompile scripts automatically detect this (≤10 Java files + inner APKs present) and re-decompile `base.apk` into an `<output>/base/` subdirectory. Config-only splits (ABI, language, density) are skipped. The main decompiled source will be in `<output>/base/sources/`.

Options:
- `-o <dir>` — Custom output directory (default: `<filename>-decompiled`)
- `--deobf` — Enable deobfuscation (recommended for obfuscated apps)
- `--no-res` — Skip resources, decompile code only (faster)
- `--engine ENGINE` — `jadx` (default), `fernflower`, or `both`

**Engine selection strategy**:

| Situation | Engine |
|---|---|
| First pass on any APK | `jadx` (fastest, handles resources) |
| JAR/AAR library analysis | `fernflower` (better Java output) |
| jadx output has warnings/broken code | `both` (compare and pick best per class) |
| Complex lambdas, generics, streams | `fernflower` |
| Quick overview of a large APK | `jadx --no-res` |

When using `--engine both`, the outputs go into `<output>/jadx/` and `<output>/fernflower/` respectively, with a comparison summary at the end showing file counts and jadx warning counts. Review classes with jadx warnings in the Fernflower output for better code.

For APK files with Fernflower, the script automatically uses dex2jar as an intermediate step. dex2jar must be installed for this to work.

See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/jadx-usage.md` and `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/fernflower-usage.md` for the full CLI references.

### Phase 3: Analyze Structure

Navigate the decompiled output to understand the app's architecture.

**Actions**:

1. **Read AndroidManifest.xml** from `<output>/resources/AndroidManifest.xml`:
   - Identify the main launcher Activity
   - List all Activities, Services, BroadcastReceivers, ContentProviders
   - Note permissions (especially `INTERNET`, `ACCESS_NETWORK_STATE`)
   - Find the application class (`android:name` on `<application>`)

2. **Survey the package structure** under `<output>/sources/`:
   - Identify the main app package and sub-packages
   - Distinguish app code from third-party libraries
   - Look for packages named `api`, `network`, `data`, `repository`, `service`, `retrofit`, `http` — these are where API calls live

3. **Read every `BuildConfig.java`** — these are almost never obfuscated and frequently leak the highest-signal constants in the entire APK (base URLs, flavor names, build type, third-party API keys, feature flags):
   ```bash
   find <output>/sources -name BuildConfig.java -exec grep -H '=' {} \;
   ```
   Each Gradle module emits its own `BuildConfig`, so expect 1–N hits. Read all of them.

4. **Identify the architecture pattern**:
   - MVP: look for `Presenter` classes
   - MVVM: look for `ViewModel` classes and `LiveData`/`StateFlow`
   - Clean Architecture: look for `domain`, `data`, `presentation` packages
   - This informs where to look for network calls in the next phases

### Phase 3.5: Recover Kotlin Class Names (only for obfuscated Kotlin apps)

If Phase 0 reported moderate / high obfuscation **and** the app is Kotlin
(Compose / kotlin_module markers detected), run the metadata recovery
script before tracing call flows. R8 obfuscates JVM symbols but cannot
strip Kotlin metadata strings, so original FQNs leak through
`@DebugMetadata` and `@Metadata.d2`.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/recover-kotlin-names.sh \
    <output>/sources <output>/mapping
```

Then use the lookup helper instead of plain grep — every hit comes
annotated with the owning class's real name:

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/lookup-name.sh \
    <output>/mapping --grep '"/api/' <output>/sources
```

Typical recovery on a real-world Kotlin app: ~100% of `*Repository` /
`*ViewModel` / `*UseCase` / `*Impl` classes, ~80% of DTOs.

See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/kotlin-name-recovery.md`
for the full technique and limitations.

### Phase 4: Trace Call Flows

Follow execution paths from user-facing entry points down to network calls.

**Actions**:

1. **Start from entry points**: Read the main Activity or Application class identified in Phase 3.

2. **Follow the initialization chain**: Application.onCreate() often sets up the HTTP client, base URL, and DI framework. Read this first.

3. **Trace user actions**: From an Activity, follow:
   - `onCreate()` → view setup → click listeners
   - Click handler → ViewModel/Presenter method
   - ViewModel → Repository → API service interface
   - API service → actual HTTP call

4. **Map DI bindings** (if Dagger/Hilt is used): Find `@Module` classes to understand which implementations are provided for which interfaces.

5. **Handle obfuscated code**: When class names are mangled, use string literals and library API calls as anchors. Retrofit annotations and URL strings are never obfuscated.

See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/call-flow-analysis.md` for detailed techniques and grep commands.

### Phase 5: Extract and Document APIs

Find all API endpoints and produce structured documentation.

**Action**: Run the API search script for a broad sweep.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/
```

On Windows (PowerShell):
```powershell
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/
```

Targeted searches:
```bash
# Only Retrofit
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/ --retrofit

# Only hardcoded URLs
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/ --urls

# Only auth patterns
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/ --auth
```

On Windows (PowerShell):
```powershell
# Only Retrofit
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Retrofit

# Only hardcoded URLs
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Urls

# Only auth patterns
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Auth
```

Document the endpoints in **two tiers** — going deep on every endpoint is
prohibitively expensive on apps with 100+ paths, and most of them do not
warrant it. Always produce Tier 1; expand Tier 2 only for the endpoints
that matter.

#### Tier 1 — flat inventory (always)

A single table covering every discovered endpoint. Aim for one line each;
if you cannot determine a column, write `?`.

| Host | Method | Path | Auth | Source file |
|------|--------|------|------|-------------|
| `api.example.com` | GET | `/v1/users/profile` | Bearer | `com/example/api/UserApi.java` |
| `api.example.com` | POST | `/v1/auth/login` | none | `com/example/api/AuthApi.java` |

This table answers "what does the backend look like" in one screen and
takes ~5 minutes to produce from the `--paths` output even on a large app.

#### Tier 2 — per-endpoint detail (only for high-value endpoints)

Reserve the detailed format for the few endpoints that actually need it:

- the entire authentication flow (login, refresh, logout, OTP/SMS, anonymous, registration)
- payment / checkout / order-creation endpoints
- anything the user explicitly asked about
- anything that looked unusual during the scan (custom signing, undocumented headers, etc.)

```markdown
### `METHOD /path`

- **Source**: `com.example.api.ApiService` (ApiService.java:42)
- **Base URL**: `https://api.example.com/v1`
- **Path params**: `id` (String)
- **Query params**: `page` (int), `limit` (int)
- **Headers**: `Authorization: Bearer <token>`
- **Request body**: `{ "email": "string", "password": "string" }`
- **Response**: `ApiResponse<User>`
- **Called from**: `LoginActivity → LoginViewModel → UserRepository → ApiService`
```

As a default, do not produce Tier 2 entries for more than ~10 endpoints
unless the user explicitly asks for more — Tier 1 plus a Tier 2 deep dive
on auth + 1-2 key flows is what most consumers of this work actually want.

See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/api-extraction-patterns.md` for library-specific search patterns and the full documentation template.

## Output

At the end of the workflow, deliver:

1. **Decompiled source** in the output directory
2. **Architecture summary** — app structure, main packages, pattern used
3. **API documentation** — all discovered endpoints in the format above
4. **Call flow map** — key paths from UI to network (especially authentication and main features)

## References

- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/setup-guide.md` — Installing Java, jadx, Fernflower/Vineflower, dex2jar, and optional tools
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/jadx-usage.md` — jadx CLI options and workflows
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/fernflower-usage.md` — Fernflower/Vineflower CLI options, when to use, APK workflow
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/api-extraction-patterns.md` — Library-specific search patterns and documentation template
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/call-flow-analysis.md` — Techniques for tracing call flows in decompiled code

## references

```

```

## references/api-extraction-patterns.md

# API Extraction Patterns

Patterns and grep commands for finding HTTP API calls in decompiled Android source code.

## Retrofit

Retrofit is the most common HTTP client in Android apps. API endpoints are declared as annotated interface methods.

### Annotations to search for

```bash
# HTTP method annotations
grep -rn '@GET\|@POST\|@PUT\|@DELETE\|@PATCH\|@HEAD' sources/

# Parameter annotations
grep -rn '@Query\|@QueryMap\|@Path\|@Body\|@Field\|@FieldMap\|@Part\|@Header\|@HeaderMap' sources/

# Headers annotation (static headers)
grep -rn '@Headers' sources/

# Base URL configuration
grep -rn 'baseUrl\|\.baseUrl(' sources/
```

### Typical Retrofit interface

```java
public interface ApiService {
    @GET("users/{id}")
    Call<User> getUser(@Path("id") String userId);

    @POST("auth/login")
    @Headers({"Content-Type: application/json"})
    Call<LoginResponse> login(@Body LoginRequest request);
}
```

When documenting, capture: HTTP method, path, path parameters, query parameters, request body type, response type, and any static headers.

## OkHttp

OkHttp is often used directly or as the transport layer for Retrofit.

```bash
# Request building
grep -rn 'Request\.Builder\|Request.Builder\|\.url(\|\.post(\|\.put(\|\.delete(\|\.patch(' sources/

# URL construction
grep -rn 'HttpUrl\|\.addQueryParameter\|\.addPathSegment' sources/

# Interceptors (often add auth headers)
grep -rn 'Interceptor\|addInterceptor\|addNetworkInterceptor\|intercept(' sources/

# Response handling
grep -rn '\.execute()\|\.enqueue(' sources/
```

## Ktor (Kotlin)

Ktor is the dominant HTTP client in Kotlin Multiplatform and modern
Kotlin-only Android apps. Unlike Retrofit, Ktor does **not** use annotations
to declare endpoints — paths appear as plain string arguments to
`client.get(...)` / `client.post(...)`, often inside an extension function.

```bash
# Calls
grep -rn '\b\(client\|httpClient\|HttpClient\)\.\(get\|post\|put\|delete\|patch\|head\|request\)\s*[<(]' sources/

# Default request / base URL configuration
grep -rn 'HttpRequestBuilder\|defaultRequest\s*{\|\burl\s*(\s*"\|URLBuilder' sources/

# Auth plugin (bearer / refresh)
grep -rn '\bbearer\s*{\|BearerTokens\s*(\|loadTokens\s*{\|refreshTokens\s*{' sources/
```

Typical Ktor call (after decompile):

```java
client.get("api/v1/users/profile") {
    parameter("locale", "en-US");
}
```

The base URL is usually applied via `defaultRequest { url { host = "..." } }`
in the client builder. Search for `host =` and `URLProtocol.HTTPS` references
to pin it down.

**Note on obfuscation:** in heavily R8-shrunk apps the call site
`client.get("path")` is inlined to something like `aVar.a(dVar, "path")`
and the `client.<verb>(` regex misses it. The path string itself is **not**
obfuscated, however — fall back to the generic path-literal search
(`--paths`) for the endpoint inventory in those cases. Ktor library
internals (`BearerTokens`, `loadTokens`, `refreshTokens`, `URLProtocol`)
remain searchable because Ktor keeps these on its public API.

Ktor's authentication plugin uses the
[`Auth { bearer { loadTokens { ... }; refreshTokens { ... } } }`](https://ktor.io/docs/auth.html)
DSL — bearer access tokens with automatic refresh. After R8, the DSL
lambdas appear as `Function2`/`Function3` impls referencing
`BearerTokens(...)` calls.

## Apollo Kotlin (GraphQL)

```bash
# Client setup
grep -rn 'ApolloClient\|\.serverUrl(\|HttpNetworkTransport' sources/

# Operations (queries / mutations / subscriptions)
grep -rn '\.query(\s*[A-Z]\|\.mutation(\s*[A-Z]\|\.subscription(\s*[A-Z]' sources/
```

Apollo generates one class per operation under a generated package; once you
find the GraphQL endpoint URL via `ApolloClient.serverUrl("...")`, use the
operation classes themselves as the API documentation — each carries its
GraphQL document text in `OPERATION_DOCUMENT`.

## Volley

```bash
grep -rn 'StringRequest\|JsonObjectRequest\|JsonArrayRequest\|Volley\.newRequestQueue\|RequestQueue' sources/
```

Volley requests typically pass the URL as a constructor argument and override `getHeaders()` or `getParams()` for custom headers/parameters.

## HttpURLConnection (legacy)

```bash
grep -rn 'HttpURLConnection\|HttpsURLConnection\|openConnection\|setRequestMethod\|setRequestProperty' sources/
```

## WebView

```bash
grep -rn 'loadUrl\|evaluateJavascript\|addJavascriptInterface\|WebViewClient\|shouldOverrideUrlLoading' sources/
```

WebView-based apps may load API endpoints via JavaScript bridges. Look for `@JavascriptInterface` annotated methods.

## Endpoint-Shaped Path Literals (obfuscation-resistant)

When the HTTP client cannot be identified (custom abstraction, heavy
inlining, KMP shared module), or the call sites are obfuscated to
`a.b(c, "path")`, fall back to extracting the path string literals
themselves. R8 does not obfuscate string contents, so paths leak through.

```bash
# All quoted strings shaped like an API path, deduplicated
grep -rhoE '"(/[A-Za-z0-9_{}.\-]+(/[A-Za-z0-9_{}.\-]+)+/?|(api|v[0-9]+|graphql|users?|account|auth|sso|oauth|profile|cart|basket|order|product|inventory|search|category|address|location|delivery|payment|invoice|favo[u]?rites?)(/[A-Za-z0-9_{}.\-]+)+/?)"' sources/ \
    | grep -Ev '^"(image|video|audio|text|application|content)/|^"/(proc|sys|dev|tmp|etc)/' \
    | sort -u
```

The skill ships this as `find-api-calls.sh --paths`, which prints both a
deduplicated inventory and the full list of call sites. On real-world
Kotlin apps this single command typically produces 100–300 distinct
endpoint paths, which is the most useful first artifact for documentation.

## Hardcoded URLs and Secrets

```bash
# HTTP/HTTPS URLs
grep -rn '"https\?://[^"]*"' sources/

# API keys and tokens
grep -rni 'api[_-]\?key\|api[_-]\?secret\|auth[_-]\?token\|bearer\|access[_-]\?token\|client[_-]\?secret' sources/

# Base URL constants
grep -rni 'BASE_URL\|API_URL\|SERVER_URL\|ENDPOINT\|API_BASE' sources/
```

## Documentation Template

For each discovered API endpoint, document it using this template:

```markdown
### `METHOD /path/to/endpoint`

- **Source**: `com.example.app.api.ApiService` (file:line)
- **Base URL**: `https://api.example.com/v1`
- **Full URL**: `https://api.example.com/v1/path/to/endpoint`
- **Path parameters**: `id` (String)
- **Query parameters**: `page` (int), `limit` (int)
- **Headers**:
  - `Authorization: Bearer <token>`
  - `Content-Type: application/json`
- **Request body**: `LoginRequest { email: String, password: String }`
- **Response type**: `ApiResponse<User>`
- **Notes**: Called from `LoginActivity.onLoginClicked()`
```

## Search Strategy

1. Start with **base URL constants** — find where the API root is configured
2. Search for **Retrofit interfaces** — they give the clearest picture of all endpoints
3. Check **interceptors** — they reveal auth schemes and common headers
4. Search for **hardcoded URLs** — catch any one-off API calls outside the main client
5. Look for **WebView URLs** — some apps use hybrid web/native approaches

## references/call-flow-analysis.md

# Call Flow Analysis

Techniques for tracing execution flows in decompiled Android applications, from entry points down to network calls.

## 1. Start from AndroidManifest.xml

The manifest declares all entry points. After decompilation, find it at:

```
<output-dir>/resources/AndroidManifest.xml
```

Key elements to look for:

```bash
# Activities (UI screens)
grep -n 'android:name=.*Activity' resources/AndroidManifest.xml

# Services (background work)
grep -n 'android:name=.*Service' resources/AndroidManifest.xml

# BroadcastReceivers
grep -n '<receiver' resources/AndroidManifest.xml

# ContentProviders
grep -n '<provider' resources/AndroidManifest.xml

# Launcher activity (main entry point)
grep -A5 'MAIN' resources/AndroidManifest.xml | grep 'android:name'
```

## 2. Follow the Android Lifecycle

Typical call chain from UI to network:

```
Activity.onCreate()
  → setContentView(R.layout.activity_main)
  → findViewById() / View Binding
  → button.setOnClickListener()
    → onClick()
      → viewModel.doSomething()
        → repository.fetchData()
          → apiService.getEndpoint()
            → HTTP request
```

Key lifecycle methods to search:

```bash
grep -rn 'onCreate\|onResume\|onStart\|onViewCreated' sources/
```

## 3. Identify Click Handlers

User interactions trigger API calls. Common patterns:

```bash
# XML onClick
grep -rn 'setOnClickListener\|onClick\|OnClickListener' sources/

# Data Binding
grep -rn '@BindingAdapter\|android:onClick' sources/ resources/

# Navigation actions
grep -rn 'findNavController\|NavController\|navigate(' sources/
```

## 4. Application Class Initialization

The `Application` subclass initializes global singletons (HTTP clients, DI frameworks, analytics):

```bash
# Find Application subclass
grep -rn 'extends Application\|: Application()' sources/

# Check onCreate for initialization
# Then read the class to see what gets configured at startup
```

Look for:
- Retrofit/OkHttp client setup
- Dagger/Hilt component initialization
- Firebase/analytics initialization
- Base URL configuration

## 5. Dependency Injection

### Dagger / Hilt

```bash
# Hilt modules
grep -rn '@Module\|@InstallIn\|@Provides\|@Binds' sources/

# Hilt entry points
grep -rn '@HiltAndroidApp\|@AndroidEntryPoint\|@HiltViewModel' sources/

# Dagger components
grep -rn '@Component\|@Subcomponent' sources/

# Injected fields
grep -rn '@Inject' sources/
```

### Koin

Koin is the dominant DI framework in Kotlin Multiplatform and a large
share of Kotlin-only Android apps. It uses a runtime DSL rather than
compile-time generated factories, so the search patterns are different:

```bash
# Confirm Koin is actually wired up
grep -rn 'org\.koin\.' sources/

# DI module declarations
grep -rn 'fun [A-Za-z]\+Module\|module\s*{\|module(' sources/

# Bindings inside a module DSL
grep -rn 'single\s*[<{(]\|factory\s*[<{(]\|viewModel\s*[<{(]\|scoped\s*[<{(]\|singleOf\|factoryOf' sources/

# Resolution call-sites (where a binding is consumed)
grep -rn '\bget\s*<\|\binject\s*<\|by\s\+inject\b\|by\s\+viewModel\b\|getKoin' sources/
```

After R8, every binding lambda becomes an anonymous
`Function2<Scope, ParametersHolder, T>` impl. To find the binding for an
interface `Foo`, look for files that contain both a Koin import / module
DSL marker and a reference to `Foo`:

```bash
grep -rln 'org\.koin\.core\.module' sources/ | xargs grep -l 'Foo'
```

### Trace through DI

1. Find where an interface is used (e.g. `ApiService` injected into a
   repository).
2. Find the `@Provides` / `@Binds` method (Hilt) **or** the
   `single { ... }` / `factory { ... }` block (Koin) that creates the
   implementation.
3. Follow the implementation to the actual HTTP call.

## 6. Find Constants and Configuration

Hardcoded values are rarely obfuscated:

```bash
# Base URLs
grep -rni 'BASE_URL\|API_URL\|SERVER_URL\|HOST' sources/

# API keys
grep -rni 'API_KEY\|CLIENT_ID\|APP_KEY\|SECRET' sources/

# BuildConfig values
grep -rn 'BuildConfig\.' sources/

# SharedPreferences keys (runtime config)
grep -rn 'getSharedPreferences\|getString(\|putString(' sources/
```

## 7. Navigating Obfuscated Code

When code is obfuscated (ProGuard/R8):

### What gets obfuscated
- Class names → `a`, `b`, `c`
- Method names → `a()`, `b()`, `c()`
- Field names → `f1234a`, `f1235b`

### What does NOT get obfuscated
- **String literals** — URLs, keys, error messages remain readable
- **Android framework classes** — `Activity`, `Fragment`, `Intent` keep their names
- **Library public APIs** — Retrofit annotations, OkHttp builders retain names
- **AndroidManifest entries** — Activity/Service names must be real

### Strategy for obfuscated code

1. **Start from strings**: Search for URLs, error messages, and known constants
2. **Start from framework classes**: Activities and Fragments are named in the manifest
3. **Follow library calls**: Retrofit `@GET`/`@POST` annotations are readable even when the interface class name is obfuscated
4. **Recover original Kotlin names from metadata**: `@DebugMetadata` and `@Metadata.d2` strings preserve the original FQNs even after R8 obfuscation. Run `scripts/recover-kotlin-names.sh` to build an `obf -> real` map (typically recovers 30-50% of classes — and almost 100% of `*Repository` / `*ViewModel` / `*Impl`). See [`kotlin-name-recovery.md`](./kotlin-name-recovery.md). This is the single highest-leverage step on any Kotlin app.
5. **Cross-reference**: If `class a` calls `Retrofit.create(b.class)`, then `b` is a Retrofit service interface
6. **`--deobf` is rarely enough on its own**: jadx's `--deobf` renames obfuscated symbols with synthetic placeholders (`p001a`, `C0123Foo`) — useful for disambiguation but it does **not** recover original names. Pair it with the metadata recovery above.

## 8. Tracing a Complete Call Flow: Example

Goal: Find how login works in an obfuscated app.

```
1. grep for "login" in strings → find "auth/login" URL in class `c.a.b.d`
2. Class `c.a.b.d` has @POST("auth/login") → it's a Retrofit interface
3. grep for `c.a.b.d` usage → class `c.a.b.f` calls it (the repository)
4. grep for `c.a.b.f` usage → class `c.a.a.g` calls it (the ViewModel)
5. grep for `c.a.a.g` usage → `LoginActivity` has a field of this type
6. Read LoginActivity.onCreate() → sets click listener → calls ViewModel method
```

Result: `LoginActivity → ViewModel → Repository → Retrofit @POST("auth/login")`

## 9. Tools and Commands Summary

| Goal | Command |
|---|---|
| Find entry points | `grep 'android:name' resources/AndroidManifest.xml` |
| Find lifecycle methods | `grep -rn 'onCreate\|onResume' sources/` |
| Find click handlers | `grep -rn 'setOnClickListener\|onClick' sources/` |
| Find DI bindings | `grep -rn '@Provides\|@Binds\|@Inject' sources/` |
| Find constants | `grep -rni 'BASE_URL\|API_KEY' sources/` |
| Find usages of a class | `grep -rn 'ClassName' sources/` |
| Follow a string | `grep -rn '"some text"' sources/` |

## references/fernflower-usage.md

# Fernflower / Vineflower CLI Reference

Fernflower is the JetBrains analytical Java decompiler. [Vineflower](https://github.com/Vineflower/vineflower) is the actively maintained community fork with better output quality and published releases. They share the same CLI interface.

## When to Use Fernflower vs jadx

| Scenario | Recommended |
|---|---|
| APK with resources needed | jadx |
| Standard Java JAR/library | Fernflower |
| jadx output has warnings/errors on specific classes | Fernflower on those classes |
| Complex lambdas, generics, streams | Fernflower |
| Large APK (>50MB), quick overview | jadx |
| Obfuscated Android app | jadx first, Fernflower on problem areas |
| Both decompilers available | Use `--engine both` and compare |

## Basic Usage

```bash
java -jar fernflower.jar [options] <source>... <destination>
```

- `<source>` — JAR file, class file, or directory containing class files
- `<destination>` — output directory

For a JAR input, Fernflower produces a JAR in the destination containing `.java` source files. Extract it with `unzip` to browse the sources.

## Key Options

Options use the format `-<key>=<value>`. Boolean options: `1` = enabled, `0` = disabled.

| Option | Default | Description |
|---|---|---|
| `-dgs=1` | 0 | Decompile generic signatures (recommended) |
| `-ren=1` | 0 | Rename obfuscated identifiers |
| `-mpm=60` | 0 | Max seconds per method — prevents hangs (recommended) |
| `-hes=0` | 1 | Show empty super() calls |
| `-hdc=0` | 1 | Show empty default constructors |
| `-udv=1` | 1 | Use debug variable names if available |
| `-ump=1` | 1 | Use debug parameter names if available |
| `-lit=1` | 0 | Output numeric literals as-is |
| `-asc=1` | 0 | Encode non-ASCII as unicode escapes |
| `-lac=1` | 0 | Decompile lambdas as anonymous classes |
| `-log=WARN` | INFO | Reduce output verbosity |
| `-e=<lib>` | — | Add library for context (not decompiled, improves type resolution) |

## Recommended Presets

### General use

```bash
java -jar fernflower.jar -dgs=1 -mpm=60 input.jar output/
```

### Obfuscated code

```bash
java -jar fernflower.jar -dgs=1 -ren=1 -mpm=60 input.jar output/
```

### Maximum detail

```bash
java -jar fernflower.jar -dgs=1 -hes=0 -hdc=0 -mpm=60 input.jar output/
```

### With Android SDK context (better type resolution)

```bash
java -jar fernflower.jar -dgs=1 -mpm=60 -e=$ANDROID_HOME/platforms/android-34/android.jar input.jar output/
```

## Working with APK Files

Fernflower cannot read APK/DEX files directly. Use dex2jar first:

```bash
# Step 1: Convert DEX to JAR
d2j-dex2jar -f -o app-converted.jar app.apk

# Step 2: Decompile with Fernflower
java -jar fernflower.jar -dgs=1 -mpm=60 app-converted.jar output/

# Step 3: Extract the resulting source JAR
unzip -o output/app-converted.jar -d output/sources/
```

The `decompile.sh --engine fernflower` script automates these steps.

## Supported Input Formats

| Format | Direct support | Via dex2jar |
|---|---|---|
| `.jar` | Yes | — |
| `.class` | Yes | — |
| `.zip` (with classes) | Yes | — |
| `.apk` | No | Yes |
| `.dex` | No | Yes |
| `.aar` | No | Yes |

## Output Format

- **JAR input** → Produces `<destination>/<input-name>.jar` containing `.java` files
- **Class file input** → Produces `.java` files directly in the destination
- **No resource decoding** — Fernflower only produces Java source, never XML/resources

## Fernflower vs Vineflower

Vineflower is the recommended fork. Improvements over upstream Fernflower:

- Published releases on GitHub and Maven Central
- Better handling of modern Java (records, sealed classes, pattern matching)
- More accurate lambda and switch expression decompilation
- Active bug fixes and community maintenance
- Same CLI interface — drop-in replacement

## references/jadx-usage.md

# jadx CLI Reference

## Basic Usage

```bash
jadx [options] <input-file>
```

Input can be an `.apk`, `.jar`, `.aar`, `.dex`, or `.zip` file.

## Key Options

| Option | Description |
|---|---|
| `-d <dir>` | Output directory for decompiled sources |
| `--deobf` | Enable deobfuscation — renames obfuscated classes/methods to readable names |
| `--show-bad-code` | Show partially decompiled code instead of error comments |
| `--no-res` | Skip resource decoding — faster when you only need code |
| `--no-src` | Skip source decompilation — only decode resources |
| `--export-gradle` | Generate a Gradle project structure (useful for importing into IDE) |
| `-e` | Same as `--export-gradle` |
| `--threads-count <N>` | Number of processing threads (default: CPU count) |
| `-Xmx<size>` | Set maximum Java heap (e.g., `-Xmx4g` for large APKs) |

## Decompiling Different File Types

### APK (Android Application Package)

```bash
jadx -d output-dir app.apk
```

Produces:
- `output-dir/sources/` — Decompiled Java source files
- `output-dir/resources/` — Decoded resources (AndroidManifest.xml, layouts, drawables, etc.)

### JAR (Java Archive)

```bash
jadx -d output-dir library.jar
```

Useful for analyzing third-party libraries bundled within an APK.

### AAR (Android Archive)

```bash
jadx -d output-dir library.aar
```

AAR files contain both compiled code and Android resources. jadx handles them directly.

## Handling Obfuscated Code

Apps built with ProGuard or R8 produce obfuscated bytecode with single-letter class and method names.

### Strategies

1. **Use `--deobf`** to generate readable replacement names:
   ```bash
   jadx --deobf -d output-dir app.apk
   ```
   jadx creates a mapping file at `output-dir/deobf-mapping.txt` that maps original obfuscated names to generated names.

2. **Use the ProGuard mapping file** if available (sometimes shipped in the APK under `assets/` or obtainable from build artifacts):
   ```bash
   jadx --deobf-map mapping.txt -d output-dir app.apk
   ```

3. **Focus on string constants and API calls** rather than class names when navigating obfuscated code. URL strings, annotation values, and library classes are not obfuscated.

## jadx-gui

For interactive exploration, use the GUI version:

```bash
jadx-gui app.apk
```

Features:
- Full-text search across all decompiled sources
- Click-through navigation (jump to definition)
- Deobfuscation with live renaming
- Smali view alongside Java

jadx-gui is included in the same distribution as the CLI tool.

## Common Workflows

### Code-only decompilation (fastest)

```bash
jadx --no-res --show-bad-code -d output app.apk
```

### Full decompilation with deobfuscation

```bash
jadx --deobf --show-bad-code -d output app.apk
```

### Export as Gradle project for IDE import

```bash
jadx -e -d output app.apk
# Then open output/ in Android Studio or IntelliJ
```

### Decompile a specific DEX from a multi-dex APK

Extract the APK (it's a ZIP), then target individual DEX files:

```bash
unzip app.apk -d extracted/
jadx -d output extracted/classes2.dex
```

## references/kotlin-name-recovery.md

# Recovering Original Class Names from Kotlin Metadata

When R8/ProGuard obfuscates a Kotlin app, JVM symbols are renamed but the
**Kotlin metadata strings cannot be stripped** — the Kotlin runtime depends
on them at runtime for reflection, coroutines, and `data class` features.

Two annotations leak the original fully-qualified names:

## `@DebugMetadata`

Generated for nearly every Kotlin coroutine `SuspendLambda` (i.e. almost
every `suspend` function in a modern app):

```java
@DebugMetadata(
    c  = "com.example.feature.account.AccountRepositoryImpl$fetch$1",
    f  = "AccountRepositoryImpl.kt",
    l  = {42, 51},
    m  = "invokeSuspend"
)
public final class a extends SuspendLambda implements Function2<...> { ... }
```

The `c =` field carries the original outer class FQN (with a `$` suffix
for inner / lambda scopes — strip everything after the first `$` to get the
declaring class).

## `@Metadata.d2`

Every Kotlin class carries a top-level `@Metadata` annotation. The `d2`
array lists internal class refs in JVM type-descriptor format
(`Lcom/example/Foo;`):

```java
@Metadata(d1 = {"..."},
          d2 = {"...","Lcom/example/feature/account/AccountRepositoryImpl;","..."})
public final class b implements ... { ... }
```

The first non-stdlib descriptor in `d2` is usually the file's primary
class.

## How to mine them

The skill ships two scripts:

```bash
# Build a mapping from a decompiled sources directory:
bash scripts/recover-kotlin-names.sh <output>/sources [mapping-dir]

# Outputs:
#   <mapping-dir>/mapping.tsv        obf_fqn  real_fqn  file
#   <mapping-dir>/mapping.json       same data, JSON
#   <mapping-dir>/by_package/        per-real-package index files

# Query the mapping:
bash scripts/lookup-name.sh <mapping-dir> Repository                 # search
bash scripts/lookup-name.sh <mapping-dir> -o ab.cd                   # obf -> real
bash scripts/lookup-name.sh <mapping-dir> -p com.example.feature     # list package
bash scripts/lookup-name.sh <mapping-dir> --grep '"api/' <output>/sources
   # ^ greps decompiled code and appends '// real.fqn' to each hit
```

## What you typically recover

On a real-world obfuscated Kotlin app the script recovers **30 – 50 % of
classes** — but more importantly, **almost 100 % of the classes you
actually want to read**:

| Class kind                | Recovery rate |
|---------------------------|---------------|
| `*Repository` / `*Impl`   | ~100 %        |
| `*ViewModel`              | ~100 %        |
| `*UseCase` / `*Interactor`| ~100 %        |
| Plain `data class` DTOs   | ~80 %         |
| Pure-Java helper classes  | low (no Kotlin metadata) |
| Anonymous inner classes   | sometimes recovered as the parent FQN |

## Why `jadx --deobf` is not enough

`--deobf` renames obfuscated identifiers using internal heuristics, but the
output is still synthetic (`p001a`, `C0123Foo`). It does **not** recover
the *original* names. Kotlin metadata recovery is the only reliable way to
map back to the names the developer actually wrote, and it costs essentially
nothing — just a regex pass over the decompiled sources.

Run both: `--deobf` for fields/methods that have no metadata source, plus
the recovery script for class names.

## Limitations

- **Method names and field names** are not recovered. Kotlin metadata only
  preserves class-level FQNs and a few signatures. For method names you
  still need jadx-gui's interactive rename or pattern inference.
- **Pure-Java classes** carry no `@Metadata`, so they remain obfuscated.
- **Heavily inlined classes** (`@JvmInline value class`, top-level fun
  files compiled into shared `*Kt.class` synthetic classes) sometimes show
  up under the wrong filename — treat results as a strong hint, not gospel.

## Reading flow with the mapping

1. Run `recover-kotlin-names.sh` once after decompiling.
2. Use `lookup-name.sh --grep '<pattern>' <sources>` instead of plain `grep`
   so every hit comes annotated with the real owning class.
3. When you hit an obfuscated FQN in code (e.g. `nq.e`), resolve it with
   `lookup-name.sh <mapping-dir> -o nq.e` — you will often see siblings
   (`nq.d`, `nq.f`, ...) that are the same class's split lambdas/inner
   classes, which is useful context.

## references/setup-guide.md

# Setup Guide: Dependencies for Android Reverse Engineering

## Java JDK 17+

jadx requires Java 17 or later.

### Ubuntu / Debian

```bash
sudo apt update
sudo apt install openjdk-17-jdk
```

### Fedora

```bash
sudo dnf install java-17-openjdk-devel
```

### Arch Linux

```bash
sudo pacman -S jdk17-openjdk
```

### macOS (Homebrew)

```bash
brew install openjdk@17
```

After installation on macOS, follow the symlink instructions printed by Homebrew, or add to your shell profile:

```bash
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
```

### Verify

```bash
java -version
# Should show version 17.x or higher
```

---

## jadx

jadx is the Java decompiler used to convert APK/JAR/AAR files to readable Java source.

### Option 1: GitHub Releases (recommended)

1. Go to <https://github.com/skylot/jadx/releases/latest>
2. Download the `jadx-<version>.zip` file (not the source archive)
3. Extract and add to PATH:

```bash
unzip jadx-*.zip -d ~/jadx
export PATH="$HOME/jadx/bin:$PATH"
# Add the export line to your ~/.bashrc or ~/.zshrc for persistence
```

### Option 2: Homebrew (macOS / Linux)

```bash
brew install jadx
```

### Option 3: Build from source

```bash
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
# Binaries will be in build/jadx/bin/
export PATH="$(pwd)/build/jadx/bin:$PATH"
```

### Verify

```bash
jadx --version
```

---

## Fernflower / Vineflower (optional, recommended)

Fernflower is the JetBrains Java decompiler. It produces better output than jadx on complex Java constructs, lambdas, and generics. [Vineflower](https://github.com/Vineflower/vineflower) is the actively maintained community fork with published releases — prefer it over upstream Fernflower.

### Option 1: Vineflower from GitHub Releases (recommended)

1. Go to <https://github.com/Vineflower/vineflower/releases/latest>
2. Download `vineflower-<version>.jar`
3. Place it and set the environment variable:

```bash
mkdir -p ~/vineflower
mv vineflower-*.jar ~/vineflower/vineflower.jar
export FERNFLOWER_JAR_PATH="$HOME/vineflower/vineflower.jar"
# Add the export to ~/.bashrc or ~/.zshrc for persistence
```

### Option 2: Build Fernflower from source

```bash
git clone https://github.com/JetBrains/fernflower.git
cd fernflower
./gradlew jar
# Produces: build/libs/fernflower.jar
export FERNFLOWER_JAR_PATH="$(pwd)/build/libs/fernflower.jar"
```

### Option 3: Homebrew (Vineflower)

```bash
brew install vineflower
```

### Verify

```bash
java -jar "$FERNFLOWER_JAR_PATH" --version
```

> **Note**: Fernflower only works on JVM bytecode (JAR, class files). For APK/DEX files, you also need **dex2jar** (see below) as an intermediate conversion step.

---

## dex2jar (optional, needed for Fernflower on APK files)

Converts Android DEX bytecode to standard Java JAR files.

### GitHub Releases

1. Go to <https://github.com/ThexXTURBOXx/dex2jar/releases/latest>
2. Download and extract:

```bash
unzip dex-tools-*.zip -d ~/dex2jar
export PATH="$HOME/dex2jar:$PATH"
```

### Homebrew

```bash
brew install dex2jar
```

### Verify

```bash
d2j-dex2jar --help
```

### Usage

```bash
# Convert APK (or DEX) to JAR
d2j-dex2jar -f -o output.jar app.apk

# Then decompile with Fernflower
java -jar vineflower.jar output.jar decompiled/
```

---

## Optional Tools

### apktool

Useful for decoding resources (XML layouts, drawables) that jadx sometimes handles poorly.

```bash
# Ubuntu/Debian
sudo apt install apktool

# macOS
brew install apktool

# Manual: https://apktool.org/docs/install
```

### adb (Android Debug Bridge)

Useful for pulling APKs directly from a connected Android device.

```bash
# Ubuntu/Debian
sudo apt install adb

# macOS
brew install android-platform-tools
```

Pull an APK from a device:

```bash
# List installed packages
adb shell pm list packages | grep <keyword>

# Get APK path
adb shell pm path com.example.app

# Pull the APK
adb pull /data/app/com.example.app-xxxx/base.apk ./app.apk
```

---

## Troubleshooting

| Problem | Solution |
|---|---|
| `jadx: command not found` | Ensure the jadx `bin/` directory is in your `$PATH` |
| `Error: Could not find or load main class` | Java is missing or wrong version — verify with `java -version` |
| jadx runs out of memory on large APKs | Increase heap: `jadx -Xmx4g -d output app.apk` or set `JAVA_OPTS="-Xmx4g"` |
| Decompiled code has many `// Error` comments | Try `--show-bad-code` to see partial output, or use `--deobf` for obfuscated apps |
| Fernflower hangs on a method | Use `-mpm=60` to set a 60-second timeout per method |
| Fernflower JAR not found | Set `FERNFLOWER_JAR_PATH` env variable to the full path of the JAR |
| dex2jar fails with `ZipException` | The APK may have a non-standard ZIP structure — try `jadx` instead |

## references/third_party_hosts.txt

```
# Third-party host denylist used by find-api-calls.sh --urls.
#
# Patterns are extended-regex hostname suffixes / fragments. A host is
# considered "third-party noise" if any pattern below matches anywhere
# in the hostname. Lines starting with '#' and blank lines are ignored.
#
# This list is intentionally conservative: when a pattern would hide a
# legitimate first-party host (e.g. an app may run its own *.s3.amazonaws.com
# bucket), keep the pattern but expect manual review of the bucketed output.

# Google / Firebase / Play / Crashlytics
\.googleapis\.com$
\.google\.com$
\.gstatic\.com$
\.googleusercontent\.com$
\.googletagmanager\.com$
\.googlesyndication\.com$
\.firebaseio\.com$
\.firebaseapp\.com$
\.firebaseinstallations\.googleapis\.com$
\.firebaseremoteconfig\.googleapis\.com$
\.crashlytics\.com$
\.app-measurement\.com$

# Apple / Microsoft / Adobe
\.apple\.com$
\.icloud\.com$
\.microsoft\.com$
\.live\.com$
\.office\.com$
\.adobe\.com$
ns\.adobe\.com

# Meta
\.facebook\.com$
\.fbcdn\.net$
\.instagram\.com$
\.whatsapp\.com$

# Other social / messaging / video
\.twitter\.com$
\.x\.com$
\.tiktok\.com$
\.youtube\.com$
\.youtu\.be$
\.linkedin\.com$
\.snapchat\.com$
\.pinterest\.com$
\.reddit\.com$

# Mobile attribution / analytics / observability
\.appsflyersdk\.com$
\.appsflyer\.com$
\.adjust\.com$
\.branch\.io$
\.amplitude\.com$
\.segment\.com$
\.mixpanel\.com$
\.hotjar\.com$
\.clarity\.ms$
\.datadoghq\.(com|eu|us)$
\.sentry\.io$
\.bugsnag\.com$
\.newrelic\.com$
\.instabug\.com$
\.embrace\.io$
\.rollout\.io$
\.launchdarkly\.com$

# Push / notifications
\.onesignal\.com$
\.urbanairship\.com$
\.airship\.com$

# Support / chat
\.zendesk\.com$
\.intercom\.io$
\.intercomcdn\.com$
\.helpshift\.com$
\.salesforce\.com$
\.freshchat\.com$
\.kustomerapp\.com$

# Payments
\.stripe\.com$
\.braintreepayments\.com$
\.braintreegateway\.com$
\.payu\.com$
\.payu\.in$
\.paypal\.com$
\.adyen\.com$
\.checkout\.com$
\.klarna\.com$

# Maps / location
\.mapbox\.com$
\.openstreetmap\.org$

# Storage / CDN (often third-party even when the bucket name is app-specific)
\.s3\.amazonaws\.com$
\.cloudfront\.net$
\.akamaihd\.net$
\.akamaized\.net$
\.fastly\.net$
\.cloudflare\.com$
\.azureedge\.net$

# DNS / well-known infra
\.localhost$
^localhost
^127\.

# Standards / RFCs / placeholders that show up as XML/XMP namespaces
\.w3\.org$
\.w3c\.org$
example\.(com|org|net)$

# Certificate authorities
\.sectigo\.com$
\.entrust\.com$
\.digicert\.com$
\.letsencrypt\.org$
```

## scripts

```

```

## scripts/check-deps.ps1

```

```

## scripts/check-deps.sh

```bash
#!/usr/bin/env bash
# check-deps.sh — Verify dependencies and report what's missing
# Output includes machine-readable INSTALL:<dep> lines for each missing dependency.
# The install-dep.sh script can install each one.
set -euo pipefail

REQUIRED_JAVA_MAJOR=17
errors=0
missing_required=()
missing_optional=()

echo "=== Android Reverse Engineering: Dependency Check ==="
echo

# --- Java ---
java_ok=false
if command -v java &>/dev/null; then
  java_version_output=$(java -version 2>&1 | head -1)
  java_version=$(echo "$java_version_output" | sed -n 's/.*"\([0-9]*\)\..*/\1/p')
  if [[ -z "$java_version" ]]; then
    java_version=$(echo "$java_version_output" | grep -oE '[0-9]+' | head -1)
  fi
  if [[ "$java_version" == "1" ]]; then
    java_version=$(echo "$java_version_output" | sed -n 's/.*"1\.\([0-9]*\)\..*/\1/p')
  fi

  if [[ -n "$java_version" ]] && (( java_version >= REQUIRED_JAVA_MAJOR )); then
    echo "[OK] Java $java_version detected"
    java_ok=true
  else
    echo "[WARN] Java detected but version $java_version is below $REQUIRED_JAVA_MAJOR"
    errors=$((errors + 1))
    missing_required+=("java")
  fi
else
  echo "[MISSING] Java is not installed or not in PATH"
  errors=$((errors + 1))
  missing_required+=("java")
fi

# --- jadx ---
if command -v jadx &>/dev/null; then
  jadx_version=$(jadx --version 2>/dev/null || echo "unknown")
  echo "[OK] jadx $jadx_version detected"
else
  echo "[MISSING] jadx is not installed or not in PATH"
  errors=$((errors + 1))
  missing_required+=("jadx")
fi

# --- Fernflower / Vineflower ---
ff_found=false
if command -v vineflower &>/dev/null; then
  echo "[OK] vineflower CLI detected"
  ff_found=true
elif command -v fernflower &>/dev/null; then
  echo "[OK] fernflower CLI detected"
  ff_found=true
else
  for candidate in \
    "${FERNFLOWER_JAR_PATH:-}" \
    "$HOME/.local/share/vineflower/vineflower.jar" \
    "$HOME/fernflower/build/libs/fernflower.jar" \
    "$HOME/vineflower/build/libs/vineflower.jar" \
    "$HOME/fernflower/fernflower.jar" \
    "$HOME/vineflower/vineflower.jar"; do
    if [[ -n "$candidate" ]] && [[ -f "$candidate" ]]; then
      echo "[OK] Fernflower/Vineflower JAR found: $candidate"
      ff_found=true
      break
    fi
  done
fi
if [[ "$ff_found" == false ]]; then
  echo "[MISSING] Fernflower/Vineflower not found (optional — better output on complex Java code)"
  missing_optional+=("vineflower")
fi

# --- dex2jar ---
if command -v d2j-dex2jar &>/dev/null || command -v d2j-dex2jar.sh &>/dev/null; then
  echo "[OK] dex2jar detected"
else
  echo "[MISSING] dex2jar not found (optional — needed to use Fernflower on APK/DEX files)"
  missing_optional+=("dex2jar")
fi

# --- Optional: apktool ---
if command -v apktool &>/dev/null; then
  echo "[OK] apktool detected (optional)"
else
  echo "[MISSING] apktool not found (optional — useful for resource decoding)"
  missing_optional+=("apktool")
fi

# --- Optional: adb ---
if command -v adb &>/dev/null; then
  echo "[OK] adb detected (optional)"
else
  echo "[MISSING] adb not found (optional — useful for pulling APKs from devices)"
  missing_optional+=("adb")
fi

# --- Machine-readable summary ---
echo
if [[ ${#missing_required[@]} -gt 0 ]]; then
  for dep in "${missing_required[@]}"; do
    echo "INSTALL_REQUIRED:$dep"
  done
fi
if [[ ${#missing_optional[@]} -gt 0 ]]; then
  for dep in "${missing_optional[@]}"; do
    echo "INSTALL_OPTIONAL:$dep"
  done
fi

echo
if (( errors > 0 )); then
  echo "*** ${#missing_required[@]} required dependency/ies missing. ***"
  echo "Run install-dep.sh <name> to install, or see references/setup-guide.md."
  exit 1
else
  if [[ ${#missing_optional[@]} -gt 0 ]]; then
    echo "Required dependencies OK. ${#missing_optional[@]} optional dependency/ies missing."
    echo "Run install-dep.sh <name> to install optional tools."
  else
    echo "All dependencies are installed. Ready to decompile."
  fi
  exit 0
fi
```

## scripts/decompile.ps1

```

```

## scripts/decompile.sh

```bash
#!/usr/bin/env bash
# decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both
set -euo pipefail

usage() {
  cat <<EOF
Usage: decompile.sh [OPTIONS] <file>

Decompile an Android APK, XAPK, JAR, or AAR file.

Arguments:
  <file>            Path to the .apk, .xapk, .jar, or .aar file

Options:
  -o, --output DIR  Output directory (default: <filename>-decompiled)
  --deobf           Enable deobfuscation of names
  --no-res          Skip resource decoding (faster, code-only)
  --engine ENGINE   Decompiler engine: jadx, fernflower, or both (default: jadx)
  -h, --help        Show this help message

Engines:
  jadx        Use jadx (default). Handles APK/JAR/AAR natively, decodes resources.
  fernflower  Use Fernflower/Vineflower. Better on complex Java, lambdas, generics.
              For APK files, requires dex2jar as intermediate step.
  both        Run both decompilers side by side for comparison.
              jadx output  → <output>/jadx/
              fernflower   → <output>/fernflower/

Environment:
  FERNFLOWER_JAR_PATH   Path to fernflower.jar or vineflower.jar

Examples:
  decompile.sh app-release.apk
  decompile.sh app-bundle.xapk
  decompile.sh --engine both --deobf app-release.apk
  decompile.sh --engine fernflower library.jar
EOF
  exit 0
}

# --- Parse arguments ---
OUTPUT_DIR=""
DEOBF=false
NO_RES=false
ENGINE="jadx"
INPUT_FILE=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    -o|--output)   OUTPUT_DIR="$2"; shift 2 ;;
    --deobf)       DEOBF=true; shift ;;
    --no-res)      NO_RES=true; shift ;;
    --engine)      ENGINE="$2"; shift 2 ;;
    -h|--help)     usage ;;
    -*)            echo "Error: Unknown option $1" >&2; usage ;;
    *)             INPUT_FILE="$1"; shift ;;
  esac
done

# --- Validate input ---
if [[ -z "$INPUT_FILE" ]]; then
  echo "Error: No input file specified." >&2
  usage
fi

if [[ ! -f "$INPUT_FILE" ]]; then
  echo "Error: File not found: $INPUT_FILE" >&2
  exit 1
fi

ext="${INPUT_FILE##*.}"
ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
case "$ext_lower" in
  apk|xapk|jar|aar) ;;
  *)
    echo "Error: Unsupported file type '.$ext'. Expected .apk, .xapk, .jar, or .aar" >&2
    exit 1
    ;;
esac

case "$ENGINE" in
  jadx|fernflower|both) ;;
  *)
    echo "Error: Unknown engine '$ENGINE'. Use jadx, fernflower, or both." >&2
    exit 1
    ;;
esac

BASENAME=$(basename "$INPUT_FILE" ".$ext_lower")
INPUT_FILE_ABS=$(realpath "$INPUT_FILE")

if [[ -z "$OUTPUT_DIR" ]]; then
  OUTPUT_DIR="${BASENAME}-decompiled"
fi

# --- XAPK handling ---
# XAPK is a ZIP containing one or more APKs, optional OBB files, and a manifest.json.
# We extract it, find all APKs inside, and decompile each one.
XAPK_EXTRACTED_DIR=""
XAPK_APK_FILES=()

if [[ "$ext_lower" == "xapk" ]]; then
  XAPK_EXTRACTED_DIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-extract-XXXXXX")
  echo "=== Extracting XAPK archive ==="
  unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_EXTRACTED_DIR"

  # Show manifest.json if present
  if [[ -f "$XAPK_EXTRACTED_DIR/manifest.json" ]]; then
    echo "XAPK manifest found:"
    cat "$XAPK_EXTRACTED_DIR/manifest.json"
    echo
  fi

  # Find all APK files inside
  while IFS= read -r -d '' apk_file; do
    XAPK_APK_FILES+=("$apk_file")
  done < <(find "$XAPK_EXTRACTED_DIR" -name "*.apk" -print0 | sort -z)

  if [[ ${#XAPK_APK_FILES[@]} -eq 0 ]]; then
    echo "Error: No APK files found inside XAPK archive." >&2
    rm -rf "$XAPK_EXTRACTED_DIR"
    exit 1
  fi

  echo "Found ${#XAPK_APK_FILES[@]} APK(s) inside XAPK:"
  for f in "${XAPK_APK_FILES[@]}"; do
    echo "  - $(basename "$f")"
  done
  echo
fi

# --- Locate fernflower JAR ---
find_fernflower_jar() {
  if [[ -n "${FERNFLOWER_JAR_PATH:-}" ]] && [[ -f "$FERNFLOWER_JAR_PATH" ]]; then
    echo "$FERNFLOWER_JAR_PATH"
    return
  fi
  # Check common locations
  for candidate in \
    "$HOME/fernflower/build/libs/fernflower.jar" \
    "$HOME/vineflower/build/libs/vineflower.jar" \
    "$HOME/fernflower/fernflower.jar" \
    "$HOME/vineflower/vineflower.jar"; do
    if [[ -f "$candidate" ]]; then
      echo "$candidate"
      return
    fi
  done
  return 1
}

# --- Locate dex2jar ---
find_dex2jar() {
  if command -v d2j-dex2jar &>/dev/null; then
    echo "d2j-dex2jar"
  elif command -v d2j-dex2jar.sh &>/dev/null; then
    echo "d2j-dex2jar.sh"
  else
    return 1
  fi
}

# --- jadx decompilation ---
run_jadx() {
  local out_dir="$1"
  local jadx_status=0
  local count=0

  if ! command -v jadx &>/dev/null; then
    echo "Error: jadx is not installed or not in PATH." >&2
    return 1
  fi

  local args=()
  args+=("-d" "$out_dir")
  [[ "$DEOBF" == true ]] && args+=("--deobf")
  [[ "$NO_RES" == true ]] && args+=("--no-res")
  args+=("--show-bad-code")
  args+=("$INPUT_FILE_ABS")

  echo "Running: jadx ${args[*]}"
  if jadx "${args[@]}"; then
    jadx_status=0
  else
    jadx_status=$?
  fi

  echo "jadx output: $out_dir/sources/"
  if [[ -d "$out_dir/sources" ]]; then
    count=$(find "$out_dir/sources" -name "*.java" | wc -l)
    echo "Java files decompiled by jadx: $count"
  fi

  if [[ $jadx_status -eq 0 ]]; then
    return 0
  fi

  if [[ $count -gt 0 ]]; then
    echo "Warning: jadx exited with status $jadx_status after writing $count Java files; treating this as partial success." >&2
    return 2
  fi

  echo "Error: jadx failed with status $jadx_status and produced no Java output." >&2
  return 1
}

# --- Fernflower decompilation ---
run_fernflower() {
  local out_dir="$1"
  local jar_to_decompile=""
  local converted_jar=""
  local intermediate_dir="$out_dir/intermediate"
  local ff_status=0
  local d2j_status=0
  local count=0
  local ff_timeout_seconds="${FERNFLOWER_TIMEOUT_SECONDS:-900}"

  local ff_jar
  if ! ff_jar=$(find_fernflower_jar); then
    echo "Error: Fernflower/Vineflower JAR not found." >&2
    echo "Set FERNFLOWER_JAR_PATH or see references/setup-guide.md" >&2
    return 1
  fi

  mkdir -p "$out_dir"

  # For APK/AAR, we need dex2jar first to convert DEX→JAR
  if [[ "$ext_lower" == "apk" || "$ext_lower" == "aar" ]]; then
    local d2j
    if ! d2j=$(find_dex2jar); then
      echo "Error: dex2jar is required to use Fernflower on .$ext_lower files." >&2
      echo "Install dex2jar — see references/setup-guide.md" >&2
      return 1
    fi

    echo "Converting $ext_lower to JAR with dex2jar..."
    mkdir -p "$intermediate_dir"
    converted_jar="$intermediate_dir/${BASENAME}-dex2jar.jar"
    if "$d2j" -f -o "$converted_jar" "$INPUT_FILE_ABS" 2>&1; then
      d2j_status=0
    else
      d2j_status=$?
    fi
    if [[ ! -f "$converted_jar" ]]; then
      echo "Error: dex2jar conversion failed with status $d2j_status." >&2
      return 1
    fi
    if [[ $d2j_status -ne 0 ]]; then
      echo "Warning: dex2jar exited with status $d2j_status but produced $converted_jar; continuing." >&2
    fi
    jar_to_decompile="$converted_jar"
  else
    jar_to_decompile="$INPUT_FILE_ABS"
  fi

  # Build fernflower args
  local ff_args=()
  ff_args+=("-dgs=1")   # decompile generic signatures
  ff_args+=("-mpm=60")  # 60s max per method to avoid hangs
  if [[ "$DEOBF" == true ]]; then
    ff_args+=("-ren=1")  # rename obfuscated identifiers
  fi
  ff_args+=("$jar_to_decompile")
  ff_args+=("$out_dir")

  echo "Running: java -jar $ff_jar ${ff_args[*]}"
  if command -v timeout &>/dev/null && [[ "$ff_timeout_seconds" =~ ^[0-9]+$ ]] && (( ff_timeout_seconds > 0 )); then
    echo "Fernflower timeout: ${ff_timeout_seconds}s (override with FERNFLOWER_TIMEOUT_SECONDS)"
    if timeout "${ff_timeout_seconds}s" java -jar "$ff_jar" "${ff_args[@]}"; then
      ff_status=0
    else
      ff_status=$?
    fi
  elif java -jar "$ff_jar" "${ff_args[@]}"; then
    ff_status=0
  else
    ff_status=$?
  fi

  # Fernflower outputs a JAR containing .java files — extract it
  local result_jar="$out_dir/$(basename "$jar_to_decompile")"
  if [[ -f "$result_jar" ]]; then
    local sources_dir="$out_dir/sources"
    mkdir -p "$sources_dir"
    if unzip -qo "$result_jar" -d "$sources_dir"; then
      rm -f "$result_jar"
    else
      echo "Warning: Fernflower result jar $result_jar could not be extracted; checking for direct folder output." >&2
    fi
  fi

  local sources_dir="$out_dir/sources"
  mkdir -p "$sources_dir"
  count=$(find "$sources_dir" -name "*.java" | wc -l)

  # Vineflower may write sources directly into the destination folder tree instead of a result jar.
  if [[ $count -eq 0 ]]; then
    local direct_count=0
    direct_count=$(find "$out_dir" \
      -path "$sources_dir" -prune -o \
      -path "$intermediate_dir" -prune -o \
      -name "*.java" -type f -print | wc -l)
    if [[ $direct_count -gt 0 ]]; then
      while IFS= read -r -d '' entry; do
        mv "$entry" "$sources_dir"/
      done < <(find "$out_dir" -mindepth 1 -maxdepth 1 \
        ! -name "sources" \
        ! -name "intermediate" \
        -print0)
      count=$(find "$sources_dir" -name "*.java" | wc -l)
    fi
  fi

  # Clean up intermediate dex2jar output
  if [[ $count -gt 0 ]]; then
    echo "Fernflower output: $sources_dir/"
    echo "Java files decompiled by Fernflower: $count"
    if [[ -n "${converted_jar:-}" ]] && [[ -f "${converted_jar:-}" ]]; then
      rm -f "$converted_jar"
    fi
    if [[ -d "$intermediate_dir" ]]; then
      rmdir "$intermediate_dir" 2>/dev/null || true
    fi
    if [[ $ff_status -ne 0 ]]; then
      echo "Warning: Fernflower/Vineflower exited with status $ff_status after writing $count Java files; treating this as partial success." >&2
      return 2
    fi
    return 0
  fi

  if [[ -n "${converted_jar:-}" ]] && [[ -f "${converted_jar:-}" ]]; then
    echo "Error: Fernflower/Vineflower produced no Java output. Intermediate dex2jar artifact kept at $converted_jar" >&2
  else
    echo "Error: Fernflower/Vineflower produced no Java output." >&2
  fi

  if [[ $ff_status -ne 0 ]]; then
    if [[ $ff_status -eq 124 ]]; then
      echo "Error: Fernflower/Vineflower exceeded timeout (${ff_timeout_seconds}s)." >&2
    fi
    echo "Error: Fernflower/Vineflower exited with status $ff_status." >&2
  fi
  return 1
}

# --- Summary helper ---
print_structure() {
  local src_dir="$1"
  local label="$2"
  if [[ -d "$src_dir" ]]; then
    local packages=()
    echo
    echo "Top-level packages ($label):"
    while IFS= read -r pkg; do
      [[ -n "$pkg" ]] && packages+=("$pkg")
    done < <(find "$src_dir" -mindepth 1 -maxdepth 3 -type d -printf '%P\n' | sort)

    local limit=${#packages[@]}
    if (( limit > 20 )); then
      limit=20
    fi

    if (( limit == 0 )); then
      echo "(none)"
      return
    fi

    local i=0
    while (( i < limit )); do
      echo "${packages[$i]}"
      ((i += 1))
    done
  fi
}

# --- Decompile a single file with the selected engine ---
decompile_single() {
  local file_abs="$1"
  local out_dir="$2"
  local label="$3"

  # Temporarily override INPUT_FILE_ABS for run_jadx/run_fernflower
  local saved_input="$INPUT_FILE_ABS"
  local saved_ext="$ext_lower"
  INPUT_FILE_ABS="$file_abs"
  ext_lower="${file_abs##*.}"
  ext_lower=$(echo "$ext_lower" | tr '[:upper:]' '[:lower:]')

  if [[ -n "$label" ]]; then
    echo "=== Decompiling $label (engine: $ENGINE) ==="
  fi

  case "$ENGINE" in
    jadx)
      local jadx_status=0
      if run_jadx "$out_dir"; then
        jadx_status=0
      else
        jadx_status=$?
      fi
      print_structure "$out_dir/sources" "jadx"
      if [[ $jadx_status -eq 1 ]]; then
        return 1
      fi
      if [[ $jadx_status -eq 2 ]]; then
        echo "jadx completed with warnings but produced usable output."
      fi
      ;;
    fernflower)
      local ff_status=0
      if run_fernflower "$out_dir"; then
        ff_status=0
      else
        ff_status=$?
      fi
      print_structure "$out_dir/sources" "fernflower"
      if [[ $ff_status -eq 1 ]]; then
        return 1
      fi
      if [[ $ff_status -eq 2 ]]; then
        echo "Fernflower completed with warnings but produced usable output."
      fi
      ;;
    both)
      local jadx_status=0
      local ff_status=0
      echo "--- Pass 1: jadx ---"
      if run_jadx "$out_dir/jadx"; then
        jadx_status=0
      else
        jadx_status=$?
      fi
      if [[ $jadx_status -eq 1 ]]; then
        return 1
      fi
      if [[ $jadx_status -eq 2 ]]; then
        echo "Continuing to Fernflower because jadx produced usable output despite warnings."
      fi
      echo
      echo "--- Pass 2: Fernflower ---"
      if run_fernflower "$out_dir/fernflower"; then
        ff_status=0
      else
        ff_status=$?
      fi
      if [[ $ff_status -eq 1 ]]; then
        return 1
      fi
      if [[ $ff_status -eq 2 ]]; then
        echo "Continuing with Fernflower output because it produced usable sources despite warnings."
      fi

      print_structure "$out_dir/jadx/sources" "jadx"
      print_structure "$out_dir/fernflower/sources" "fernflower"

      echo
      echo "=== Comparison ==="
      local jadx_count=0 ff_count=0
      if [[ -d "$out_dir/jadx/sources" ]]; then
        jadx_count=$(find "$out_dir/jadx/sources" -name "*.java" | wc -l)
      fi
      if [[ -d "$out_dir/fernflower/sources" ]]; then
        ff_count=$(find "$out_dir/fernflower/sources" -name "*.java" | wc -l)
      fi
      echo "jadx:        $jadx_count Java files"
      echo "Fernflower:  $ff_count Java files"

      if [[ -d "$out_dir/jadx/sources" ]]; then
        local jadx_error_files
        local jadx_errors
        jadx_error_files=$(grep -rl 'JADX WARNING\|JADX WARN\|JADX ERROR\|Code decompiled incorrectly' "$out_dir/jadx/sources" 2>/dev/null || true)
        if [[ -n "$jadx_error_files" ]]; then
          jadx_errors=$(printf '%s\n' "$jadx_error_files" | wc -l)
        else
          jadx_errors=0
        fi
        echo "jadx files with warnings/errors: $jadx_errors"
      fi
      echo
      echo "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output."
      ;;
  esac

  INPUT_FILE_ABS="$saved_input"
  ext_lower="$saved_ext"
}

# --- Run ---
echo "=== Decompiling $INPUT_FILE (engine: $ENGINE) ==="
echo "Output directory: $OUTPUT_DIR"
echo

if [[ "$ext_lower" == "xapk" ]]; then
  # Decompile each APK found inside the XAPK
  mkdir -p "$OUTPUT_DIR"

  # Copy XAPK manifest for reference
  if [[ -f "$XAPK_EXTRACTED_DIR/manifest.json" ]]; then
    cp "$XAPK_EXTRACTED_DIR/manifest.json" "$OUTPUT_DIR/xapk-manifest.json"
  fi

  # Copy OBB file list for reference
  obb_files=()
  while IFS= read -r -d '' obb; do
    obb_files+=("$obb")
  done < <(find "$XAPK_EXTRACTED_DIR" -name "*.obb" -print0 2>/dev/null)
  if [[ ${#obb_files[@]} -gt 0 ]]; then
    echo "OBB files found (not decompiled, data-only):"
    for obb in "${obb_files[@]}"; do
      echo "  - $(basename "$obb") ($(du -h "$obb" | cut -f1))"
    done
    echo
  fi

  for apk_file in "${XAPK_APK_FILES[@]}"; do
    apk_name=$(basename "$apk_file" .apk)
    echo
    echo "======================================================"
    decompile_single "$apk_file" "$OUTPUT_DIR/$apk_name" "$apk_name.apk"
  done

  # Cleanup extracted XAPK
  rm -rf "$XAPK_EXTRACTED_DIR"

  echo
  echo "=== XAPK decompilation complete ==="
  echo "Subdirectories in $OUTPUT_DIR/:"
  ls -1 "$OUTPUT_DIR/"
else
  decompile_single "$INPUT_FILE_ABS" "$OUTPUT_DIR" ""

  # --- Split/bundled APK detection ---
  # Some APKs are bundles: the outer APK contains base.apk + split_config.*.apk
  # inside the resources directory. jadx will decompile the thin outer wrapper
  # and produce very few Java files. Detect this and re-decompile base.apk.
  sources_dir="$OUTPUT_DIR/sources"
  resources_dir="$OUTPUT_DIR/resources"
  if [[ -d "$sources_dir" && -d "$resources_dir" ]]; then
    java_count=$(find "$sources_dir" -name "*.java" -type f 2>/dev/null | wc -l)
    base_apk=$(find "$resources_dir" -maxdepth 1 -name "base.apk" -type f 2>/dev/null | head -1)
    inner_apk_count=$(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f 2>/dev/null | wc -l)

    if [[ "$java_count" -le 10 && -n "$base_apk" ]]; then
      echo
      echo "=== Split/bundled APK detected ==="
      echo "Outer APK produced only $java_count Java file(s) but contains $inner_apk_count inner APK(s):"
      find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -exec basename {} \; | while read -r f; do echo "  - $f"; done
      echo
      echo "Decompiling base.apk (contains the actual app code)..."
      decompile_single "$base_apk" "$OUTPUT_DIR/base" "base.apk"

      # Decompile non-config split APKs
      while IFS= read -r -d '' split_apk; do
        split_name=$(basename "$split_apk" .apk)
        case "$split_name" in
          base|split_config.*) continue ;;
        esac
        echo
        echo "Decompiling $split_name.apk..."
        decompile_single "$split_apk" "$OUTPUT_DIR/$split_name" "$split_name.apk"
      done < <(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -print0 2>/dev/null)

      # Report skipped config splits
      config_splits=$(find "$resources_dir" -maxdepth 1 -name "split_config.*.apk" -type f 2>/dev/null)
      if [[ -n "$config_splits" ]]; then
        echo
        echo "Skipped config splits (resource/ABI only):"
        echo "$config_splits" | while read -r f; do echo "  - $(basename "$f")"; done
      fi

      echo
      echo "Main decompiled source is in: $OUTPUT_DIR/base/sources/"
    fi
  fi

  echo
  echo "=== Decompilation complete ==="
fi
```

## scripts/find-api-calls.ps1

```

```

## scripts/find-api-calls.sh

```bash
#!/usr/bin/env bash
# find-api-calls.sh — Search decompiled source for API calls and HTTP endpoints
set -euo pipefail

usage() {
  cat <<EOF
Usage: find-api-calls.sh <source-dir> [OPTIONS]

Search decompiled Java/Kotlin source for HTTP API calls and endpoints.

Arguments:
  <source-dir>    Path to the decompiled sources directory

Options:
  --retrofit      Search only for Retrofit annotations
  --okhttp        Search only for OkHttp patterns
  --ktor          Search only for Ktor client patterns
  --apollo        Search only for Apollo (GraphQL) patterns
  --volley        Search only for Volley patterns
  --urls          Search only for hardcoded URLs
  --paths         Extract unique endpoint-shaped path string literals
                  (works on heavily obfuscated apps where call sites are inlined)
  --auth          Search only for auth-related patterns
  --all           Search all patterns (default)
  -h, --help      Show this help message

Output:
  Results are printed as file:line:match for easy navigation.
EOF
  exit 0
}

SOURCE_DIR=""
SEARCH_RETROFIT=false
SEARCH_OKHTTP=false
SEARCH_KTOR=false
SEARCH_APOLLO=false
SEARCH_VOLLEY=false
SEARCH_URLS=false
SEARCH_PATHS=false
SEARCH_AUTH=false
SEARCH_ALL=true

while [[ $# -gt 0 ]]; do
  case "$1" in
    --retrofit) SEARCH_RETROFIT=true; SEARCH_ALL=false; shift ;;
    --okhttp)   SEARCH_OKHTTP=true;   SEARCH_ALL=false; shift ;;
    --ktor)     SEARCH_KTOR=true;     SEARCH_ALL=false; shift ;;
    --apollo)   SEARCH_APOLLO=true;   SEARCH_ALL=false; shift ;;
    --volley)   SEARCH_VOLLEY=true;    SEARCH_ALL=false; shift ;;
    --urls)     SEARCH_URLS=true;      SEARCH_ALL=false; shift ;;
    --paths)    SEARCH_PATHS=true;     SEARCH_ALL=false; shift ;;
    --auth)     SEARCH_AUTH=true;      SEARCH_ALL=false; shift ;;
    --all)      SEARCH_ALL=true; shift ;;
    -h|--help)  usage ;;
    -*)         echo "Error: Unknown option $1" >&2; usage ;;
    *)          SOURCE_DIR="$1"; shift ;;
  esac
done

if [[ -z "$SOURCE_DIR" ]]; then
  echo "Error: No source directory specified." >&2
  usage
fi

if [[ ! -d "$SOURCE_DIR" ]]; then
  echo "Error: Directory not found: $SOURCE_DIR" >&2
  exit 1
fi

GREP_OPTS="-rn --include=*.java --include=*.kt"

section() {
  echo
  echo "==== $1 ===="
  echo
}

run_grep() {
  local pattern="$1"
  # shellcheck disable=SC2086
  grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
}

# Print a one-screen summary FIRST so a reader knows what to expect from
# the long output that follows. Skipped when a single section flag was
# requested (the user wants raw matches, not an overview). One pass over
# the tree, counts bucketed by tag — running 8 separate greps was too slow.
if [[ "$SEARCH_ALL" == true ]]; then
  section "Summary (counted in a single pass)"
  declare -A H=(
    [retrofit]=0 [okhttp]=0 [ktor]=0 [apollo]=0 [volley]=0
    [hilt]=0 [koin]=0 [bearer]=0 [hmac]=0
  )
  while IFS= read -r line; do
    case "$line" in
      *"@GET("*|*"@POST("*|*"@PUT("*|*"@DELETE("*|*"@PATCH("*|*"@HTTP("*) H[retrofit]=$((H[retrofit]+1));;
    esac
    case "$line" in
      *"Request.Builder"*|*"HttpUrl"*|*".newCall("*) H[okhttp]=$((H[okhttp]+1));;
    esac
    case "$line" in
      *"BearerTokens"*|*"defaultRequest {"*|*"client.get("*|*"client.post("*|*"httpClient.get("*|*"httpClient.post("*|*"HttpClient.get("*) H[ktor]=$((H[ktor]+1));;
    esac
    case "$line" in
      *"ApolloClient"*|*".serverUrl("*) H[apollo]=$((H[apollo]+1));;
    esac
    case "$line" in
      *"StringRequest"*|*"JsonObjectRequest"*|*"RequestQueue"*) H[volley]=$((H[volley]+1));;
    esac
    case "$line" in
      *"@HiltAndroidApp"*|*"@AndroidEntryPoint"*|*"@HiltViewModel"*|*"@Provides"*|*"@Binds"*) H[hilt]=$((H[hilt]+1));;
    esac
    case "$line" in
      *"org.koin."*|*"module {"*|*"single<"*|*"factory<"*|*"singleOf("*|*"factoryOf("*) H[koin]=$((H[koin]+1));;
    esac
    case "$line" in
      *'"Bearer '*|*'"bearer '*|*"BearerTokens"*) H[bearer]=$((H[bearer]+1));;
    esac
    case "$line" in
      *"HmacSHA"*|*'Mac.getInstance("Hmac'*) H[hmac]=$((H[hmac]+1));;
    esac
  done < <(grep -rEh --include='*.java' --include='*.kt' \
      '@(GET|POST|PUT|DELETE|PATCH|HTTP)\(|Request\.Builder|HttpUrl|\.newCall\(|BearerTokens|defaultRequest \{|client\.(get|post)\(|httpClient\.(get|post)\(|ApolloClient|\.serverUrl\(|StringRequest|JsonObjectRequest|RequestQueue|@HiltAndroidApp|@AndroidEntryPoint|@HiltViewModel|@Provides|@Binds|org\.koin\.|module \{|single<|factory<|"[Bb]earer |HmacSHA|Mac\.getInstance' \
      "$SOURCE_DIR" 2>/dev/null || true)
  printf '  HTTP framework:   Retrofit=%-5s OkHttp=%-5s Ktor=%-5s Apollo=%-5s Volley=%-5s\n' \
      "${H[retrofit]}" "${H[okhttp]}" "${H[ktor]}" "${H[apollo]}" "${H[volley]}"
  printf '  DI framework:     Hilt/Dagger=%-5s Koin=%-5s\n' \
      "${H[hilt]}" "${H[koin]}"
  printf '  Auth signals:     Bearer=%-5s HMAC/Sign=%-5s\n' \
      "${H[bearer]}" "${H[hmac]}"
  echo
  echo "  Run with one of --retrofit / --okhttp / --ktor / --apollo / --volley /"
  echo "  --paths / --urls / --auth to inspect a single section."
fi

# --- Retrofit ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_RETROFIT" == true ]]; then
  section "Retrofit Annotations"
  run_grep '@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|HTTP)\s*\('
  section "Retrofit Headers & Parameters"
  run_grep '@(Headers|Header|Query|QueryMap|Path|Body|Field|FieldMap|Part|PartMap|Url)\s*\('
  section "Retrofit Base URL"
  run_grep '(baseUrl|base_url)\s*\('
fi

# --- OkHttp ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_OKHTTP" == true ]]; then
  section "OkHttp Request Building"
  run_grep '(Request\.Builder|HttpUrl|\.newCall|\.enqueue|addInterceptor|addNetworkInterceptor)'
  section "OkHttp URL Construction"
  run_grep '(\.url\s*\(|\.addQueryParameter|\.addPathSegment|\.scheme\s*\(|\.host\s*\()'
fi

# --- Ktor (Kotlin) ---
# Ktor doesn't use annotations. Endpoints appear as string args to
# client.get/post/etc., or are built via HttpRequestBuilder.url(...). Auth
# is configured via the bearer { loadTokens / refreshTokens } DSL.
if [[ "$SEARCH_ALL" == true || "$SEARCH_KTOR" == true ]]; then
  section "Ktor — Client Calls"
  run_grep '\b(client|httpClient|HttpClient)\.(get|post|put|delete|patch|head|request)\s*[<(]'
  section "Ktor — Request Building / Default Request"
  run_grep '(HttpRequestBuilder|defaultRequest\s*\{|\burl\s*\(\s*"|URLBuilder|URLProtocol)'
  section "Ktor — Auth Plugin (Bearer / Refresh)"
  run_grep '(\bbearer\s*\{|BearerTokens\s*\(|loadTokens\s*\{|refreshTokens\s*\{|\bAuth\s*\)\s*\{)'
fi

# --- Apollo (GraphQL) ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_APOLLO" == true ]]; then
  section "Apollo — GraphQL Client"
  run_grep '(ApolloClient|\.serverUrl\s*\(|\.subscriptionNetworkTransport|HttpNetworkTransport)'
  section "Apollo — Operations"
  run_grep '(\.query\s*\(\s*[A-Z]|\.mutation\s*\(\s*[A-Z]|\.subscription\s*\(\s*[A-Z])'
fi

# --- Volley ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_VOLLEY" == true ]]; then
  section "Volley Requests"
  run_grep '(StringRequest|JsonObjectRequest|JsonArrayRequest|ImageRequest|RequestQueue|Volley\.newRequestQueue)'
fi

# --- Endpoint-shaped path literals ---
# Survives R8 obfuscation: even when call sites are inlined to a.b(c, "path"),
# the path strings themselves are not obfuscated. This produces a deduplicated
# inventory of likely API endpoints that other modes miss.
if [[ "$SEARCH_ALL" == true || "$SEARCH_PATHS" == true ]]; then
  section "Endpoint-Shaped Path Literals (deduplicated)"
  # Quoted strings that begin with /<segment> or <segment>/ where the leading
  # segment is a typical API root word. Cap segment count and length to keep
  # the regex grounded.
  # An endpoint-shaped string is one of:
  #   "/seg/seg..."                   — absolute path with >= 2 segments
  #   "api-root/seg/seg..."           — relative path starting with a known
  #                                     API root keyword and containing >= 1
  #                                     '/' followed by another segment
  # Segments are URL-safe chars plus {} for path-template placeholders.
  SEG='[A-Za-z0-9_{}.\-]+'
  ROOT='(api|v[0-9]+|graphql|rest|mobile|auth|oauth|sso|users?|account|session|token|register|signup|signin|logout|password|verify|otp|sms|profile|customer|cart|basket|order|checkout|payment|invoice|product|catalog|inventory|search|category|favo[u]?rites?|wishlist|address|location|delivery|shipping|review|feedback|notification|push|message|chat|track|event|stat[a-z]*|metric|config|settings?|feature|flag|banner|content|media|upload|download|file|image|video|live|stream|webhook|callback)'
  PATHS_REGEX="\"(/${SEG}(/${SEG})+/?|${ROOT}(/${SEG})+/?)\""
  # Filter out frequent false positives (MIME types, /proc, /sys, /dev).
  EXCLUDE='^"(image|video|audio|text|application|content|font|model|multipart|message)/|^"/(proc|sys|dev|tmp|etc|usr|var|opt)/'
  # Print a flat unique list rather than file:line — this is the inventory.
  grep -rhoE --include='*.java' --include='*.kt' "$PATHS_REGEX" "$SOURCE_DIR" 2>/dev/null \
      | grep -Ev "$EXCLUDE" \
      | sort -u || true
  echo
  section "Endpoint-Shaped Path Literals — call sites"
  grep $GREP_OPTS -E "$PATHS_REGEX" "$SOURCE_DIR" 2>/dev/null \
      | grep -Ev ":[0-9]+:.*${EXCLUDE#^}" || true
fi

# --- Hardcoded URLs ---
# A loose grep for http(s)://... drowns in compression-dictionary garbage and
# in third-party SDK URLs (Google, Firebase, AppsFlyer, Datadog, ...). The
# strict regex requires a syntactically valid hostname and rejects strings
# containing whitespace, angle brackets, or non-printable bytes. Hosts are
# then bucketed into "first-party candidates" vs "third-party (denylist)".
if [[ "$SEARCH_ALL" == true || "$SEARCH_URLS" == true ]]; then
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  DENYLIST="$HERE/../references/third_party_hosts.txt"
  # Accept three host shapes, all rejecting whitespace / angle brackets /
  # non-printables in the path:
  #   * IPv4 literal (dev/staging endpoints, high signal)            192.168.0.1
  #   * dotted host: >=2 labels ending in a 2+ letter TLD (incl apex) example.com
  #   * bare single-label host, BUT only when followed by ':port' or  localhost:3000
  #     '/path' — keeps internal hosts (localhost, internal-backend)  svc/health
  #     while still dropping Kotlin-stdlib dictionary fragments like
  #     "http://An Introduction..." (bare word, no port/path follows).
  STRICT_URL='https?://(([0-9]{1,3}(\.[0-9]{1,3}){3}|[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})(:[0-9]{1,5})?(/[^"<>[:space:]]*)?|[A-Za-z0-9-]+(:[0-9]{1,5}(/[^"<>[:space:]]*)?|/[^"<>[:space:]]*))'

  TMP="$(mktemp)"
  trap 'rm -f "$TMP"' EXIT
  # Extraction (STRICT_URL) is deliberately permissive; this awk pass drops the
  # residual Kotlin-stdlib dictionary noise WITHOUT losing the high-signal
  # shapes a strict-only regex discards (IPs, apex domains, internal hosts).
  # Decision table, top-down, on the host (authority before any :port / /path):
  #   * IPv4 literal                    -> keep  (dict fragments are words,
  #                                              never dotted-quads)
  #   * >=3 labels (sub.domain.tld)     -> keep  (any TLD; same tolerance the
  #                                              original strict regex had)
  #   * any host WITH a :port or /path  -> keep  (structured = high signal:
  #                                              localhost:3000, svc/health)
  #   * bare 2-label apex, no port/path -> keep ONLY if the TLD is a real one,
  #                                              compared as a whole field (kills
  #                                              "www.this" / "this.introduction",
  #                                              keeps "mytrackera-api.com")
  # Trade-off: a first-party host referenced bare with an uncommon TLD (e.g.
  # https://foo.store with no path) is dropped — give it a path/port, or add the
  # TLD to the list below, if you hit that case.
  { grep -rhoE --include='*.java' --include='*.kt' "$STRICT_URL" "$SOURCE_DIR" 2>/dev/null || true; } \
      | sort -u \
      | awk '
          { rest=$0; sub(/^https?:\/\//,"",rest)
            host=rest; sub(/[/:].*/,"",host)
            haspathport = (rest ~ /[/:]/)
            if (host ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { print; next }   # IPv4
            n = split(host, a, ".")
            if (n >= 3)      { print; next }                                 # sub.domain.tld
            if (haspathport) { print; next }                                 # has :port or /path
            if (n == 2 && a[2] ~ /^(com|net|org|io|co|app|dev|me|ai|xyz|info|biz|gov|edu|mil|int|tech|cloud|uk|de|fr|it|es|nl|in|us|ca|au|jp|cn|br|ru|eu|ch|se|no|fi|dk|pl|pt|gr|ie|be|at|cz|sg|hk|kr|tw|mx|ar|cl|za|nz)$/) print  # real apex TLD
          }' > "$TMP"

  # Extract host: strip scheme, take part up to first ':' or '/'.
  HOSTS_TMP="$(mktemp)"
  sed -E 's#^https?://##; s#[/:].*$##' "$TMP" | sort -u > "$HOSTS_TMP"

  if [[ -f "$DENYLIST" ]]; then
    # Build a single combined regex from the denylist (one line each).
    DENY_REGEX="$(grep -vE '^\s*(#|$)' "$DENYLIST" | tr '\n' '|' | sed 's/|$//')"
    THIRD_HOSTS=$(grep -E "$DENY_REGEX" "$HOSTS_TMP" || true)
    FIRST_HOSTS=$(grep -vE "$DENY_REGEX" "$HOSTS_TMP" || true)
  else
    THIRD_HOSTS=""
    FIRST_HOSTS=$(cat "$HOSTS_TMP")
  fi

  section "Likely First-Party Hosts (frequency-sorted)"
  if [[ -n "$FIRST_HOSTS" ]]; then
    while IFS= read -r h; do
      [[ -z "$h" ]] && continue
      n=$(grep -cE "://${h//./\\.}([/:\"]|$)" "$TMP" || true)
      printf '  %5d  %s\n' "$n" "$h"
    done <<< "$FIRST_HOSTS" | sort -rn -k1
  else
    echo "  (none — every URL matched the third-party denylist)"
  fi

  section "Third-Party Hosts (denylist matches, collapsed)"
  if [[ -n "$THIRD_HOSTS" ]]; then
    echo "$THIRD_HOSTS" | sed 's/^/  /'
  else
    echo "  (none)"
  fi

  section "All First-Party URLs (full strings)"
  if [[ -n "$FIRST_HOSTS" ]]; then
    while IFS= read -r h; do
      [[ -z "$h" ]] && continue
      grep -E "://${h//./\\.}([/:\"]|$)" "$TMP" | sed 's/^/  /'
    done <<< "$FIRST_HOSTS"
  fi

  rm -f "$HOSTS_TMP" "$TMP"
  trap - EXIT

  section "HttpURLConnection"
  run_grep '(openConnection|setRequestMethod|HttpURLConnection|HttpsURLConnection)'
  section "WebView URLs"
  run_grep '(loadUrl|loadData|evaluateJavascript|addJavascriptInterface|WebViewClient|WebChromeClient)'
fi

# --- Auth patterns ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_AUTH" == true ]]; then
  section "Authentication & API Keys"
  run_grep -i '(api[_-]?key|auth[_-]?token|bearer|authorization|x-api-key|client[_-]?secret|access[_-]?token|refresh[_-]?token)'

  # Request-signing schemes: a hardcoded HMAC / RSA secret in an APK is a
  # security finding worth surfacing prominently. These patterns catch the
  # common shapes of homegrown / SDK-issued request signers.
  section "Request Signing (HMAC / signature schemes)"
  run_grep '(HmacSHA(1|256|512)|Mac\.getInstance\("Hmac|SecretKeySpec\(|Signature\.getInstance\()'
  run_grep -i '(x-signature|x-client-authorization|x-amz-signature|x-hmac|aws4-hmac|signRequest|signatureFor|computeSignature|signaturev[0-9])'

  # Hardcoded high-entropy strings adjacent to "secret"/"key" assignments
  # are the canonical leaked-credential pattern.
  section "Possible Hardcoded Secrets / Keys"
  run_grep -i '(app[_-]?secret|client[_-]?secret|signing[_-]?key|hmac[_-]?secret|consumer[_-]?secret|private[_-]?key)'

  section "Base URLs and Constants"
  run_grep -i '(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)'

  # Ktor BearerTokens / refresh DSL — common on Kotlin apps and lives on
  # Ktor's public API, so it survives R8 unchanged.
  section "Ktor Auth (Bearer + Refresh)"
  run_grep '(BearerTokens|loadTokens\s*\{|refreshTokens\s*\{|\bbearer\s*\{)'
fi

echo
echo "=== Search complete ==="
```

## scripts/fingerprint.sh

```bash
#!/usr/bin/env bash
# fingerprint.sh — Triage an APK/XAPK before decompiling.
#
# Detects mobile framework (Flutter, React Native, Cordova/Capacitor,
# Xamarin, KMP/native), HTTP-stack hints, obfuscation level, native libs,
# and notable third-party SDKs.
#
# Decompiling Java is mostly useless for Flutter / RN / Xamarin / Cordova
# apps — different tools are needed. Run this BEFORE Phase 2 to choose
# the right path.

set -euo pipefail

usage() {
  cat <<EOF
Usage: fingerprint.sh <file.apk|file.xapk>

Prints a one-screen summary:
  * mobile framework (with rationale)
  * HTTP / DI / serialization stack hints
  * obfuscation indicator
  * native libraries (consolidated across split APKs)
  * notable third-party SDKs found in assets/
EOF
  exit 0
}

[[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]] && usage
INPUT="$1"
[[ ! -f "$INPUT" ]] && { echo "File not found: $INPUT" >&2; exit 1; }

TMP="$(mktemp -d -t apkfp.XXXXXX)"
trap 'rm -rf "$TMP"' EXIT

# Resolve to a list of APKs (handle XAPK = ZIP of APKs)
APKS=()
case "${INPUT,,}" in
  *.xapk|*.apks|*.apkm)
    unzip -q -o "$INPUT" -d "$TMP/xapk"
    while IFS= read -r p; do APKS+=("$p"); done < <(find "$TMP/xapk" -maxdepth 2 -type f -name '*.apk')
    ;;
  *.apk)
    APKS=("$INPUT")
    ;;
  *)
    echo "Unsupported input: $INPUT" >&2; exit 1 ;;
esac

# Aggregate ZIP listings from every APK in the bundle (split-aware view)
LISTING="$TMP/listing.txt"
: > "$LISTING"
for apk in "${APKS[@]}"; do
  unzip -l -- "$apk" 2>/dev/null | awk '{print $NF}' >> "$LISTING"
done

# Most class-level libs live inside classes*.dex, not as visible zip paths.
# Extract the type-name strings out of each dex with `strings` and append them
# to the listing so `has()` can match e.g. 'io/ktor/' or 'org/koin/'.
DEX_STRINGS="$TMP/dex_strings.txt"
: > "$DEX_STRINGS"
for apk in "${APKS[@]}"; do
  for dex in $(unzip -Z1 -- "$apk" 2>/dev/null | grep -E '^classes[0-9]*\.dex$' || true); do
    # DEX type descriptors look like "Lcom/foo/Bar;". Extract the inner
    # slash-separated FQN so callers can match e.g. 'io/ktor/' directly.
    unzip -p -- "$apk" "$dex" 2>/dev/null \
      | strings -n 8 \
      | grep -oE 'L[a-z][a-zA-Z0-9_]*(/[a-zA-Z0-9_$]+)+;' \
      | sed -E 's/^L//; s/;$//' \
      >> "$DEX_STRINGS" || true
  done
done
sort -u "$DEX_STRINGS" -o "$DEX_STRINGS"

has() { grep -qE "$1" "$LISTING" || grep -qE "$1" "$DEX_STRINGS"; }

# ----------------------------------------------------------------------
# Framework detection (priority order — first match wins)
# ----------------------------------------------------------------------
FRAMEWORK="unknown"
RATIONALE=""

if has '^lib/[^/]+/libflutter\.so$'; then
  FRAMEWORK="Flutter"
  RATIONALE="lib/<abi>/libflutter.so present"
  has '^lib/[^/]+/libapp\.so$' && RATIONALE+="; libapp.so contains AOT-compiled Dart"
elif has '^lib/[^/]+/libhermes\.so$' || has '^assets/index\.android\.bundle$' || has '^lib/[^/]+/libreactnativejni\.so$'; then
  FRAMEWORK="React Native"
  reasons=()
  has '^lib/[^/]+/libhermes\.so$'             && reasons+=("libhermes.so")
  has '^lib/[^/]+/libreactnativejni\.so$'     && reasons+=("libreactnativejni.so")
  has '^assets/index\.android\.bundle$'       && reasons+=("assets/index.android.bundle")
  RATIONALE="${reasons[*]}"
elif has '^assets/www/index\.html$' || has '^assets/www/cordova\.js$' || has '^assets/public/index\.html$'; then
  FRAMEWORK="Cordova / Capacitor (WebView hybrid)"
  RATIONALE="assets/www/ or assets/public/ shell present"
elif has '^lib/[^/]+/libmonodroid\.so$' || has '^assemblies/'; then
  FRAMEWORK="Xamarin / .NET MAUI"
  RATIONALE="libmonodroid.so or assemblies/ present — code is in .NET DLLs"
elif has '^lib/[^/]+/libmaui\.so$'; then
  FRAMEWORK=".NET MAUI"
  RATIONALE="libmaui.so present"
elif has '^assets/flutter_assets/' && ! has '^lib/[^/]+/libflutter\.so$'; then
  FRAMEWORK="Flutter (code-only split?)"
  RATIONALE="flutter_assets/ but no libflutter.so in this APK — check splits"
else
  # Native: distinguish Compose vs classic Android by androidx.compose presence
  if has 'androidx\.compose'; then
    FRAMEWORK="Native Android (Kotlin + Jetpack Compose)"
    RATIONALE="androidx.compose.* libraries detected"
  elif has '^META-INF/.*\.kotlin_module$'; then
    FRAMEWORK="Native Android (Kotlin)"
    RATIONALE="kotlin_module metadata present, no Compose markers"
  else
    FRAMEWORK="Native Android (Java/Kotlin)"
    RATIONALE="no cross-platform framework markers found"
  fi
fi

# ----------------------------------------------------------------------
# HTTP / DI / serialization stack hints
# ----------------------------------------------------------------------
http=()
has 'retrofit2'                && http+=("Retrofit")
has 'okhttp3'                  && http+=("OkHttp")
has 'io/ktor/'                 && http+=("Ktor")
has 'com/apollographql/'       && http+=("Apollo (GraphQL)")
has 'com/android/volley'       && http+=("Volley")

di=()
has 'dagger/hilt/'              && di+=("Hilt")
has '^META-INF/.*dagger.*'      && di+=("Dagger")
has 'org/koin/'                 && di+=("Koin")
has 'javax/inject/'             && [[ ${#di[@]} -eq 0 ]] && di+=("javax.inject")

ser=()
has 'kotlinx/serialization/'    && ser+=("kotlinx.serialization")
has 'com/google/gson/'          && ser+=("Gson")
has 'com/squareup/moshi/'       && ser+=("Moshi")
has 'com/fasterxml/jackson/'    && ser+=("Jackson")

# ----------------------------------------------------------------------
# Obfuscation indicator (R8/ProGuard) — count single-letter dex packages
# ----------------------------------------------------------------------
# Note: pipefail is on, so guard greps that may legitimately return 0 matches.
short_dirs=$( { grep -oE '^[a-z]{1,2}/' "$LISTING" || true; } | sort -u | wc -l | tr -d ' ')
if [[ "$short_dirs" -gt 30 ]]; then
  OBFUSCATION="HIGH ($short_dirs single/double-letter dirs at root)"
elif [[ "$short_dirs" -gt 10 ]]; then
  OBFUSCATION="MODERATE ($short_dirs short root dirs)"
else
  OBFUSCATION="LOW (no significant short-name namespace pollution)"
fi

# ----------------------------------------------------------------------
# Native libraries (consolidated)
# ----------------------------------------------------------------------
NATIVE=$(grep -E '^lib/[^/]+/[^/]+\.so$' "$LISTING" | sort -u || true)

# ----------------------------------------------------------------------
# Notable third-party SDKs (assets-based markers)
# ----------------------------------------------------------------------
sdks=()
has '^assets/com/appsflyer/'        && sdks+=("AppsFlyer")
has 'datadog\.buildId|com/datadog/' && sdks+=("Datadog")
has 'io/sentry/'                    && sdks+=("Sentry")
has 'com/google/firebase/'          && sdks+=("Firebase")
has 'com/google/android/gms/'       && sdks+=("Google Play Services")
has 'com/facebook/'                 && sdks+=("Facebook SDK")
has 'com/payu/'                     && sdks+=("PayU")
has 'com/stripe/'                   && sdks+=("Stripe")
has 'com/braintreepayments/'        && sdks+=("Braintree")
has 'com/storyteller/'              && sdks+=("Storyteller")
has 'zendesk/'                      && sdks+=("Zendesk")
has 'com/intercom/'                 && sdks+=("Intercom")
has 'com/segment/analytics'         && sdks+=("Segment")
has 'com/amplitude/'                && sdks+=("Amplitude")
has 'com/mixpanel/'                 && sdks+=("Mixpanel")
has 'com/onesignal/'                && sdks+=("OneSignal")
has 'com/microsoft/clarity'         && sdks+=("Microsoft Clarity")
has 'com/hotjar/'                   && sdks+=("Hotjar")
has 'com/instabug/'                 && sdks+=("Instabug")

# BuildConfig.java is almost never obfuscated and often holds base URLs / flavor.
if has 'BuildConfig\.class$'; then
  BUILDCONFIG="present (grep BuildConfig.java after decompile for base URLs / flavor)"
else
  BUILDCONFIG="not detected in zip listing (still worth grepping after decompile)"
fi

# ----------------------------------------------------------------------
# Summary
# ----------------------------------------------------------------------
echo "=== APK Fingerprint: $(basename "$INPUT") ==="
echo
echo "Framework:        $FRAMEWORK"
echo "  Rationale:      $RATIONALE"
echo "Obfuscation:      $OBFUSCATION"
echo
echo "HTTP stack:       ${http[*]:-none detected}"
echo "DI:               ${di[*]:-none detected}"
echo "Serialization:    ${ser[*]:-none detected}"
echo "BuildConfig:      $BUILDCONFIG"
echo
echo "Third-party SDKs: ${sdks[*]:-none detected}"
echo
echo "Native libraries (consolidated across splits):"
if [[ -n "$NATIVE" ]]; then
  echo "$NATIVE" | sed 's/^/  /'
else
  echo "  (none)"
fi
echo

# ----------------------------------------------------------------------
# Recommendation
# ----------------------------------------------------------------------
echo "Recommended next step:"
case "$FRAMEWORK" in
  Flutter*)
    echo "  Java decompilation will yield ~no app code. The Dart logic lives in"
    echo "  libapp.so (AOT). Use tools designed for Flutter:"
    echo "    - reFlutter / Doldrums / blutter (extract Dart class structure)"
    echo "    - strings/rabin2 on libapp.so for endpoints & string constants"
    ;;
  React*)
    echo "  Java code is just the RN host. Real app logic is in JS/Hermes:"
    echo "    - if Hermes: hbctool disasm assets/index.android.bundle"
    echo "    - if JSC:    js-beautify the bundle and grep for 'fetch('/'axios'"
    ;;
  Cordova*)
    echo "  All app code is in assets/www/ (or assets/public/). Just unzip and"
    echo "  inspect the HTML/JS — no Java decompile needed."
    ;;
  Xamarin*|.NET*)
    echo "  App logic is in .NET DLLs (assemblies/). Use ILSpy or dotPeek;"
    echo "  jadx will only show the Mono host."
    ;;
  *)
    echo "  Proceed with Phase 2: bash scripts/decompile.sh <file>"
    ;;
esac
```

## scripts/install-dep.ps1

```

```

## scripts/install-dep.sh

```bash
#!/usr/bin/env bash
# install-dep.sh — Install a single dependency for Android reverse engineering
# Usage: install-dep.sh <dependency>
# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb
#
# Exit codes:
#   0 — installed successfully
#   1 — installation failed
#   2 — requires manual action (e.g. sudo needed but not available)
set -euo pipefail

usage() {
  cat <<EOF
Usage: install-dep.sh <dependency>

Install a dependency required for Android reverse engineering.

Available dependencies:
  java         Java JDK 17+
  jadx         jadx decompiler
  vineflower   Vineflower (Fernflower fork) decompiler
  dex2jar      DEX to JAR converter
  apktool      Android resource decoder
  adb          Android Debug Bridge

The script detects your OS and package manager, then:
  - Installs directly if possible (brew, or user-local install)
  - Uses sudo if available and needed
  - Prints manual instructions if neither option works
EOF
  exit 0
}

if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
  usage
fi

DEP="$1"

# --- Detect environment ---
OS="unknown"
PKG_MANAGER="none"
HAS_SUDO=false
ARCH=$(uname -m)

case "$(uname -s)" in
  Linux)  OS="linux" ;;
  Darwin) OS="macos" ;;
esac

# Detect package manager
if command -v brew &>/dev/null; then
  PKG_MANAGER="brew"
elif command -v apt-get &>/dev/null; then
  PKG_MANAGER="apt"
elif command -v dnf &>/dev/null; then
  PKG_MANAGER="dnf"
elif command -v pacman &>/dev/null; then
  PKG_MANAGER="pacman"
fi

# Check sudo availability
if command -v sudo &>/dev/null; then
  if sudo -n true 2>/dev/null; then
    HAS_SUDO=true
  else
    # sudo exists but may need password — we'll try it and let it prompt
    HAS_SUDO=true
  fi
fi

info()  { echo "[INFO] $*"; }
ok()    { echo "[OK] $*"; }
fail()  { echo "[FAIL] $*" >&2; }
manual() {
  echo "[MANUAL] $*" >&2
  echo "         Cannot install automatically. Please install manually and retry." >&2
  exit 2
}

# --- Helper: install via system package manager (needs sudo on Linux) ---
pkg_install() {
  local pkg="$1"
  case "$PKG_MANAGER" in
    brew)
      info "Installing $pkg via Homebrew..."
      brew install "$pkg"
      ;;
    apt)
      if [[ "$HAS_SUDO" == true ]]; then
        info "Installing $pkg via apt..."
        sudo apt-get update -qq && sudo apt-get install -y -qq "$pkg"
      else
        manual "Run: sudo apt-get install $pkg"
      fi
      ;;
    dnf)
      if [[ "$HAS_SUDO" == true ]]; then
        info "Installing $pkg via dnf..."
        sudo dnf install -y "$pkg"
      else
        manual "Run: sudo dnf install $pkg"
      fi
      ;;
    pacman)
      if [[ "$HAS_SUDO" == true ]]; then
        info "Installing $pkg via pacman..."
        sudo pacman -S --noconfirm "$pkg"
      else
        manual "Run: sudo pacman -S $pkg"
      fi
      ;;
    *)
      manual "No supported package manager found. Install $pkg manually."
      ;;
  esac
}

# --- Helper: download a file ---
download() {
  local url="$1" dest="$2"
  if command -v curl &>/dev/null; then
    curl -fsSL -o "$dest" "$url"
  elif command -v wget &>/dev/null; then
    wget -q -O "$dest" "$url"
  else
    fail "Neither curl nor wget available."
    return 1
  fi
}

# --- Helper: get latest GitHub release tag ---
gh_latest_tag() {
  local repo="$1"
  local url="https://api.github.com/repos/$repo/releases/latest"
  if command -v curl &>/dev/null; then
    curl -fsSL "$url" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/'
  elif command -v wget &>/dev/null; then
    wget -q -O - "$url" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/'
  fi
}

# --- Helper: add a line to shell profile if not already present ---
add_to_profile() {
  local line="$1"
  local profile=""
  if [[ -f "$HOME/.zshrc" ]]; then
    profile="$HOME/.zshrc"
  elif [[ -f "$HOME/.bashrc" ]]; then
    profile="$HOME/.bashrc"
  elif [[ -f "$HOME/.profile" ]]; then
    profile="$HOME/.profile"
  fi

  if [[ -n "$profile" ]]; then
    if ! grep -qF "$line" "$profile" 2>/dev/null; then
      echo "$line" >> "$profile"
      info "Added to $profile: $line"
      info "Run 'source $profile' or start a new shell to apply."
    fi
  else
    info "Add this to your shell profile: $line"
  fi
}

# =====================================================================
# Dependency installers
# =====================================================================

install_java() {
  if command -v java &>/dev/null; then
    local ver
    ver=$(java -version 2>&1 | head -1 | sed -n 's/.*"\([0-9]*\)\..*/\1/p')
    if [[ -n "$ver" ]] && (( ver >= 17 )); then
      ok "Java $ver already installed"
      return 0
    fi
  fi

  info "Installing Java JDK 17+..."
  case "$PKG_MANAGER" in
    brew)    brew install openjdk@17 ;;
    apt)     pkg_install "openjdk-17-jdk" ;;
    dnf)     pkg_install "java-17-openjdk-devel" ;;
    pacman)  pkg_install "jdk17-openjdk" ;;
    *)       manual "Install Java JDK 17+ from https://adoptium.net/" ;;
  esac

  # Verify
  if command -v java &>/dev/null; then
    ok "Java installed: $(java -version 2>&1 | head -1)"
  else
    fail "Java installation may require PATH update."
    if [[ "$PKG_MANAGER" == "brew" ]]; then
      add_to_profile 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"'
    fi
    exit 1
  fi
}

install_jadx() {
  if command -v jadx &>/dev/null; then
    ok "jadx already installed: $(jadx --version 2>/dev/null || echo 'unknown')"
    return 0
  fi

  # Try brew first (cleanest)
  if [[ "$PKG_MANAGER" == "brew" ]]; then
    info "Installing jadx via Homebrew..."
    brew install jadx
    ok "jadx installed via Homebrew"
    return 0
  fi

  # User-local install from GitHub releases (no sudo needed)
  info "Installing jadx from GitHub releases..."
  local tag
  tag=$(gh_latest_tag "skylot/jadx")
  if [[ -z "$tag" ]]; then
    fail "Could not determine latest jadx version."
    manual "Download from https://github.com/skylot/jadx/releases/latest"
  fi

  local version="${tag#v}"
  local url="https://github.com/skylot/jadx/releases/download/${tag}/jadx-${version}.zip"
  local tmp_zip
  tmp_zip=$(mktemp /tmp/jadx-XXXXXX.zip)

  info "Downloading jadx $version..."
  download "$url" "$tmp_zip"

  local install_dir="$HOME/.local/share/jadx"
  rm -rf "$install_dir"
  mkdir -p "$install_dir"
  unzip -qo "$tmp_zip" -d "$install_dir"
  rm -f "$tmp_zip"
  chmod +x "$install_dir/bin/jadx" "$install_dir/bin/jadx-gui" 2>/dev/null || true

  # Add to PATH
  mkdir -p "$HOME/.local/bin"
  ln -sf "$install_dir/bin/jadx" "$HOME/.local/bin/jadx"
  ln -sf "$install_dir/bin/jadx-gui" "$HOME/.local/bin/jadx-gui"
  export PATH="$HOME/.local/bin:$PATH"
  add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'

  if command -v jadx &>/dev/null; then
    ok "jadx $version installed to $install_dir"
  else
    ok "jadx $version installed to $install_dir"
    info "Run: export PATH=\"\$HOME/.local/bin:\$PATH\" to use it now"
  fi
}

install_vineflower() {
  # Check if already available
  if command -v vineflower &>/dev/null || command -v fernflower &>/dev/null; then
    ok "Vineflower/Fernflower CLI already installed"
    return 0
  fi
  for candidate in \
    "${FERNFLOWER_JAR_PATH:-}" \
    "$HOME/vineflower/vineflower.jar" \
    "$HOME/fernflower/fernflower.jar" \
    "$HOME/fernflower/build/libs/fernflower.jar" \
    "$HOME/vineflower/build/libs/vineflower.jar"; do
    if [[ -n "$candidate" ]] && [[ -f "$candidate" ]]; then
      ok "Vineflower/Fernflower JAR already exists: $candidate"
      return 0
    fi
  done

  # Try brew
  if [[ "$PKG_MANAGER" == "brew" ]]; then
    info "Installing vineflower via Homebrew..."
    if brew install vineflower 2>/dev/null; then
      ok "Vineflower installed via Homebrew"
      return 0
    fi
    info "Homebrew formula not available, falling back to direct download."
  fi

  # Download JAR from GitHub releases (no sudo needed)
  info "Installing Vineflower from GitHub releases..."
  local tag
  tag=$(gh_latest_tag "Vineflower/vineflower")
  if [[ -z "$tag" ]]; then
    fail "Could not determine latest Vineflower version."
    manual "Download from https://github.com/Vineflower/vineflower/releases/latest"
  fi

  local version="${tag#v}"
  local url="https://github.com/Vineflower/vineflower/releases/download/${tag}/vineflower-${version}.jar"
  local install_dir="$HOME/.local/share/vineflower"
  mkdir -p "$install_dir"

  info "Downloading Vineflower $version..."
  download "$url" "$install_dir/vineflower.jar"

  # Create wrapper script
  mkdir -p "$HOME/.local/bin"
  cat > "$HOME/.local/bin/vineflower" <<'WRAPPER'
#!/usr/bin/env bash
exec java -jar "$HOME/.local/share/vineflower/vineflower.jar" "$@"
WRAPPER
  chmod +x "$HOME/.local/bin/vineflower"

  export PATH="$HOME/.local/bin:$PATH"
  export FERNFLOWER_JAR_PATH="$install_dir/vineflower.jar"
  add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'
  add_to_profile "export FERNFLOWER_JAR_PATH=\"$install_dir/vineflower.jar\""

  ok "Vineflower $version installed to $install_dir/vineflower.jar"
  info "FERNFLOWER_JAR_PATH set to $install_dir/vineflower.jar"
}

install_dex2jar() {
  if command -v d2j-dex2jar &>/dev/null || command -v d2j-dex2jar.sh &>/dev/null; then
    ok "dex2jar already installed"
    return 0
  fi

  # Try brew
  if [[ "$PKG_MANAGER" == "brew" ]]; then
    info "Installing dex2jar via Homebrew..."
    if brew install dex2jar 2>/dev/null; then
      ok "dex2jar installed via Homebrew"
      return 0
    fi
    info "Homebrew formula not available, falling back to direct download."
  fi

  # Download from GitHub (no sudo needed)
  info "Installing dex2jar from GitHub releases..."
  local tag
  tag=$(gh_latest_tag "ThexXTURBOXx/dex2jar")
  if [[ -z "$tag" ]]; then
    # Fallback to a known maintained release if GitHub metadata is unavailable.
    tag="2.4.35"
  fi

  local version="${tag#v}"
  local url="https://github.com/ThexXTURBOXx/dex2jar/releases/download/${tag}/dex-tools-${version}.zip"
  local tmp_zip
  tmp_zip=$(mktemp /tmp/dex2jar-XXXXXX.zip)

  info "Downloading dex2jar $version..."
  if ! download "$url" "$tmp_zip"; then
    # Try alternate naming
    url="https://github.com/ThexXTURBOXx/dex2jar/releases/download/${tag}/dex-tools-v${version}.zip"
    download "$url" "$tmp_zip" || {
      fail "Download failed."
      manual "Download from https://github.com/ThexXTURBOXx/dex2jar/releases/latest"
    }
  fi

  local install_dir="$HOME/.local/share/dex2jar"
  rm -rf "$install_dir"
  mkdir -p "$install_dir"
  unzip -qo "$tmp_zip" -d "$install_dir"
  rm -f "$tmp_zip"

  # The zip may contain a top-level directory — find the actual bin location
  local bin_dir=""
  if [[ -f "$install_dir/d2j-dex2jar.sh" ]]; then
    bin_dir="$install_dir"
  else
    bin_dir=$(find "$install_dir" -name "d2j-dex2jar.sh" -exec dirname {} \; | head -1)
  fi

  if [[ -z "$bin_dir" ]]; then
    fail "Could not find d2j-dex2jar.sh in extracted archive."
    manual "Download and extract manually from https://github.com/ThexXTURBOXx/dex2jar/releases"
  fi

  chmod +x "$bin_dir"/*.sh 2>/dev/null || true

  mkdir -p "$HOME/.local/bin"
  for script in "$bin_dir"/d2j-*.sh; do
    local name
    name=$(basename "$script" .sh)
    ln -sf "$script" "$HOME/.local/bin/$name"
  done

  export PATH="$HOME/.local/bin:$PATH"
  add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'

  ok "dex2jar $version installed to $install_dir"
}

install_apktool() {
  if command -v apktool &>/dev/null; then
    ok "apktool already installed"
    return 0
  fi

  case "$PKG_MANAGER" in
    brew)    info "Installing apktool via Homebrew..."; brew install apktool ;;
    apt)     pkg_install "apktool" ;;
    *)       manual "Install apktool from https://apktool.org/docs/install" ;;
  esac

  if command -v apktool &>/dev/null; then
    ok "apktool installed"
  else
    fail "apktool installation may have failed."
    exit 1
  fi
}

install_adb() {
  if command -v adb &>/dev/null; then
    ok "adb already installed"
    return 0
  fi

  case "$PKG_MANAGER" in
    brew)    info "Installing adb via Homebrew..."; brew install android-platform-tools ;;
    apt)     pkg_install "adb" ;;
    dnf)     pkg_install "android-tools" ;;
    pacman)  pkg_install "android-tools" ;;
    *)       manual "Install Android SDK Platform Tools from https://developer.android.com/tools/releases/platform-tools" ;;
  esac

  if command -v adb &>/dev/null; then
    ok "adb installed"
  else
    fail "adb installation may have failed."
    exit 1
  fi
}

# =====================================================================
# Dispatch
# =====================================================================

case "$DEP" in
  java)        install_java ;;
  jadx)        install_jadx ;;
  vineflower|fernflower)  install_vineflower ;;
  dex2jar)     install_dex2jar ;;
  apktool)     install_apktool ;;
  adb)         install_adb ;;
  *)
    echo "Error: Unknown dependency '$DEP'" >&2
    echo "Available: java, jadx, vineflower, dex2jar, apktool, adb" >&2
    exit 1
    ;;
esac
```

## scripts/lookup-name.sh

```bash
#!/usr/bin/env bash
# lookup-name.sh — Query the mapping produced by recover-kotlin-names.sh.
#
# Modes:
#   lookup-name.sh <mapping-dir> <substring>      search by real-FQN substring
#   lookup-name.sh <mapping-dir> -o <obf>         resolve obf -> real
#   lookup-name.sh <mapping-dir> -p <pkg>         list a real package
#   lookup-name.sh <mapping-dir> --grep <regex> <sources-dir>
#       grep decompiled sources and annotate each hit with the real class name

set -euo pipefail

usage() {
  cat <<EOF
Usage: lookup-name.sh <mapping-dir> <query>
       lookup-name.sh <mapping-dir> -o <obf-fqn>
       lookup-name.sh <mapping-dir> -p <real-package-substring>
       lookup-name.sh <mapping-dir> --grep <regex> <sources-dir>

<mapping-dir> is the directory produced by recover-kotlin-names.sh
(must contain mapping.json).
EOF
  exit 0
}

[[ $# -lt 2 ]] && usage
DIR="$1"; shift
[[ ! -f "$DIR/mapping.json" ]] && { echo "no mapping.json in $DIR" >&2; exit 1; }

python3 - "$DIR" "$@" <<'PY'
import json, os, re, sys, subprocess
DIR = sys.argv[1]
args = sys.argv[2:]
MAP = json.load(open(os.path.join(DIR, "mapping.json")))
REV = {}
for o, r in MAP.items():
    REV.setdefault(r, []).append(o)

def search(q):
    ql = q.lower()
    for r in sorted(REV):
        if ql in r.lower():
            print(r)
            for o in sorted(REV[r]):
                print(f"    {o}")

def by_obf(o):
    if o not in MAP:
        print(f"no mapping for {o}", file=sys.stderr); sys.exit(1)
    print(f"{o}  ->  {MAP[o]}")
    sibs = [s for s in REV[MAP[o]] if s != o]
    for s in sorted(sibs):
        print(f"    sibling: {s}")

def by_pkg(p):
    pl = p.lower()
    for r in sorted(REV):
        if pl in r.rsplit(".", 1)[0].lower():
            print(r)
            for o in sorted(REV[r]):
                print(f"    {o}")

def grep_annot(pattern, sources):
    res = subprocess.run(
        ["grep", "-rEn", "--include=*.java", pattern, sources],
        capture_output=True, text=True)
    for line in res.stdout.splitlines():
        try:
            path, lineno, content = line.split(":", 2)
        except ValueError:
            continue
        rel = os.path.relpath(path, sources)
        obf = rel.replace(os.sep, ".")[:-5]
        suffix = f"  // {MAP[obf]}" if obf in MAP else ""
        print(f"{rel}:{lineno}:{content}{suffix}")

if args[0] == "-o" and len(args) == 2:
    by_obf(args[1])
elif args[0] == "-p" and len(args) == 2:
    by_pkg(args[1])
elif args[0] == "--grep" and len(args) == 3:
    grep_annot(args[1], args[2])
else:
    search(" ".join(args))
PY
```

## scripts/recover-kotlin-names.sh

```bash
#!/usr/bin/env bash
# recover-kotlin-names.sh — Rebuild a (obfuscated -> real) class-name map
# from Kotlin metadata strings left in decompiled sources.
#
# R8 obfuscates JVM symbols but cannot strip the Kotlin metadata strings —
# the Kotlin runtime (reflection, coroutines) needs them at runtime. Two
# annotations carry the original FQN:
#
#   * @DebugMetadata(c = "<full.qualified.Name>", f = "<File.kt>", ...)
#     emitted for almost every `suspend` function (every coroutine
#     SuspendLambda).
#
#   * @Metadata(... d2 = {"...L<pkg/Class>;..."} ...) listing internal
#     class refs of the file.
#
# Typical recovery on a real-world app: 30-50 % of classes regain their real
# names — usually 100 % of the *Repository / *ViewModel / *UseCase / *Impl
# classes you actually want to read.

set -euo pipefail

usage() {
  cat <<EOF
Usage: recover-kotlin-names.sh <decompiled-sources-dir> [output-dir]

Walks every *.java under <decompiled-sources-dir>, mines @DebugMetadata
and @Metadata annotations, and writes:

  <output-dir>/mapping.tsv   tab-separated  obf_fqn <TAB> real_fqn <TAB> file
  <output-dir>/mapping.json  same data as JSON  { obf_fqn: real_fqn, ... }
  <output-dir>/by_package/   one file per real package, listing
                             real_fqn <TAB> obf_fqn <TAB> file

If [output-dir] is omitted, files are written next to the sources dir.
EOF
  exit 0
}

[[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]] && usage
SRC="$1"
OUT="${2:-$(dirname "$SRC")/mapping}"
[[ ! -d "$SRC" ]] && { echo "not a directory: $SRC" >&2; exit 1; }

mkdir -p "$OUT/by_package"

python3 - "$SRC" "$OUT" <<'PY'
import os, re, sys, json
from collections import defaultdict

SRC, OUT = sys.argv[1], sys.argv[2]

# @DebugMetadata(c = "com.foo.Bar$Inner$1", ...)
RE_DEBUG = re.compile(r'@DebugMetadata\([^)]*?c\s*=\s*"([^"]+)"', re.S)
# @Metadata(... d2 = { "...Lcom/foo/Bar;..." ...} )
RE_DTWO  = re.compile(r'@Metadata\([^)]*?d2\s*=\s*\{([^}]*)\}', re.S)
RE_LCLASS = re.compile(r'L([A-Za-z][\w/$]+);')
# jadx sometimes emits this comment for renamed classes
RE_RENAMED = re.compile(r'/\*\s*renamed from:\s*([\w.$]+)\s*\*/')

# Skip third-party / framework trees — their names are already real.
SKIP_PREFIXES = (
    "kotlin.", "kotlinx.", "androidx.", "android.", "java.", "javax.",
    "com.google.", "com.facebook.", "com.appsflyer.", "com.datadog.",
    "io.ktor.", "io.sentry.", "io.realm.", "okhttp3.", "okio.",
    "com.squareup.", "com.bumptech.", "com.airbnb.", "com.payu.",
    "com.storyteller.", "zendesk.", "io.intercom.", "com.microsoft.",
    "com.tinder.", "com.hotjar.", "com.amplitude.", "com.segment.",
    "com.mixpanel.", "com.onesignal.", "com.stripe.", "com.braintreepayments.",
    "retrofit2.", "dagger.", "javax.inject.", "org.jetbrains.",
)

mapping = {}
file_real = {}
counts = defaultdict(int)

for dp, _, files in os.walk(SRC):
    for f in files:
        if not f.endswith(".java"):
            continue
        path = os.path.join(dp, f)
        rel = os.path.relpath(path, SRC)
        obf = rel[:-5].replace(os.sep, ".")
        if obf.startswith(SKIP_PREFIXES):
            continue
        try:
            text = open(path, "r", errors="replace").read()
        except OSError:
            continue
        real = None

        m = RE_DEBUG.search(text)
        if m:
            real = m.group(1).split("$", 1)[0]
            counts["debug_meta"] += 1

        if not real:
            m = RE_DTWO.search(text)
            if m:
                for lm in RE_LCLASS.finditer(m.group(1)):
                    cand = lm.group(1).replace("/", ".").split("$", 1)[0]
                    if "." in cand and not cand.startswith(("kotlin.", "java.", "android")):
                        real = cand
                        counts["d2"] += 1
                        break

        if not real:
            m = RE_RENAMED.search(text)
            if m:
                real = m.group(1)
                counts["renamed"] += 1

        if real:
            mapping[obf] = real
            file_real[obf] = path

with open(os.path.join(OUT, "mapping.tsv"), "w") as f:
    f.write("obf_fqn\treal_fqn\tfile\n")
    for k in sorted(mapping):
        f.write(f"{k}\t{mapping[k]}\t{file_real[k]}\n")

with open(os.path.join(OUT, "mapping.json"), "w") as f:
    json.dump(mapping, f, indent=2, sort_keys=True)

by_pkg = defaultdict(list)
for obf, real in mapping.items():
    pkg = real.rsplit(".", 1)[0] if "." in real else "(default)"
    by_pkg[pkg].append((real, obf, file_real[obf]))

for pkg, rows in by_pkg.items():
    safe = os.path.basename(pkg).replace(".", "_") or "default"
    with open(os.path.join(OUT, "by_package", f"{safe}.txt"), "w") as f:
        for real, obf, p in sorted(rows):
            f.write(f"{real}\t{obf}\t{p}\n")

print(f"Recovered {len(mapping)} class names")
for k, v in counts.items():
    print(f"  via {k}: {v}")
print(f"Real packages: {len(by_pkg)}")
print(f"Wrote {OUT}/mapping.tsv, mapping.json, by_package/")
PY
```

