Add tracker-analysis and ad-analysis skills with active vs passive SDK detection

Introduces two new skills for privacy auditing of decompiled Android apps:

- tracker-analysis: 4-phase workflow detecting Firebase, Adjust, AppsFlyer,
  Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry with init/events/
  consent/data exfiltration analysis
- ad-analysis: 3-phase workflow detecting AdMob, Unity, IronSource, AppLovin,
  Meta AN, Vungle, InMobi, Chartboost, Pangle, Mintegral with ad format
  mapping, mediation analysis, and consent framework detection

Key addition: --entrypoints flag in find-ads.sh distinguishes SDKs actively
called by app code from passive mediation adapter dependencies, enabling
accurate "Ad Architecture" classification (single mediator / multiple direct /
hybrid).

Also improves install-dep.sh sudo detection for non-interactive environments
(Claude Code, CI, pipes) and adds CLAUDE.md project instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Avogadro 2026-03-02 15:10:47 +01:00
parent ddeb9bc332
commit f216ec0914
17 changed files with 2312 additions and 34 deletions

View File

@ -1,26 +1,26 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "android-reverse-engineering-skill",
"description": "Claude Code plugins for Android reverse engineering",
"description": "Claude Code plugins for Android reverse engineering, tracker detection, and ad SDK analysis",
"owner": {
"name": "Simone Avogadro"
},
"metadata": {
"description": "Claude Code plugins for Android reverse engineering",
"version": "1.0.0"
"description": "Claude Code plugins for Android reverse engineering, tracker detection, and ad SDK analysis",
"version": "1.1.0"
},
"plugins": [
{
"name": "android-reverse-engineering",
"source": "./plugins/android-reverse-engineering",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.",
"version": "1.0.0",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, and analyze tracker/ad SDKs for privacy auditing.",
"version": "1.1.0",
"author": {
"name": "Simone Avogadro"
},
"repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
"license": "Apache-2.0",
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"],
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction", "tracker-analysis", "ad-analysis", "privacy"],
"category": "security"
}
]

82
CLAUDE.md Normal file
View File

@ -0,0 +1,82 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A Claude Code Skill (plugin) for Android reverse engineering, API extraction, and privacy auditing. It provides three skills:
- **android-reverse-engineering**: 5-phase workflow — dependency verification, APK/XAPK/JAR/AAR decompilation (jadx and/or Fernflower), manifest/structure analysis, call flow tracing, and HTTP API endpoint extraction
- **tracker-analysis**: 4-phase workflow — detect analytics/tracker SDKs (Firebase, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry), analyze init/events/user ID/consent, report data exfiltration endpoints
- **ad-analysis**: 3-phase workflow — detect ad SDKs (AdMob, Unity, IronSource, AppLovin, Meta AN, Vungle, InMobi, Chartboost, Pangle, Mintegral), map ad formats and mediation setup, report privacy/consent
## Repository Structure
- `.claude-plugin/marketplace.json` — Marketplace catalog entry
- `plugins/android-reverse-engineering/.claude-plugin/plugin.json` — Plugin manifest
- `plugins/android-reverse-engineering/commands/decompile.md``/decompile` slash command
- `plugins/android-reverse-engineering/commands/find-trackers.md``/find-trackers` slash command
- `plugins/android-reverse-engineering/commands/find-ads.md``/find-ads` slash command
- `plugins/android-reverse-engineering/skills/android-reverse-engineering/` — Core RE skill (5-phase workflow, references, scripts)
- `plugins/android-reverse-engineering/skills/tracker-analysis/` — Tracker/analytics SDK detection skill (4-phase workflow, references, find-trackers.sh)
- `plugins/android-reverse-engineering/skills/ad-analysis/` — Advertising SDK detection skill (3-phase workflow, references, find-ads.sh)
## Key Scripts
Core scripts under `plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/`:
```bash
# Check installed dependencies
bash scripts/check-deps.sh
# Install a dependency (auto-detects OS/package manager)
bash scripts/install-dep.sh <dep> # e.g., jadx, vineflower, dex2jar
# Decompile an APK/JAR/AAR/XAPK
bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] <file>
# Search decompiled source for API calls
bash scripts/find-api-calls.sh <source-dir> [--retrofit|--okhttp|--volley|--urls|--auth|--all]
```
Tracker analysis script under `plugins/android-reverse-engineering/skills/tracker-analysis/scripts/`:
```bash
# Search for tracker/analytics SDKs
bash find-trackers.sh <source-dir> [--firebase|--adjust|--appsflyer|--mixpanel|--amplitude|--segment|--braze|--clevertap|--flurry|--all]
```
Ad analysis script under `plugins/android-reverse-engineering/skills/ad-analysis/scripts/`:
```bash
# Search for advertising SDKs
bash find-ads.sh <source-dir> [--admob|--unity|--ironsource|--applovin|--facebook|--formats|--mediation|--consent|--entrypoints|--all]
```
## Architecture
**Plugin structure follows Claude Code skill conventions:**
- `skills/<name>/SKILL.md` defines the skill's workflow and capabilities
- `commands/<name>.md` defines slash commands with YAML frontmatter
- `scripts/` contains executable bash utilities invoked by the skill
- `references/` contains in-depth technical documentation the skill consults
**Script conventions:**
- Exit codes: 0 = success, 1 = error, 2 = manual action needed
- `check-deps.sh` outputs machine-readable `INSTALL_REQUIRED:` and `INSTALL_OPTIONAL:` lines
- `decompile.sh` handles XAPK by extracting the archive and decompiling each APK separately
- `${CLAUDE_PLUGIN_ROOT}` references the plugin root directory; `FERNFLOWER_JAR_PATH` for custom JAR location
**Decompiler strategy:**
- jadx: default for APKs (fast, handles resources natively)
- Fernflower/Vineflower: better Java output for complex code, requires dex2jar for APK input
- `--engine both`: runs both in parallel and compares output quality
## No Build/Test/Lint
This is a documentation-and-scripts plugin with no compiled code, no test suite, and no linter configuration. Changes are validated by reading the markdown/bash and testing scripts manually against APK files.
## Conventions
- Line endings are LF (enforced via `.gitattributes` for WSL/Windows compatibility)
- Scripts target Bash 4.0+ and support Linux (apt/dnf/pacman) and macOS (Homebrew)
- Scripts fall back to user-local installs (`~/.local/`) when sudo is unavailable

View File

@ -1,12 +1,14 @@
# Android Reverse Engineering & API Extraction — Claude Code skill
A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extracts the HTTP APIs** used by the app — Retrofit endpoints, OkHttp calls, hardcoded URLs, authentication patterns — so you can document and reproduce them without the original source code.
A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts HTTP APIs**, and **audits privacy** by detecting tracker/analytics and advertising SDKs — so you can document endpoints, understand data collection, and assess ad monetization without the original source code.
## What it does
- **Decompiles** APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower (single engine or side-by-side comparison)
- **Extracts and documents APIs**: Retrofit endpoints, OkHttp calls, hardcoded URLs, auth headers and tokens
- **Traces call flows** from Activities/Fragments through ViewModels and repositories down to HTTP calls
- **Detects tracker/analytics SDKs**: Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry — with deep analysis of init, events, user identification, consent, and data exfiltration endpoints
- **Detects advertising SDKs**: AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral — with ad format mapping, mediation analysis, and consent framework detection
- **Analyzes** app structure: manifest, packages, architecture patterns
- **Handles obfuscated code**: strategies for navigating ProGuard/R8 output
@ -50,23 +52,36 @@ Then in Claude Code:
## Usage
### Slash command
### Slash commands
```
/decompile path/to/app.apk
```
Runs the full workflow: dependency check, decompilation, and initial structure analysis.
This runs the full workflow: dependency check, decompilation, and initial structure analysis.
```
/find-trackers path/to/decompiled/sources/
```
Detects analytics/tracker SDKs and produces a privacy report with init patterns, events, user identification, consent handling, and data endpoints.
```
/find-ads path/to/decompiled/sources/
```
Detects advertising SDKs and produces a report with ad formats, mediation setup, ad unit IDs, and consent framework analysis.
### Natural language
The skill activates on phrases like:
The skills activate on phrases like:
- "Decompile this APK"
- "Reverse engineer this Android app"
- "Extract API endpoints from this app"
- "Follow the call flow from LoginActivity"
- "Analyze this AAR library"
- "Find trackers in this app"
- "What analytics SDKs does this app use?"
- "Detect ad networks in this app"
- "Show me the ad mediation setup"
### Manual scripts
@ -96,6 +111,16 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --retrofit
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --urls
# Find tracker/analytics SDKs
bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/
bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/ --firebase
bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/ --adjust
# Find advertising SDKs
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --admob
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --mediation
```
## Repository Structure
@ -109,21 +134,39 @@ android-reverse-engineering-skill/
│ ├── .claude-plugin/
│ │ └── plugin.json # Plugin manifest
│ ├── skills/
│ │ └── android-reverse-engineering/
│ │ ├── SKILL.md # Core workflow (5 phases)
│ │ ├── android-reverse-engineering/ # Core RE skill
│ │ │ ├── SKILL.md # 5-phase workflow
│ │ │ ├── references/
│ │ │ │ ├── setup-guide.md
│ │ │ │ ├── jadx-usage.md
│ │ │ │ ├── fernflower-usage.md
│ │ │ │ ├── api-extraction-patterns.md
│ │ │ │ └── call-flow-analysis.md
│ │ │ └── scripts/
│ │ │ ├── check-deps.sh
│ │ │ ├── install-dep.sh
│ │ │ ├── decompile.sh
│ │ │ └── find-api-calls.sh
│ │ ├── tracker-analysis/ # Tracker/analytics SDK detection
│ │ │ ├── SKILL.md # 4-phase workflow
│ │ │ ├── references/
│ │ │ │ ├── tracker-sdk-catalog.md
│ │ │ │ ├── tracker-init-patterns.md
│ │ │ │ └── data-exfiltration-patterns.md
│ │ │ └── scripts/
│ │ │ └── find-trackers.sh
│ │ └── ad-analysis/ # Advertising SDK detection
│ │ ├── SKILL.md # 3-phase workflow
│ │ ├── references/
│ │ │ ├── setup-guide.md
│ │ │ ├── jadx-usage.md
│ │ │ ├── fernflower-usage.md
│ │ │ ├── api-extraction-patterns.md
│ │ │ └── call-flow-analysis.md
│ │ │ ├── ad-sdk-catalog.md
│ │ │ ├── mediation-patterns.md
│ │ │ └── ad-format-patterns.md
│ │ └── scripts/
│ │ ├── check-deps.sh
│ │ ├── install-dep.sh
│ │ ├── decompile.sh
│ │ └── find-api-calls.sh
│ │ └── find-ads.sh
│ └── commands/
│ └── decompile.md # /decompile slash command
│ ├── decompile.md # /decompile slash command
│ ├── find-trackers.md # /find-trackers slash command
│ └── find-ads.md # /find-ads slash command
├── LICENSE
└── README.md
```

View File

@ -1,13 +1,13 @@
{
"name": "android-reverse-engineering",
"version": "1.0.0",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, and analyze tracker/ad SDKs for privacy auditing.",
"author": {
"name": "Simone Avogadro"
},
"repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
"license": "Apache-2.0",
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"],
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction", "tracker-analysis", "ad-analysis", "privacy"],
"skills": "./skills/",
"commands": "./commands/"
}

View File

@ -0,0 +1,81 @@
---
allowed-tools: Bash, Read, Glob, Grep, Write
description: Detect and analyze advertising SDKs in decompiled Android sources
user-invocable: true
argument-hint: <path to decompiled sources directory>
argument: path to decompiled sources directory (optional)
---
# /find-ads
Detect and analyze advertising SDKs in a decompiled Android app.
## Instructions
You are starting the ad analysis workflow. Follow these steps:
### Step 1: Get the source directory
If the user provided a path as an argument, use that. Otherwise, ask the user for the path to the decompiled sources directory.
If no decompiled sources exist yet, tell the user to run `/decompile` first on their APK/XAPK file.
Verify the directory exists and contains `.java` or `.kt` files:
```bash
find "$SOURCE_DIR" -name "*.java" -o -name "*.kt" | head -5
```
### Step 2: Run broad detection
Execute the ad detection script to sweep for all known ad SDKs:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "$SOURCE_DIR" --all
```
Parse the output — non-empty sections indicate a detected SDK or feature.
### Step 3: Analyze detected SDKs and identify active entry points
For each SDK found:
1. **Identify the role** — is it the primary mediator or a mediated network?
2. **Extract ad unit/placement IDs** — find all ID strings
3. **Map ad formats** — which formats (banner, interstitial, rewarded, native, app-open) are implemented?
4. **Trace the load/show lifecycle** — where is each ad loaded and when is it shown?
5. **Check mediation setup** — if mediation is detected, identify all adapters and the waterfall/bidding strategy
**Distinguish active vs passive SDKs** by running entry point detection:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "$SOURCE_DIR" --entrypoints
```
This searches for ad SDK calls **only in app code** (excluding library packages like `com/google`, `com/unity3d`, etc.). Compare these results with the full detection from Step 2:
- SDKs that appear in `--entrypoints` output are **actively called** by the app
- SDKs detected in Step 2 but absent from `--entrypoints` are **passive dependencies** (mediation adapters only)
- Determine the **ad architecture**: Single mediator, Multiple direct, or Hybrid
Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/` for SDK-specific patterns.
### Step 4: Produce report
Generate a structured report with:
- **Summary table**: ad network, role (mediator/mediated/active/passive), formats, ad unit IDs, consent
- **Ad architecture**: type (Single mediator / Multiple direct / Hybrid), active entry points with file:line, passive SDK list, ASCII diagram
- **Mediation setup**: primary mediator, strategy, mediated networks, adapter classes
- **Ad formats by placement**: for each format — SDK, ad unit, load/show location, trigger
- **Ad unit ID table**: all discovered IDs with format, SDK, and source location
- **Privacy & consent**: UMP, TCF, COPPA, AD_ID permission, per-network consent
Refer to `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/SKILL.md` for the full report format.
### Step 5: Offer next steps
Tell the user what they can do next:
- **Deep-dive a specific ad network**: "I can trace the full integration for AdMob"
- **Analyze mediation waterfall**: "I can map the exact network priority order"
- **Check trackers**: "Run `/find-trackers` to analyze analytics/tracker SDKs too"
- **Export report**: "I can save this report as a markdown file"

View File

@ -0,0 +1,66 @@
---
allowed-tools: Bash, Read, Glob, Grep, Write
description: Detect and analyze tracker/analytics SDKs in decompiled Android sources
user-invocable: true
argument-hint: <path to decompiled sources directory>
argument: path to decompiled sources directory (optional)
---
# /find-trackers
Detect and analyze analytics/tracker SDKs in a decompiled Android app.
## Instructions
You are starting the tracker analysis workflow. Follow these steps:
### Step 1: Get the source directory
If the user provided a path as an argument, use that. Otherwise, ask the user for the path to the decompiled sources directory.
If no decompiled sources exist yet, tell the user to run `/decompile` first on their APK/XAPK file.
Verify the directory exists and contains `.java` or `.kt` files:
```bash
find "$SOURCE_DIR" -name "*.java" -o -name "*.kt" | head -5
```
### Step 2: Run broad detection
Execute the tracker detection script to sweep for all known tracker SDKs:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh "$SOURCE_DIR" --all
```
Parse the output — non-empty sections indicate a detected SDK.
### Step 3: Analyze each detected SDK
For each SDK found in the sweep, perform deep analysis:
1. **Read the initialization code** — find where the SDK is set up, extract API keys/app IDs
2. **Catalog tracked events** — list all event names and their parameters
3. **Check user identification** — find setUserId/identify calls, what user data is collected
4. **Verify consent handling** — check if the app gates analytics on user consent
5. **Identify data endpoints** — known domains and any custom relay/proxy
Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/` for SDK-specific patterns.
### Step 4: Produce report
Generate a structured report with:
- **Summary table**: SDK name, init location, event count, user ID method, consent status
- **Per-SDK detail**: initialization, events, user data, consent, endpoints
- **Privacy summary**: total SDKs, consent coverage, user data shared, endpoint domains
Refer to `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/SKILL.md` for the full report format.
### Step 5: Offer next steps
Tell the user what they can do next:
- **Deep-dive a specific SDK**: "I can trace the full data flow for Firebase Analytics"
- **Check data exfiltration**: "I can search for proxy/relay patterns and custom endpoints"
- **Analyze ad SDKs**: "Run `/find-ads` to analyze advertising SDKs too"
- **Export report**: "I can save this report as a markdown file"

View File

@ -0,0 +1,177 @@
---
description: Detect and analyze advertising SDKs in decompiled Android apps. Identifies AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral. Extracts ad formats (banner, interstitial, rewarded, native), mediation setup, ad unit IDs, and consent/privacy frameworks.
trigger: find ads|ad analysis|advertising SDK|detect ads|ad network|AdMob|Unity Ads|IronSource|AppLovin|Facebook ads|Meta Audience Network|Vungle|InMobi|Chartboost|Pangle|Mintegral|mediation|ad format|rewarded ad|interstitial ad|banner ad
---
# Ad Analysis
Detect and analyze advertising SDKs embedded in decompiled Android applications. Produces a structured report covering ad network identification, ad formats used, mediation configuration, ad unit/placement IDs, and consent/privacy framework compliance.
## Prerequisites
This skill operates on **already decompiled** source code. If the app has not been decompiled yet, use the `android-reverse-engineering` skill first:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh <apk-file>
```
The decompiled sources directory (typically `<output>/sources/`) is the input for this skill.
## Workflow
### Phase 1: Verify Decompiled Sources
Confirm that the decompiled source directory exists and contains Java/Kotlin files.
**Action**: Check the provided directory path.
- If the user provides a path, verify it exists and contains `.java` or `.kt` files
- If no decompiled sources are available, instruct the user to run `/decompile` first
- Look for `AndroidManifest.xml` — it contains ad-related Activities, Services, meta-data (APPLICATION_ID, sdk keys), and the AD_ID permission
### Phase 2: Detect SDKs, Formats, and Mediation
Run the ad detection script to identify all ad SDKs, formats, and mediation setup.
**Action**: Execute the detection script.
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --all
```
For targeted searches:
```bash
# Single SDK
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --admob
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --unity
# Specific aspects
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --formats # cross-SDK format detection
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --mediation # mediation adapter detection
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --consent # consent/privacy frameworks
```
Parse the output to identify:
- Which ad SDKs are present
- Which ad formats are used (banner, interstitial, rewarded, native, app-open)
- Whether mediation is set up and which SDK is the mediator
- Consent framework presence
For each detected SDK, read the surrounding code to extract:
1. **Initialization**: Where and how is the SDK initialized? Extract app ID/SDK key
2. **Ad formats**: Which formats are implemented? Extract ad unit/placement IDs
3. **Mediation**: Is this SDK a mediator or a mediated network? Which adapters are present?
- See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md`
4. **Ad format details**: For each format, understand the load/show lifecycle
- See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-format-patterns.md`
5. **Consent**: Does the app implement UMP, TCF, or SDK-specific consent?
### Phase 2b: Identify Active Entry Points
Distinguish which ad SDKs the app calls directly vs which are only present as passive mediation dependencies.
**Action**: Run the entry point detection.
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <sources-dir> --entrypoints
```
This searches for ad SDK init/load/show calls **only in app code**, excluding known library packages (`com/google`, `com/unity3d`, `com/ironsource`, etc.). The results reveal which SDKs the app actively uses.
**Analysis**:
1. Compare the entry point results with the full SDK detection from Phase 2
2. For each detected SDK, classify it as:
- **Active**: the app code calls its init/load/show methods directly
- **Passive**: its code is present only inside library packages (mediation adapters)
3. Determine the ad architecture:
- **Single mediator**: the app calls only 1 ad SDK (e.g., AdMob `MobileAds.initialize()`), all others are passive adapter dependencies
- **Multiple direct**: the app calls 2+ ad SDKs directly (manual ad network brokerage)
- **Hybrid**: the app calls a mediator for some formats and additional SDKs directly for others
See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md` — "Active vs Passive SDKs" section for detailed patterns.
### Phase 3: Produce Report
Generate a structured ad analysis report.
**Report format:**
```markdown
# Ad Analysis Report — <app name>
## Summary
| Ad Network | Role | Formats | Ad Unit IDs | Consent |
|---|---|---|---|---|
| AdMob | Mediator | Banner, Interstitial, Rewarded | ca-app-pub-XXX/YYY, ... | UMP ✓ |
| Unity Ads | Mediated | Interstitial, Rewarded | (via mediation) | — |
| IronSource | Mediated | Rewarded | (via mediation) | — |
## Ad Architecture
- **Type**: Single mediator / Multiple direct / Hybrid
- **Active entry points** (called from app code):
- `MobileAds.initialize()``com/example/app/AdManager.java:28`
- `InterstitialAd.load()``com/example/app/GameActivity.java:112`
- **Passive SDKs** (present only as mediation adapter dependencies):
- Unity Ads, IronSource, AppLovin, Meta AN — invoked only via `com.google.ads.mediation.*` adapters
```
App code ──→ AdMob (mediator)
├──→ Google Ads (direct)
├──→ UnityAdapter ──→ Unity Ads SDK [passive]
├──→ IronSourceAdapter ──→ IS SDK [passive]
└──→ AppLovinAdapter ──→ AL SDK [passive]
```
## Mediation Setup
- **Primary mediator**: AdMob
- **Strategy**: Hybrid (bidding + waterfall)
- **Mediated networks**: Unity Ads, IronSource, AppLovin, Meta AN
- **Adapter classes found**: list
## Ad Formats by Placement
### Banner
- **SDK**: AdMob (direct)
- **Size**: BANNER (320×50)
- **Ad unit**: `ca-app-pub-XXXXX/YYYYY`
- **Location**: `MainActivity.java:45` — bottom of screen
- **Auto-refresh**: yes
### Interstitial
- **SDK**: AdMob (mediated to Unity, IronSource)
- **Ad unit**: `ca-app-pub-XXXXX/ZZZZZ`
- **Load location**: `GameActivity.java:112`
- **Show trigger**: level complete (`onLevelComplete()`)
### Rewarded
- **SDK**: AdMob (mediated)
- **Ad unit**: `ca-app-pub-XXXXX/WWWWW`
- **Reward**: 50 coins (`onUserEarnedReward`)
- **Show trigger**: "Watch ad for coins" button
## Ad Unit / Placement IDs
| ID | Format | SDK | Location |
|---|---|---|---|
| `ca-app-pub-XXXXX/YYYYY` | Banner | AdMob | MainActivity.java:45 |
| `ca-app-pub-XXXXX/ZZZZZ` | Interstitial | AdMob | GameActivity.java:112 |
## Privacy & Consent
- **UMP / Funding Choices**: ✓ implemented in `ConsentActivity.java`
- **TCF v2**: ✓ (IABTCF_ keys in SharedPreferences)
- **COPPA**: tagForChildDirectedTreatment = false
- **AD_ID permission**: declared in manifest
- **Per-network consent**: setHasUserConsent called for [list]
```
## References
- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-sdk-catalog.md` — Package names, classes, manifest markers for detection
- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md` — Mediation layers, waterfall vs bidding, adapter identification
- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-format-patterns.md` — Banner, interstitial, rewarded, native, app-open patterns

View File

@ -0,0 +1,247 @@
# Ad Format Patterns
Code patterns for each ad format across major SDKs — how to identify banner, interstitial, rewarded, native, and app-open ads in decompiled code.
## Banner Ads
Small ads displayed within the app UI, typically at top or bottom of screen.
### Lifecycle
1. Create ad view (XML layout or programmatic)
2. Set ad unit ID
3. Load ad request
4. Ad displays inline — auto-refreshes periodically
### SDK-specific patterns
**AdMob**:
```java
AdView adView = new AdView(context);
adView.setAdSize(AdSize.BANNER); // or LARGE_BANNER, MEDIUM_RECTANGLE, FULL_BANNER, LEADERBOARD
adView.setAdUnitId("ca-app-pub-XXXXX/YYYYY");
adView.loadAd(new AdRequest.Builder().build());
```
**Unity Ads**:
```java
BannerView bannerView = new BannerView(activity, "placement_id", new UnityBannerSize(320, 50));
bannerView.load();
```
**IronSource**:
```java
IronSource.loadBanner(bannerLayout, "placement_name");
// or LevelPlay:
LevelPlayBannerAdView banner = new LevelPlayBannerAdView(context, "ad_unit_id");
banner.loadAd();
```
**AppLovin MAX**:
```java
MaxAdView adView = new MaxAdView("ad_unit_id", context);
adView.loadAd();
adView.startAutoRefresh();
```
**Meta AN**:
```java
AdView adView = new AdView(context, "placement_id", AdSize.BANNER_HEIGHT_50);
adView.loadAd();
```
### Grep for all banners
```bash
grep -rn 'AdView\|BannerView\|AdSize\.\|BANNER\|loadBanner\|IronSourceBannerLayout\|MaxAdView\|MBBannerView\|PAGBannerAd\|InMobiBanner' "$SOURCE_DIR"
```
---
## Interstitial Ads
Full-screen ads shown at natural transition points (level complete, between pages).
### Lifecycle
1. Load ad in advance (preload)
2. Check if ad is ready
3. Show ad at transition point
4. Handle close callback → resume app flow
### SDK-specific patterns
**AdMob**:
```java
InterstitialAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest,
new InterstitialAdLoadCallback() {
@Override public void onAdLoaded(InterstitialAd ad) { /* cache */ }
@Override public void onAdFailedToLoad(LoadAdError error) { /* retry */ }
});
// Show:
interstitialAd.setFullScreenContentCallback(new FullScreenContentCallback() { ... });
interstitialAd.show(activity);
```
**Unity Ads**:
```java
UnityAds.load("placement_id", new IUnityAdsLoadListener() { ... });
UnityAds.show(activity, "placement_id", new IUnityAdsShowListener() { ... });
```
**IronSource**:
```java
IronSource.loadInterstitial();
if (IronSource.isInterstitialReady()) {
IronSource.showInterstitial("placement_name");
}
```
**AppLovin MAX**:
```java
MaxInterstitialAd interstitialAd = new MaxInterstitialAd("ad_unit_id", activity);
interstitialAd.loadAd();
interstitialAd.showAd();
```
### Grep for all interstitials
```bash
grep -rn 'InterstitialAd\|showInterstitial\|loadInterstitial\|MaxInterstitialAd\|isInterstitialReady\|FullScreenContentCallback\|TTFullScreenVideoAd\|PAGInterstitialAd' "$SOURCE_DIR"
```
---
## Rewarded Ads
Full-screen ads that give the user an in-app reward for watching.
### Lifecycle
1. Load ad in advance
2. Show ad when user opts in (e.g., "Watch ad for extra life")
3. User watches ad to completion
4. Reward callback fires → grant reward
5. Handle incomplete views (user skipped)
### SDK-specific patterns
**AdMob**:
```java
RewardedAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest,
new RewardedAdLoadCallback() {
@Override public void onAdLoaded(RewardedAd ad) { /* cache */ }
});
// Show:
rewardedAd.show(activity, new OnUserEarnedRewardListener() {
@Override public void onUserEarnedReward(RewardItem reward) {
int amount = reward.getAmount();
String type = reward.getType();
}
});
```
**Unity Ads**:
```java
// Same load/show as interstitial, but placement configured as rewarded on dashboard
UnityAds.load("rewarded_placement", loadListener);
UnityAds.show(activity, "rewarded_placement", showListener);
// Reward granted in onUnityAdsShowComplete with UnityAds.UnityAdsShowCompletionState.COMPLETED
```
**IronSource**:
```java
// Auto-loaded by default
if (IronSource.isRewardedVideoAvailable()) {
IronSource.showRewardedVideo("placement_name");
}
// Reward in RewardedVideoListener.onRewardedVideoAdRewarded(Placement placement)
```
**AppLovin MAX**:
```java
MaxRewardedAd rewardedAd = MaxRewardedAd.getInstance("ad_unit_id", activity);
rewardedAd.loadAd();
rewardedAd.showAd(); // reward via MaxRewardedAdListener.onUserRewarded(MaxAd, MaxReward)
```
### Grep for all rewarded
```bash
grep -rn 'RewardedAd\|RewardedVideo\|showRewardedVideo\|OnUserEarnedRewardListener\|RewardItem\|MaxRewardedAd\|TTRewardVideoAd\|PAGRewardedAd\|MBRewardVideoHandler' "$SOURCE_DIR"
```
---
## Native Ads
Ads that match the app's look and feel, rendered with custom templates.
### Lifecycle
1. Create ad loader with ad unit ID
2. Load ad
3. Receive native ad object with assets (headline, body, image, CTA, icon)
4. Inflate custom layout and bind assets
5. Register ad view for impression/click tracking
### SDK-specific patterns
**AdMob**:
```java
AdLoader adLoader = new AdLoader.Builder(context, "ca-app-pub-XXXXX/YYYYY")
.forNativeAd(nativeAd -> { /* bind to NativeAdView */ })
.withNativeAdOptions(new NativeAdOptions.Builder().build())
.build();
adLoader.loadAd(new AdRequest.Builder().build());
```
**Meta AN**:
```java
NativeAd nativeAd = new NativeAd(context, "placement_id");
nativeAd.loadAd(nativeAd.buildLoadAdConfig()
.withAdListener(new NativeAdListener() { ... })
.build());
```
**AppLovin MAX**:
```java
MaxNativeAdLoader nativeAdLoader = new MaxNativeAdLoader("ad_unit_id", context);
nativeAdLoader.setNativeAdListener(new MaxNativeAdListener() { ... });
nativeAdLoader.loadAd();
```
### Grep for all native
```bash
grep -rn 'NativeAd\|NativeAdView\|NativeAdLoader\|AdLoader\.Builder\|NativeBannerAd\|NativeAdLayout\|MaxNativeAdLoader\|InMobiNative\|VungleNativeAd' "$SOURCE_DIR"
```
---
## App Open Ads
Full-screen ads shown when the app is foregrounded (cold start or resume from background).
### Lifecycle
1. Preload ad during app initialization or background
2. On app foreground, check if ad is available and not expired
3. Show ad before the main content appears
4. Handle close callback → show app content
### Pattern (AdMob-specific)
```java
AppOpenAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest,
AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT,
new AppOpenAd.AppOpenAdLoadCallback() {
@Override public void onAdLoaded(AppOpenAd ad) { /* cache with timestamp */ }
});
// Show on foreground:
appOpenAd.show(activity);
```
Common implementation pattern uses `Application.ActivityLifecycleCallbacks` to detect foreground transitions:
```java
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override public void onActivityStarted(Activity activity) {
// Show app open ad if available
}
});
```
### Grep for app open
```bash
grep -rn 'AppOpenAd\|AppOpenAdLoadCallback\|APP_OPEN_AD\|AppOpenAdManager\|appOpenAd' "$SOURCE_DIR"
```

View File

@ -0,0 +1,103 @@
# Ad SDK Catalog
Quick-reference for detecting advertising SDKs in decompiled Android apps.
## AdMob / Google Mobile Ads
| Field | Value |
|---|---|
| **Package** | `com.google.android.gms.ads` |
| **Key classes** | `MobileAds`, `AdView`, `InterstitialAd`, `RewardedAd`, `NativeAd`, `AppOpenAd`, `AdRequest`, `AdLoader` |
| **Manifest markers** | `<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID"/>`, `<activity android:name="com.google.android.gms.ads.AdActivity"/>` |
| **Gradle** | `com.google.android.gms:play-services-ads` |
| **Grep detection** | `MobileAds\.initialize\|com\.google\.android\.gms\.ads\|ca-app-pub-` |
## Unity Ads
| Field | Value |
|---|---|
| **Package** | `com.unity3d.ads`, `com.unity3d.services` |
| **Key classes** | `UnityAds`, `BannerView`, `IUnityAdsInitializationListener`, `IUnityAdsLoadListener`, `IUnityAdsShowListener` |
| **Manifest markers** | `<activity android:name="com.unity3d.services.ads.adunit.AdUnitActivity"/>`, `<activity android:name="com.unity3d.ads.adunit.AdUnitTransparentActivity"/>` |
| **Gradle** | `com.unity3d.ads:unity-ads` |
| **Grep detection** | `UnityAds\.initialize\|com\.unity3d\.ads\|IUnityAds` |
## IronSource / LevelPlay
| Field | Value |
|---|---|
| **Package** | `com.ironsource.mediationsdk`, `com.ironsource.sdk` |
| **Key classes** | `IronSource`, `IronSourceBannerLayout`, `InterstitialListener`, `RewardedVideoListener`, `LevelPlayBannerAdView`, `LevelPlayInterstitialAd`, `LevelPlayRewardedAd` |
| **Manifest markers** | `<activity android:name="com.ironsource.sdk.controller.ControllerActivity"/>`, `<provider android:name="com.ironsource.sdk.handlers.provider.IronSourceDataProvider"/>` |
| **Gradle** | `com.ironsource.sdk:mediationsdk` |
| **Grep detection** | `IronSource\.init\|com\.ironsource\.mediationsdk\|LevelPlay` |
## AppLovin / MAX
| Field | Value |
|---|---|
| **Package** | `com.applovin.sdk`, `com.applovin.mediation` |
| **Key classes** | `AppLovinSdk`, `MaxAdView`, `MaxInterstitialAd`, `MaxRewardedAd`, `MaxNativeAdLoader`, `MaxAdListener`, `MaxRewardedAdListener` |
| **Manifest markers** | `<meta-data android:name="applovin.sdk.key"/>`, `<activity android:name="com.applovin.adview.AppLovinFullscreenActivity"/>` |
| **Gradle** | `com.applovin:applovin-sdk` |
| **Grep detection** | `AppLovinSdk\.getInstance\|MaxAdView\|MaxInterstitialAd\|MaxRewardedAd` |
## Meta Audience Network (Facebook)
| Field | Value |
|---|---|
| **Package** | `com.facebook.ads` |
| **Key classes** | `AudienceNetworkAds`, `AdView`, `InterstitialAd`, `RewardedVideoAd`, `NativeAd`, `NativeBannerAd`, `NativeAdLayout` |
| **Manifest markers** | `<activity android:name="com.facebook.ads.AudienceNetworkActivity"/>`, `<provider android:name="com.facebook.ads.AudienceNetworkContentProvider"/>` |
| **Gradle** | `com.facebook.android:audience-network-sdk` |
| **Grep detection** | `AudienceNetworkAds\.initialize\|com\.facebook\.ads\.\|InterstitialAdListener` |
## Vungle / Liftoff
| Field | Value |
|---|---|
| **Package** | `com.vungle.warren` (legacy), `com.vungle.ads` (v7+) |
| **Key classes** | `Vungle`, `VungleBanner`, `VungleInterstitial`, `VungleRewarded`, `VungleNativeAd` |
| **Manifest markers** | `<activity android:name="com.vungle.warren.ui.VungleActivity"/>` |
| **Gradle** | `com.vungle:vungle-ads` |
| **Grep detection** | `Vungle\.init\|com\.vungle\.\|VungleBanner\|VungleInterstitial` |
## InMobi
| Field | Value |
|---|---|
| **Package** | `com.inmobi.sdk`, `com.inmobi.ads` |
| **Key classes** | `InMobiSdk`, `InMobiBanner`, `InMobiInterstitial`, `InMobiNative` |
| **Manifest markers** | `<activity android:name="com.inmobi.rendering.InMobiAdActivity"/>` |
| **Gradle** | `com.inmobi.monetization:inmobi-ads` |
| **Grep detection** | `InMobiSdk\.init\|InMobiBanner\|InMobiInterstitial` |
## Chartboost
| Field | Value |
|---|---|
| **Package** | `com.chartboost.sdk` |
| **Key classes** | `Chartboost`, `ChartboostDelegate` |
| **Manifest markers** | `<activity android:name="com.chartboost.sdk.CBImpressionActivity"/>` |
| **Gradle** | `com.chartboost:chartboost-sdk` |
| **Grep detection** | `Chartboost\.startWithAppId\|Chartboost\.showInterstitial\|com\.chartboost\.sdk` |
## Pangle / TikTok (Bytedance)
| Field | Value |
|---|---|
| **Package** | `com.bytedance.sdk.openadsdk` (legacy), `com.pgl.sys` (v5+) |
| **Key classes** | `TTAdSdk`, `TTAdConfig`, `TTAdNative`, `TTFullScreenVideoAd`, `TTRewardVideoAd`, `TTBannerAd`; v5: `PAGSdk`, `PAGBannerAd`, `PAGInterstitialAd`, `PAGRewardedAd` |
| **Manifest markers** | `<provider android:name="com.bytedance.sdk.openadsdk.multipro.TTMultiProvider"/>` |
| **Gradle** | `com.pangle.global:ads-sdk` |
| **Grep detection** | `TTAdSdk\.init\|PAGSdk\.init\|com\.bytedance\.sdk\.openadsdk` |
## Mintegral
| Field | Value |
|---|---|
| **Package** | `com.mbridge.msdk` |
| **Key classes** | `MBridgeSDKFactory`, `MBBannerView`, `MBInterstitialVideoHandler`, `MBRewardVideoHandler`, `MBNewInterstitialHandler` |
| **Manifest markers** | `<activity android:name="com.mbridge.msdk.activity.MBCommonActivity"/>` |
| **Gradle** | `com.mbridge.msdk.oversea:mbbanner`, `com.mbridge.msdk.oversea:interstitialvideo`, etc. |
| **Grep detection** | `MBridgeSDKFactory\|com\.mbridge\.msdk\|MBBannerView\|MBRewardVideoHandler` |

View File

@ -0,0 +1,231 @@
# Mediation Patterns
How ad mediation works in decompiled Android code — identifying the primary mediator, adapter chains, waterfall vs bidding, and configuration extraction.
## What is Mediation
Ad mediation allows an app to use multiple ad networks through a single SDK. A **mediator** (e.g., AdMob, IronSource, AppLovin MAX) orchestrates which ad network serves each impression via:
- **Waterfall**: Networks are tried in priority order; first to fill wins
- **Bidding (header bidding)**: Networks bid simultaneously; highest bid wins
- **Hybrid**: Some networks bid, others fill via waterfall
## Identifying the Primary Mediator
The mediator is the SDK that controls ad loading. Look for:
### Google AdMob Mediation
```java
// AdMob is the mediator when you see mediation adapters:
com.google.ads.mediation.*
// Adapter classes follow the pattern:
com.google.ads.mediation.unity.UnityAdapter
com.google.ads.mediation.ironsource.IronSourceMediationAdapter
com.google.ads.mediation.applovin.AppLovinMediationAdapter
com.google.ads.mediation.facebook.FacebookMediationAdapter
com.google.ads.mediation.vungle.VungleMediationAdapter
```
**Grep**:
```bash
grep -rn 'com\.google\.ads\.mediation\.' "$SOURCE_DIR"
```
### IronSource / LevelPlay Mediation
```java
// IronSource is the mediator when you see adapter packages:
com.ironsource.adapters.*
// Adapter naming:
com.ironsource.adapters.admob.*
com.ironsource.adapters.unityads.*
com.ironsource.adapters.applovin.*
com.ironsource.adapters.facebook.*
com.ironsource.adapters.vungle.*
```
**Grep**:
```bash
grep -rn 'com\.ironsource\.adapters\.' "$SOURCE_DIR"
```
### AppLovin MAX Mediation
```java
// MAX is the mediator when you see MAX adapter packages:
com.applovin.mediation.adapters.*
// Adapter classes extend MediationAdapterBase:
com.applovin.mediation.adapters.GoogleMediationAdapter
com.applovin.mediation.adapters.UnityAdsMediationAdapter
com.applovin.mediation.adapters.IronSourceMediationAdapter
com.applovin.mediation.adapters.FacebookMediationAdapter
```
**Grep**:
```bash
grep -rn 'com\.applovin\.mediation\.adapters\.' "$SOURCE_DIR"
```
## Waterfall vs Bidding
### Waterfall indicators
```bash
# Priority/floor price configuration
grep -rn 'waterfall\|floorPrice\|ecpm\|eCPM\|setPriority\|adNetworkOrder' "$SOURCE_DIR"
# Manual network ordering
grep -rn 'setNetworkOrder\|setAdapterOrder\|setWaterfallConfiguration' "$SOURCE_DIR"
```
### Bidding indicators
```bash
# Real-time bidding signals
grep -rn 'bidding\|headerBidding\|bidToken\|collectSignal\|getBiddingToken' "$SOURCE_DIR"
# Bidding adapter classes (often have "Bidding" in the name)
grep -rn 'BiddingAdapter\|BiddingProvider\|RTBAdapter' "$SOURCE_DIR"
# MAX bidding
grep -rn 'MaxMediatedNetworkInfo\|isBidding\|bidFloor' "$SOURCE_DIR"
```
## Adapter Discovery
### List all mediation adapters present
```bash
# Find all adapter class files
find "$SOURCE_DIR" -path "*/mediation/adapters/*.java" -o -path "*/adapters/*Adapter*.java" | head -50
# Find adapter registration/initialization
grep -rn 'registerAdapter\|initializeAdapter\|setAdapterState\|MediationAdapter' "$SOURCE_DIR"
```
### Extract adapter configuration
```bash
# Network IDs and app keys passed to adapters
grep -rn 'setAppKey\|setAppId\|setGameId\|setSdkKey\|setNetworkKey' "$SOURCE_DIR"
# Ad unit/placement mapping (mediator → network)
grep -rn 'placementMap\|adUnitMap\|networkPlacementId\|customEventExtras' "$SOURCE_DIR"
```
## Common Mediation Setups
### Setup 1: AdMob as Mediator
```
AdMob (primary) → loads via:
├── Google Ads (direct)
├── UnityAdapter → Unity Ads SDK
├── IronSourceMediationAdapter → IronSource SDK
├── AppLovinMediationAdapter → AppLovin SDK
└── FacebookMediationAdapter → Meta AN SDK
```
### Setup 2: IronSource/LevelPlay as Mediator
```
IronSource (primary) → loads via:
├── IronSource Network (direct)
├── AdMob adapter → Google Mobile Ads SDK
├── Unity adapter → Unity Ads SDK
├── AppLovin adapter → AppLovin SDK
└── Vungle adapter → Vungle SDK
```
### Setup 3: AppLovin MAX as Mediator
```
AppLovin MAX (primary) → loads via:
├── AppLovin Network (direct, always bidding)
├── GoogleMediationAdapter → AdMob SDK
├── IronSourceMediationAdapter → IronSource SDK
├── UnityAdsMediationAdapter → Unity Ads SDK
└── FacebookMediationAdapter → Meta AN SDK
```
## Active vs Passive SDKs
When mediation is present, most ad SDKs in the APK are **passive dependencies** — they are only invoked internally by the mediator's adapter classes, never by the app's own code.
### The Entry Point Graph
```
App code (com.example.myapp.*)
Mediator SDK (e.g., AdMob) ← ACTIVE: called by app code
├──→ Adapter (com.google.ads.mediation.unity.*)
│ └──→ Unity Ads SDK ← PASSIVE: called only by adapter
├──→ Adapter (com.google.ads.mediation.ironsource.*)
│ └──→ IronSource SDK ← PASSIVE: called only by adapter
└──→ Adapter (com.google.ads.mediation.applovin.*)
└──→ AppLovin SDK ← PASSIVE: called only by adapter
```
### How to Distinguish
An SDK is **active** if its init/load/show calls appear in app code. An SDK is **passive** if its code is present only inside library packages.
**Key principle**: Search for SDK calls only in non-library directories. Known library packages to exclude:
| Package prefix | SDK |
|---|---|
| `com/google` | Google/AdMob |
| `com/unity3d` | Unity Ads |
| `com/ironsource` | IronSource/LevelPlay |
| `com/applovin` | AppLovin/MAX |
| `com/facebook` | Meta Audience Network |
| `com/vungle`, `io/vungle` | Vungle/Liftoff |
| `com/inmobi` | InMobi |
| `com/chartboost` | Chartboost |
| `com/bytedance`, `com/pgl` | Pangle/TikTok |
| `com/mbridge`, `com/mintegral` | Mintegral |
### Grep Examples
```bash
# Search for ad SDK calls ONLY in app code (exclude all library packages)
grep -rn --include="*.java" --include="*.kt" \
--exclude-dir="com/google" --exclude-dir="com/unity3d" \
--exclude-dir="com/ironsource" --exclude-dir="com/applovin" \
--exclude-dir="com/facebook" --exclude-dir="com/vungle" \
--exclude-dir="com/inmobi" --exclude-dir="com/chartboost" \
--exclude-dir="com/bytedance" --exclude-dir="com/pgl" \
--exclude-dir="com/mbridge" \
-E '(MobileAds\.initialize|UnityAds\.initialize|IronSource\.init|AppLovinSdk\.getInstance)' \
"$SOURCE_DIR"
```
**Interpreting results**:
- If `MobileAds.initialize` appears in `com/example/myapp/AdManager.java` → AdMob is **active**
- If `UnityAds.initialize` appears only in `com/google/ads/mediation/unity/UnityAdapter.java` → Unity Ads is **passive** (but this file is excluded, so it won't show up)
- If `UnityAds.initialize` appears in `com/example/myapp/AdsHelper.java` → Unity Ads is also **active** (direct integration alongside mediation)
### Architecture Classification
| Architecture | Meaning | Example |
|---|---|---|
| **Single mediator** | App calls 1 SDK; all others are passive adapter deps | AdMob init + 5 mediated networks |
| **Multiple direct** | App calls 2+ SDKs directly; no mediation | AdMob banners + Unity interstitials |
| **Hybrid** | App calls a mediator + some SDKs directly | AdMob mediation + direct IronSource rewarded |
### Automated Detection
Use the `--entrypoints` flag of `find-ads.sh`:
```bash
bash find-ads.sh <source-dir> --entrypoints
```
This runs the exclusion-based grep automatically and shows only app-initiated SDK calls.
## Extracting Mediation Configuration
Look for server-side configuration that controls the mediation waterfall:
```bash
# JSON config responses from mediation servers
grep -rn 'mediationConfig\|auctionResponse\|waterfallConfig\|adNetworkConfig' "$SOURCE_DIR"
# Remote config / A/B test for ad setup
grep -rn 'RemoteConfig.*ad\|firebase.*ad_config\|ad_waterfall\|ad_network_ids' "$SOURCE_DIR"
```

View File

@ -0,0 +1,296 @@
#!/usr/bin/env bash
# find-ads.sh — Search decompiled source for advertising SDK usage
set -euo pipefail
usage() {
cat <<EOF
Usage: find-ads.sh <source-dir> [OPTIONS]
Search decompiled Java/Kotlin source for advertising SDK usage.
Arguments:
<source-dir> Path to the decompiled sources directory
Options:
--admob Search only for AdMob/Google Mobile Ads
--unity Search only for Unity Ads
--ironsource Search only for IronSource/LevelPlay
--applovin Search only for AppLovin/MAX
--facebook Search only for Meta Audience Network
--vungle Search only for Vungle/Liftoff
--inmobi Search only for InMobi
--chartboost Search only for Chartboost
--pangle Search only for Pangle/TikTok
--mintegral Search only for Mintegral
--formats Search only for cross-SDK ad format patterns
--mediation Search only for mediation adapter patterns
--consent Search only for consent/privacy framework patterns
--manifest Search only for AndroidManifest.xml ad markers
--entrypoints Search only for ad SDK calls in app code (excludes library packages)
--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_ADMOB=false
SEARCH_UNITY=false
SEARCH_IRONSOURCE=false
SEARCH_APPLOVIN=false
SEARCH_FACEBOOK=false
SEARCH_VUNGLE=false
SEARCH_INMOBI=false
SEARCH_CHARTBOOST=false
SEARCH_PANGLE=false
SEARCH_MINTEGRAL=false
SEARCH_FORMATS=false
SEARCH_MEDIATION=false
SEARCH_CONSENT=false
SEARCH_MANIFEST=false
SEARCH_ENTRYPOINTS=false
SEARCH_ALL=true
while [[ $# -gt 0 ]]; do
case "$1" in
--admob) SEARCH_ADMOB=true; SEARCH_ALL=false; shift ;;
--unity) SEARCH_UNITY=true; SEARCH_ALL=false; shift ;;
--ironsource) SEARCH_IRONSOURCE=true; SEARCH_ALL=false; shift ;;
--applovin) SEARCH_APPLOVIN=true; SEARCH_ALL=false; shift ;;
--facebook) SEARCH_FACEBOOK=true; SEARCH_ALL=false; shift ;;
--vungle) SEARCH_VUNGLE=true; SEARCH_ALL=false; shift ;;
--inmobi) SEARCH_INMOBI=true; SEARCH_ALL=false; shift ;;
--chartboost) SEARCH_CHARTBOOST=true; SEARCH_ALL=false; shift ;;
--pangle) SEARCH_PANGLE=true; SEARCH_ALL=false; shift ;;
--mintegral) SEARCH_MINTEGRAL=true; SEARCH_ALL=false; shift ;;
--formats) SEARCH_FORMATS=true; SEARCH_ALL=false; shift ;;
--mediation) SEARCH_MEDIATION=true; SEARCH_ALL=false; shift ;;
--consent) SEARCH_CONSENT=true; SEARCH_ALL=false; shift ;;
--manifest) SEARCH_MANIFEST=true; SEARCH_ALL=false; shift ;;
--entrypoints) SEARCH_ENTRYPOINTS=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
}
run_grep_xml() {
local pattern="$1"
grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
}
# --- AdMob / Google Mobile Ads ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_ADMOB" == true ]]; then
section "AdMob — Initialization"
run_grep '(MobileAds\.initialize\s*\(|MobileAds\.setRequestConfiguration|com\.google\.android\.gms\.ads)'
section "AdMob — Banner"
run_grep '(AdView\s|AdSize\.|\.loadAd\s*\(|AdRequest\.Builder|com\.google\.android\.gms\.ads\.AdView)'
section "AdMob — Interstitial"
run_grep '(InterstitialAd\.load\s*\(|InterstitialAdLoadCallback|FullScreenContentCallback|com\.google\.android\.gms\.ads\.interstitial)'
section "AdMob — Rewarded"
run_grep '(RewardedAd\.load\s*\(|RewardedAdLoadCallback|OnUserEarnedRewardListener|RewardedInterstitialAd\.load)'
section "AdMob — Native"
run_grep '(AdLoader\.Builder|NativeAd\.|NativeAdView|UnifiedNativeAd|NativeAdOptions|com\.google\.android\.gms\.ads\.nativead)'
section "AdMob — App Open"
run_grep '(AppOpenAd\.load\s*\(|AppOpenAd\.AppOpenAdLoadCallback|AppOpenAdManager)'
section "AdMob — Ad Unit IDs"
run_grep '(ca-app-pub-|/[0-9]+/[0-9]+|adUnitId|setAdUnitId\s*\()'
fi
# --- Unity Ads ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_UNITY" == true ]]; then
section "Unity Ads — Initialization"
run_grep '(UnityAds\.initialize\s*\(|UnityAds\.isInitialized|IUnityAdsInitializationListener|com\.unity3d\.ads)'
section "Unity Ads — Load & Show"
run_grep '(UnityAds\.load\s*\(|UnityAds\.show\s*\(|IUnityAdsLoadListener|IUnityAdsShowListener)'
section "Unity Ads — Banner"
run_grep '(BannerView\s*\(|UnityBannerSize|com\.unity3d\.services\.banners)'
fi
# --- IronSource / LevelPlay ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_IRONSOURCE" == true ]]; then
section "IronSource — Initialization"
run_grep '(IronSource\.init\s*\(|IronSource\.setUserId|IronSourceConfig|com\.ironsource\.mediationsdk)'
section "IronSource — Interstitial"
run_grep '(IronSource\.loadInterstitial|IronSource\.showInterstitial|IronSource\.isInterstitialReady|InterstitialListener)'
section "IronSource — Rewarded Video"
run_grep '(IronSource\.showRewardedVideo|IronSource\.isRewardedVideoAvailable|RewardedVideoListener)'
section "IronSource — Banner"
run_grep '(IronSource\.loadBanner|IronSource\.destroyBanner|IronSourceBannerLayout|BannerListener)'
section "IronSource — LevelPlay"
run_grep '(LevelPlay|LevelPlayAdError|LevelPlayBannerAdView|LevelPlayInterstitialAd|LevelPlayRewardedAd)'
fi
# --- AppLovin / MAX ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_APPLOVIN" == true ]]; then
section "AppLovin — Initialization"
run_grep '(AppLovinSdk\.getInstance|AppLovinSdk\.initializeSdk|AppLovinSdkSettings|com\.applovin\.sdk)'
section "AppLovin MAX — Banner"
run_grep '(MaxAdView\s|MaxAdViewAdListener|maxAdView\.loadAd|maxAdView\.startAutoRefresh)'
section "AppLovin MAX — Interstitial"
run_grep '(MaxInterstitialAd\s*\(|MaxAdListener|interstitialAd\.loadAd|interstitialAd\.showAd)'
section "AppLovin MAX — Rewarded"
run_grep '(MaxRewardedAd\.getInstance|MaxRewardedAdListener|rewardedAd\.loadAd|rewardedAd\.showAd)'
section "AppLovin MAX — Native"
run_grep '(MaxNativeAdLoader|MaxNativeAdView|MaxNativeAdListener)'
fi
# --- Facebook / Meta Audience Network ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_FACEBOOK" == true ]]; then
section "Meta Audience Network — Initialization"
run_grep '(AudienceNetworkAds\.initialize|AudienceNetworkAds\.buildInitSettings|com\.facebook\.ads)'
section "Meta AN — Banner"
run_grep '(com\.facebook\.ads\.AdView|com\.facebook\.ads\.AdSize|\.loadAd\s*\(.*AdView)'
section "Meta AN — Interstitial"
run_grep '(com\.facebook\.ads\.InterstitialAd|InterstitialAdListener|InterstitialAdExtendedListener)'
section "Meta AN — Rewarded"
run_grep '(RewardedVideoAd\s*\(|RewardedVideoAdListener|RewardedVideoAdExtendedListener|com\.facebook\.ads\.RewardedVideoAd)'
section "Meta AN — Native"
run_grep '(NativeAd\s*\(|NativeBannerAd|NativeAdListener|com\.facebook\.ads\.NativeAd|NativeAdLayout)'
fi
# --- Vungle / Liftoff ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_VUNGLE" == true ]]; then
section "Vungle — Initialization & Ads"
run_grep '(Vungle\.init\s*\(|VungleInitListener|Vungle\.loadAd\s*\(|Vungle\.playAd\s*\(|Vungle\.isInitialized|com\.vungle\.warren)'
run_grep '(VungleBanner|VungleInterstitial|VungleRewarded|VungleNativeAd|com\.vungle\.ads)'
fi
# --- InMobi ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_INMOBI" == true ]]; then
section "InMobi — Initialization & Ads"
run_grep '(InMobiSdk\.init\s*\(|InMobiBanner|InMobiInterstitial|InMobiNative|com\.inmobi\.sdk|com\.inmobi\.ads)'
fi
# --- Chartboost ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_CHARTBOOST" == true ]]; then
section "Chartboost — Initialization & Ads"
run_grep '(Chartboost\.startWithAppId|Chartboost\.showInterstitial|Chartboost\.showRewardedVideo|Chartboost\.cacheInterstitial|com\.chartboost\.sdk)'
fi
# --- Pangle / TikTok ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_PANGLE" == true ]]; then
section "Pangle — Initialization & Ads"
run_grep '(TTAdSdk\.init\s*\(|TTAdConfig\.Builder|TTAdNative|TTFullScreenVideoAd|TTRewardVideoAd|TTBannerAd|com\.bytedance\.sdk\.openadsdk)'
run_grep '(PAGSdk\.init\s*\(|PAGConfig\.Builder|PAGBannerAd|PAGInterstitialAd|PAGRewardedAd|com\.pgl\.sys)'
fi
# --- Mintegral ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_MINTEGRAL" == true ]]; then
section "Mintegral — Initialization & Ads"
run_grep '(MBridgeSDKFactory\.getMBridgeSDK|MBBannerView|MBInterstitialVideoHandler|MBRewardVideoHandler|MBNewInterstitialHandler|com\.mbridge\.msdk)'
fi
# --- Cross-SDK Ad Format Patterns ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_FORMATS" == true ]]; then
section "Cross-SDK — All Banner Patterns"
run_grep '(BannerView|AdView|BannerAd|IronSourceBannerLayout|MaxAdView|MBBannerView|PAGBannerAd|TTBannerAd|InMobiBanner|VungleBanner)'
section "Cross-SDK — All Interstitial Patterns"
run_grep '(InterstitialAd|showInterstitial|loadInterstitial|MaxInterstitialAd|MBInterstitialVideoHandler|PAGInterstitialAd|TTFullScreenVideoAd|InMobiInterstitial|VungleInterstitial)'
section "Cross-SDK — All Rewarded Patterns"
run_grep '(RewardedAd|RewardedVideo|showRewardedVideo|MaxRewardedAd|MBRewardVideoHandler|PAGRewardedAd|TTRewardVideoAd|VungleRewarded|OnUserEarnedRewardListener)'
section "Cross-SDK — All Native Patterns"
run_grep '(NativeAd|NativeAdView|NativeAdLoader|NativeBannerAd|MaxNativeAdLoader|InMobiNative|VungleNativeAd|NativeAdLayout)'
section "Cross-SDK — Ad Unit / Placement IDs"
run_grep '(adUnitId|setAdUnitId|placementId|setPlacementId|PLACEMENT_ID|AD_UNIT_ID|ca-app-pub-|BANNER_ID|INTERSTITIAL_ID|REWARDED_ID)'
fi
# --- Mediation Adapters ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_MEDIATION" == true ]]; then
section "Mediation — Google AdMob Mediation Adapters"
run_grep '(com\.google\.ads\.mediation|MediationAdapter|MediationBannerAdapter|MediationInterstitialAdapter|MediationRewardedAd)'
section "Mediation — IronSource Mediation Adapters"
run_grep '(com\.ironsource\.adapters|IronSourceAdapter|ISBannerAdapter|ISInterstitialAdapter|ISRewardedVideoAdapter)'
section "Mediation — AppLovin MAX Mediation Adapters"
run_grep '(com\.applovin\.mediation\.adapters|MediationAdapterBase|MaxAdapter|MaxMediatedNetworkInfo)'
section "Mediation — Waterfall & Bidding Configuration"
run_grep '(waterfall|bidding|headerBidding|realTimeBidding|setWaterfall|mediationConfig|adNetworkId|networkName)'
fi
# --- Consent / Privacy Frameworks ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_CONSENT" == true ]]; then
section "Consent — Google UMP / Funding Choices"
run_grep '(ConsentInformation|ConsentRequestParameters|ConsentForm\.load|UserMessagingPlatform|com\.google\.android\.ump)'
section "Consent — IAB TCF v2"
run_grep '(IABTCF_|TCString|GDPR_APPLIES|GDPR_CONSENT|gdprApplies|tcfPolicyVersion|tcString)'
section "Consent — COPPA / Child-Directed"
run_grep '(tagForChildDirectedTreatment|COPPA|TAG_FOR_CHILD_DIRECTED|setTagForUnderAgeOfConsent|maxAdContentRating)'
section "Consent — SDK-specific Opt-out"
run_grep '(setHasUserConsent|setDoNotSell|setCCPADoNotSell|setGDPRConsent|gdprForgetMe|setDataProcessingOptions|setNonPersonalizedOnly)'
fi
# --- AndroidManifest.xml Markers ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_MANIFEST" == true ]]; then
section "AndroidManifest — Ad Activities & Services"
run_grep_xml '(com\.google\.android\.gms\.ads|AdActivity|com\.unity3d\.ads|com\.ironsource|com\.applovin|com\.facebook\.ads|com\.vungle|com\.inmobi|com\.chartboost|com\.bytedance)'
section "AndroidManifest — Ad Permissions"
run_grep_xml '(AD_ID|com\.google\.android\.gms\.permission\.AD_ID|BIND_GET_INSTALL_REFERRER_SERVICE)'
section "AndroidManifest — Ad Meta-data"
run_grep_xml '(com\.google\.android\.gms\.ads\.APPLICATION_ID|com\.google\.android\.gms\.ads\.AD_MANAGER_APP|applovin\.sdk\.key|com\.facebook\.sdk\.ApplicationId|unity\.ads\.gameId)'
fi
# --- Entry Points: Ad SDK calls from app code only ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_ENTRYPOINTS" == true ]]; then
section "Entry Points — Ad SDK calls from app code (library packages excluded)"
# Build --exclude-dir arguments for known library packages
EXCLUDE_DIRS=(
--exclude-dir="com/google"
--exclude-dir="com/unity3d"
--exclude-dir="com/ironsource"
--exclude-dir="com/applovin"
--exclude-dir="com/facebook"
--exclude-dir="com/vungle"
--exclude-dir="com/inmobi"
--exclude-dir="com/chartboost"
--exclude-dir="com/bytedance"
--exclude-dir="com/pgl"
--exclude-dir="com/mbridge"
--exclude-dir="com/mintegral"
--exclude-dir="com/adcolony"
--exclude-dir="com/tapjoy"
--exclude-dir="com/fyber"
--exclude-dir="com/amazon/device/ads"
--exclude-dir="io/vungle"
)
ENTRYPOINT_PATTERN='(MobileAds\.initialize|MobileAds\.setRequestConfiguration|InterstitialAd\.load|RewardedAd\.load|RewardedInterstitialAd\.load|AppOpenAd\.load|AdView\.loadAd|AdLoader\.Builder|UnityAds\.initialize|UnityAds\.load|UnityAds\.show|IronSource\.init|IronSource\.loadInterstitial|IronSource\.showInterstitial|IronSource\.showRewardedVideo|IronSource\.loadBanner|AppLovinSdk\.getInstance|AppLovinSdk\.initializeSdk|MaxInterstitialAd|MaxRewardedAd|MaxAdView|AudienceNetworkAds\.initialize|Vungle\.init|Vungle\.loadAd|Vungle\.playAd|InMobiSdk\.init|Chartboost\.startWithAppId|TTAdSdk\.init|PAGSdk\.init|MBridgeSDKFactory\.getMBridgeSDK|LevelPlayInterstitialAd|LevelPlayRewardedAd|LevelPlayBannerAdView)'
# shellcheck disable=SC2086
grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true
echo
echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded."
echo "If no results appear, ad SDKs may only be invoked internally via mediation adapters."
fi
echo
echo "=== Search complete ==="

View File

@ -59,13 +59,18 @@ elif command -v pacman &>/dev/null; then
PKG_MANAGER="pacman"
fi
# Check sudo availability
# Check sudo availability — must actually work, not just exist
if command -v sudo &>/dev/null; then
if sudo -n true 2>/dev/null; then
# Passwordless sudo works (NOPASSWD or cached credentials)
HAS_SUDO=true
elif [[ -t 0 ]]; then
# stdin is a terminal — sudo can prompt for password interactively
HAS_SUDO=true
else
# sudo exists but may need password — we'll try it and let it prompt
HAS_SUDO=true
# sudo exists but can't authenticate (no TTY, no cached credentials)
# This happens inside Claude Code, CI, cron, pipes, etc.
HAS_SUDO=false
fi
fi
@ -73,8 +78,13 @@ info() { echo "[INFO] $*"; }
ok() { echo "[OK] $*"; }
fail() { echo "[FAIL] $*" >&2; }
manual() {
echo "[MANUAL] $*" >&2
echo " Cannot install automatically. Please install manually and retry." >&2
echo "" >&2
echo "[MANUAL ACTION REQUIRED]" >&2
echo " Cannot install automatically (no interactive terminal for sudo)." >&2
echo " Please run the following command in a terminal, then retry:" >&2
echo "" >&2
echo " $*" >&2
echo "" >&2
exit 2
}
@ -91,7 +101,7 @@ pkg_install() {
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"
manual "sudo apt-get update && sudo apt-get install -y $pkg"
fi
;;
dnf)
@ -99,7 +109,7 @@ pkg_install() {
info "Installing $pkg via dnf..."
sudo dnf install -y "$pkg"
else
manual "Run: sudo dnf install $pkg"
manual "sudo dnf install -y $pkg"
fi
;;
pacman)
@ -107,7 +117,7 @@ pkg_install() {
info "Installing $pkg via pacman..."
sudo pacman -S --noconfirm "$pkg"
else
manual "Run: sudo pacman -S $pkg"
manual "sudo pacman -S $pkg"
fi
;;
*)
@ -116,6 +126,35 @@ pkg_install() {
esac
}
# --- Helper: check that a tool needed by this script is available ---
require_tool() {
local tool="$1"
if command -v "$tool" &>/dev/null; then
return 0
fi
# Build the install command for the user
local install_cmd=""
case "$PKG_MANAGER" in
brew) install_cmd="brew install $tool" ;;
apt) install_cmd="sudo apt-get update && sudo apt-get install -y $tool" ;;
dnf) install_cmd="sudo dnf install -y $tool" ;;
pacman) install_cmd="sudo pacman -S $tool" ;;
esac
echo "" >&2
echo "[MISSING PREREQUISITE] '$tool' is required by install-dep.sh but is not installed." >&2
if [[ -n "$install_cmd" ]]; then
echo " Please run the following command in a terminal first, then retry:" >&2
echo "" >&2
echo " $install_cmd" >&2
else
echo " Please install '$tool' using your system package manager, then retry." >&2
fi
echo "" >&2
exit 2
}
# --- Helper: download a file ---
download() {
local url="$1" dest="$2"
@ -124,8 +163,7 @@ download() {
elif command -v wget &>/dev/null; then
wget -q -O "$dest" "$url"
else
fail "Neither curl nor wget available."
return 1
require_tool "curl" # exits with instructions
fi
}
@ -204,6 +242,9 @@ install_jadx() {
return 0
fi
# Check prerequisites for download-based install
require_tool "unzip"
# Try brew first (cleanest)
if [[ "$PKG_MANAGER" == "brew" ]]; then
info "Installing jadx via Homebrew..."
@ -319,6 +360,9 @@ install_dex2jar() {
return 0
fi
# Check prerequisites for download-based install
require_tool "unzip"
# Try brew
if [[ "$PKG_MANAGER" == "brew" ]]; then
info "Installing dex2jar via Homebrew..."

View File

@ -0,0 +1,129 @@
---
description: Detect and analyze analytics/tracker SDKs in decompiled Android apps. Identifies Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry, and other telemetry SDKs. Extracts initialization patterns, event logging, user identification, consent mechanisms, and data exfiltration endpoints.
trigger: find trackers|tracker analysis|analytics SDK|detect trackers|privacy analysis trackers|telemetry SDK|Firebase Analytics|Adjust SDK|AppsFlyer|Mixpanel|Amplitude|Segment|Braze|CleverTap|Flurry|what trackers|which analytics|tracking SDK
---
# Tracker Analysis
Detect and analyze analytics/tracker SDKs embedded in decompiled Android applications. Produces a structured privacy report covering SDK identification, initialization, event logging, user identification, consent handling, and known data exfiltration endpoints.
## Prerequisites
This skill operates on **already decompiled** source code. If the app has not been decompiled yet, use the `android-reverse-engineering` skill first:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh <apk-file>
```
The decompiled sources directory (typically `<output>/sources/`) is the input for this skill.
## Workflow
### Phase 1: Verify Decompiled Sources
Confirm that the decompiled source directory exists and contains Java/Kotlin files.
**Action**: Check the provided directory path.
- If the user provides a path, verify it exists and contains `.java` or `.kt` files
- If no decompiled sources are available, instruct the user to run `/decompile` first
- Look for `AndroidManifest.xml` in the parent or sibling `resources/` directory — it contains tracker-relevant metadata
### Phase 2: Broad Detection Sweep
Run the tracker detection script to identify all tracker SDKs present in the codebase.
**Action**: Execute the detection script.
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <sources-dir> --all
```
For targeted searches on a specific SDK:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <sources-dir> --firebase
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <sources-dir> --adjust
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <sources-dir> --mixpanel
```
Parse the output to identify which SDKs are present. Empty sections mean that SDK is not used.
### Phase 3: Deep Semantic Analysis
For each detected SDK, perform a thorough analysis following the patterns in the reference documents.
**For each SDK found, trace these in order:**
1. **Initialization**: How is the SDK initialized? Where is the API key/app ID? Is it hardcoded or loaded from config?
- Read the initialization code and extract credentials
- Check `AndroidManifest.xml` for meta-data entries
- See `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-init-patterns.md`
2. **Event Logging**: What events are tracked? Are they standard SDK events or custom?
- List all event names (both string literals and constants)
- Note what data is attached to each event (parameters, properties)
- Look for revenue/purchase tracking
3. **User Identification**: How are users identified across sessions?
- Find `setUserId`, `identify`, `onUserLogin` calls
- Check what user properties are set (email, name, custom attributes)
- Look for cross-device identification
4. **Consent & Opt-out**: Does the app implement consent management for this SDK?
- Find opt-out/opt-in calls
- Check for GDPR compliance (forget-me, data deletion)
- Check for consent gating (is analytics enabled only after consent?)
5. **Data Exfiltration**: Where does the data go?
- Identify endpoints from `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/data-exfiltration-patterns.md`
- Check for custom endpoints or proxy/relay configurations
- Look for batch upload patterns
### Phase 4: Produce Report
Generate a structured tracker analysis report.
**Report format:**
```markdown
# Tracker Analysis Report — <app name>
## Summary
| SDK | Version hint | Init location | Events tracked | User ID | Consent |
|-----|-------------|---------------|----------------|---------|---------|
| Firebase Analytics | (from gradle/manifest) | Application.onCreate | 12 custom + standard | setUserId | setConsent ✓ |
| Adjust | — | MainActivity | 3 events | — | gdprForgetMe ✓ |
| ... | | | | | |
## Detailed Analysis
### Firebase Analytics
- **Initialization**: `FirebaseAnalytics.getInstance()` in `MyApp.java:34`
- **API Key / App ID**: `google_app_id` = `1:123456:android:abc` (from strings.xml)
- **Events tracked**:
- `select_content` (standard) — item_id, item_name
- `purchase_complete` (custom) — amount, currency, product_id
- ...
- **User identification**: `setUserId("...")` called in `LoginViewModel.java:78`
- **User properties**: `subscription_tier`, `app_version`
- **Consent**: `setAnalyticsCollectionEnabled(false)` in `ConsentManager.java:22`, toggled based on user preference
- **Endpoints**: `app-measurement.com` (standard, no custom relay)
### [Next SDK...]
## Privacy Summary
- **Total SDKs detected**: N
- **SDKs with consent gating**: list
- **SDKs without consent gating**: list (⚠️)
- **User data shared**: email, user ID, device ID, location, ...
- **Known endpoint domains**: list of all domains data is sent to
```
## References
- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-sdk-catalog.md` — Package names, classes, manifest markers for detection
- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-init-patterns.md` — Init calls, event logging, user ID, consent per SDK
- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/data-exfiltration-patterns.md` — Endpoints, proxy patterns, batch upload

View File

@ -0,0 +1,150 @@
# Data Exfiltration Patterns
How tracker SDKs send data out of the device — known endpoints, custom configurations, proxy patterns, and batch upload mechanisms.
## Known Endpoints by SDK
### Firebase Analytics / Google Analytics
| Endpoint | Purpose |
|---|---|
| `app-measurement.com` | Primary event collection |
| `firebase-settings.crashlytics.com` | Crashlytics config |
| `firebaselogging-pa.googleapis.com` | Event logging |
| `google-analytics.com/collect` | Legacy GA hits |
| `www.googletagmanager.com` | GTM container |
### Adjust
| Endpoint | Purpose |
|---|---|
| `app.adjust.com` | Event tracking, attribution |
| `app.adjust.io` | Alternative domain |
| `app.adjust.world` | Regional alternative |
| `cdn.adjust.com` | SDK config |
| `gdpr.adjust.com` | GDPR forget-me requests |
### AppsFlyer
| Endpoint | Purpose |
|---|---|
| `launches.appsflyer.com` | App launch events |
| `events.appsflyer.com` | In-app events |
| `register.appsflyer.com` | Install attribution |
| `inapps.appsflyer.com` | In-app purchase validation |
| `gcdsdk.appsflyer.com` | SDK config |
### Mixpanel
| Endpoint | Purpose |
|---|---|
| `api.mixpanel.com/track` | Event tracking |
| `api.mixpanel.com/engage` | People profiles |
| `api.mixpanel.com/decide` | Feature flags, A/B tests |
| `api-js.mixpanel.com` | Alternative endpoint |
### Amplitude
| Endpoint | Purpose |
|---|---|
| `api.amplitude.com/2/httpapi` | Event upload (v2) |
| `api2.amplitude.com/2/httpapi` | Batch event upload |
| `cdn.amplitude.com` | SDK config |
| `regionconfig.amplitude.com` | Region routing |
### Segment
| Endpoint | Purpose |
|---|---|
| `api.segment.io/v1/t` | Track events |
| `api.segment.io/v1/i` | Identify calls |
| `api.segment.io/v1/batch` | Batch upload |
| `cdn-settings.segment.com` | Workspace settings |
### Braze
| Endpoint | Purpose |
|---|---|
| `sdk.iad-01.braze.com` | US-01 SDK endpoint (varies per cluster) |
| `sdk.iad-02.braze.com` | US-02 SDK endpoint |
| `sdk.fra-01.braze.eu` | EU-01 SDK endpoint |
| `rest.iad-01.braze.com` | REST API |
Braze uses cluster-specific endpoints: `sdk.iad-NN.braze.com`, `sdk.fra-NN.braze.eu`. Look for `.setCustomEndpoint()` in `BrazeConfig.Builder`.
### CleverTap
| Endpoint | Purpose |
|---|---|
| `wzrkt.com` | Primary analytics/events |
| `in.clevertap.com` | India region |
| `eu1.clevertap.com` | EU region |
| `sg1.clevertap.com` | Singapore region |
| `clevertap-prod.com` | Legacy |
### Flurry
| Endpoint | Purpose |
|---|---|
| `data.flurry.com` | Event data upload |
| `adlog.flurry.com` | Ad analytics |
| `cfg.flurry.com` | SDK configuration |
## Custom Endpoint Configuration
Some SDKs allow custom endpoint configuration — important for identifying proxy/relay setups.
### Grep for custom endpoints
```bash
# Braze custom endpoint
grep -rn 'setCustomEndpoint\|setEndpoint\|setApiUrl' "$SOURCE_DIR"
# Segment custom host
grep -rn 'connectionFactory\|apiHost\|cdnHost' "$SOURCE_DIR"
# Mixpanel custom server
grep -rn 'setServerURL\|setDecideUrl\|setEventsUrl' "$SOURCE_DIR"
# Amplitude custom server
grep -rn 'setServerUrl\|setUseDynamicConfig' "$SOURCE_DIR"
# Generic proxy patterns
grep -rn 'proxyUrl\|proxyHost\|relayEndpoint\|analyticsProxy\|trackingProxy' "$SOURCE_DIR"
```
## Proxy & Relay Patterns
Apps may route analytics through their own servers to avoid ad blockers or aggregate data.
### Detection patterns
```bash
# Look for OkHttp interceptors that rewrite analytics URLs
grep -rn 'Interceptor.*analytics\|Interceptor.*tracking\|rewriteUrl.*analytics' "$SOURCE_DIR"
# Server-side relay: app sends events to its own API, which forwards to tracker
grep -rn '/api/analytics\|/api/events\|/api/tracking\|/v1/collect\|/v1/events' "$SOURCE_DIR"
# Custom transport layer
grep -rn 'AnalyticsTransport\|EventTransport\|TrackingTransport\|EventDispatcher' "$SOURCE_DIR"
```
## Batch Upload Patterns
Most SDKs batch events and upload periodically or on app backgrounding.
### Common batch patterns
```bash
# Database/file storage for batched events
grep -rn 'EventDatabase\|event_queue\|analytics_db\|pending_events\|EventStore' "$SOURCE_DIR"
# Flush triggers
grep -rn '\.flush\(\)\|flushEvents\|flushQueue\|uploadEvents\|sendBatch\|dispatchEvents' "$SOURCE_DIR"
# Batch size / interval config
grep -rn 'flushInterval\|batchSize\|maxQueueSize\|uploadInterval\|flushAt\|setFlushInterval' "$SOURCE_DIR"
```
## Finding the Underlying HTTP Calls
Tracker SDKs ultimately use standard HTTP mechanisms. To see the actual network calls:
```bash
# OkHttp client creation inside SDK packages
grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/google/firebase/
grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/adjust/
grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/appsflyer/
# POST bodies — look for JSON construction near known SDK packages
grep -rn 'toJson\|JSONObject\|JsonWriter\|Gson\|Moshi' "$SOURCE_DIR"/com/mixpanel/
```

View File

@ -0,0 +1,305 @@
# Tracker SDK Initialization & Event Patterns
Detailed code patterns for each tracker SDK — how they initialize, log events, identify users, and handle consent.
## Firebase Analytics
### Initialization
```java
// Typically in Application.onCreate() or Activity
FirebaseAnalytics mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
// Auto-initialized via google-services.json — may have no explicit init call
FirebaseApp.initializeApp(context);
```
**Where to find API key**: `google-services.json``project_info.firebase_url` and `client[].api_key`. After decompilation, check `res/values/strings.xml` for `google_app_id`, `firebase_database_url`, `gcm_defaultSenderId`.
### Event Logging
```java
Bundle params = new Bundle();
params.putString(FirebaseAnalytics.Param.ITEM_ID, id);
params.putString(FirebaseAnalytics.Param.ITEM_NAME, name);
mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, params);
// Custom events
mFirebaseAnalytics.logEvent("custom_event_name", params);
```
**Grep**: `\.logEvent\s*\(` — captures both standard and custom events.
### User Identification
```java
mFirebaseAnalytics.setUserId("user_id_string");
mFirebaseAnalytics.setUserProperty("favorite_food", "pizza");
```
### Consent / Opt-out
```java
mFirebaseAnalytics.setAnalyticsCollectionEnabled(false); // kill switch
// Granular consent (v21.0.0+)
mFirebaseAnalytics.setConsent(Map.of(
ConsentType.ANALYTICS_STORAGE, ConsentStatus.DENIED,
ConsentType.AD_STORAGE, ConsentStatus.DENIED
));
```
---
## Adjust
### Initialization
```java
AdjustConfig config = new AdjustConfig(this, "APP_TOKEN", AdjustConfig.ENVIRONMENT_PRODUCTION);
config.setLogLevel(LogLevel.WARN);
config.setOnAttributionChangedListener(attribution -> { /* ... */ });
Adjust.onCreate(config);
```
**Where to find API key**: First argument to `AdjustConfig()` constructor — the app token string.
### Event Tracking
```java
AdjustEvent event = new AdjustEvent("EVENT_TOKEN");
event.setRevenue(1.0, "USD");
event.addCallbackParameter("key", "value");
Adjust.trackEvent(event);
```
### Attribution
```java
config.setOnAttributionChangedListener(attribution -> {
attribution.trackerToken;
attribution.trackerName;
attribution.network;
attribution.campaign;
});
```
### Consent
```java
Adjust.gdprForgetMe(context); // GDPR right to be forgotten
Adjust.disableThirdPartySharing(context);
Adjust.trackThirdPartySharing(thirdPartySharing);
```
---
## AppsFlyer
### Initialization
```java
AppsFlyerLib.getInstance().init("AF_DEV_KEY", conversionListener, this);
AppsFlyerLib.getInstance().start(this);
```
**Where to find API key**: First argument to `.init()` — the dev key string.
### Event Tracking
```java
Map<String, Object> eventValues = new HashMap<>();
eventValues.put(AFInAppEventParameterName.REVENUE, 1200);
eventValues.put(AFInAppEventParameterName.CURRENCY, "USD");
AppsFlyerLib.getInstance().logEvent(context, AFInAppEventType.PURCHASE, eventValues);
```
### User Identity
```java
AppsFlyerLib.getInstance().setCustomerUserId("user_id");
AppsFlyerLib.getInstance().setAdditionalData(customData);
```
### Consent
```java
AppsFlyerLib.getInstance().anonymizeUser(true);
AppsFlyerLib.getInstance().stop(true, context); // full opt-out
```
---
## Mixpanel
### Initialization
```java
MixpanelAPI mixpanel = MixpanelAPI.getInstance(context, "MIXPANEL_TOKEN");
```
**Where to find token**: First string argument to `getInstance()`.
### Event Tracking
```java
JSONObject props = new JSONObject();
props.put("source", "search");
mixpanel.track("Button Clicked", props);
mixpanel.timeEvent("Time On Page"); // starts timer
mixpanel.track("Time On Page"); // ends timer, records duration
```
### User Identity
```java
mixpanel.identify("user_id");
mixpanel.alias("new_id", "existing_id");
mixpanel.getPeople().set("$email", "user@example.com");
mixpanel.getPeople().increment("login_count", 1);
```
### Consent
```java
mixpanel.optOutTracking(); // full opt-out
mixpanel.optInTracking(); // re-enable
```
---
## Amplitude
### Initialization
```java
// v1 (legacy)
Amplitude.getInstance().initialize(context, "API_KEY").enableForegroundTracking(application);
// v2 (kotlin-first)
val amplitude = Amplitude(Configuration(apiKey = "API_KEY", context = applicationContext))
```
**Where to find API key**: Argument to `initialize()` or `Configuration()`.
### Event Tracking
```java
Amplitude.getInstance().logEvent("button_clicked");
Amplitude.getInstance().logEvent("purchase", new JSONObject().put("price", 9.99));
Amplitude.getInstance().logRevenue(new Revenue().setProductId("product").setPrice(9.99));
```
### User Identity
```java
Amplitude.getInstance().setUserId("user_id");
Identify identify = new Identify().set("plan", "premium").add("login_count", 1);
Amplitude.getInstance().identify(identify);
```
### Consent
```java
Amplitude.getInstance().setOptOut(true);
Amplitude.getInstance().enableCoppaControl(); // COPPA
```
---
## Segment
### Initialization
```java
Analytics analytics = new Analytics.Builder(context, "WRITE_KEY")
.trackApplicationLifecycleEvents()
.build();
Analytics.setSingletonInstance(analytics);
```
**Where to find write key**: Second argument to `Analytics.Builder()`.
### Event Tracking
```java
Analytics.with(context).track("Item Purchased",
new Properties().putValue("item", "sword").putValue("revenue", 9.99));
Analytics.with(context).screen("Home", new Properties().putValue("tab", "feed"));
```
### User Identity
```java
Analytics.with(context).identify("user_id",
new Traits().putName("John").putEmail("john@example.com"), null);
Analytics.with(context).alias("new_id");
```
### Consent
```java
Analytics.with(context).optOut(true);
```
---
## Braze
### Initialization
```java
BrazeConfig brazeConfig = new BrazeConfig.Builder()
.setApiKey("API_KEY")
.setCustomEndpoint("sdk.iad-01.braze.com")
.build();
Braze.configure(context, brazeConfig);
```
**Where to find API key**: `.setApiKey()` and `.setCustomEndpoint()` in the config builder.
### Event & Purchase Logging
```java
Braze.getInstance(context).logCustomEvent("event_name", new BrazeProperties().addProperty("key", "value"));
Braze.getInstance(context).logPurchase("product_id", "USD", new BigDecimal("9.99"));
```
### User Identity
```java
Braze.getInstance(context).changeUser("user_id");
Braze.getInstance(context).getCurrentUser().setEmail("user@example.com");
Braze.getInstance(context).getCurrentUser().setCustomUserAttribute("attr", "value");
```
---
## CleverTap
### Initialization
```java
CleverTapAPI clevertapDefaultInstance = CleverTapAPI.getDefaultInstance(applicationContext);
// Auto-init via manifest meta-data: CLEVERTAP_ACCOUNT_ID, CLEVERTAP_TOKEN
ActivityLifecycleCallback.register(application);
```
**Where to find credentials**: Manifest `<meta-data>` tags: `CLEVERTAP_ACCOUNT_ID`, `CLEVERTAP_TOKEN`.
### Event Tracking
```java
HashMap<String, Object> props = new HashMap<>();
props.put("Product Name", "Widget");
clevertapDefaultInstance.pushEvent("Product Viewed", props);
// Charged event (purchase)
clevertapDefaultInstance.pushChargedEvent(chargeDetails, items);
```
### User Identity
```java
HashMap<String, Object> profile = new HashMap<>();
profile.put("Name", "John");
profile.put("Email", "john@example.com");
profile.put("Identity", "user_id");
clevertapDefaultInstance.onUserLogin(profile);
clevertapDefaultInstance.pushProfile(profile);
```
---
## Flurry
### Initialization
```java
new FlurryAgent.Builder()
.withLogEnabled(true)
.withCaptureUncaughtExceptions(true)
.build(context, "FLURRY_API_KEY");
```
**Where to find API key**: Last argument to `.build()`.
### Event Tracking
```java
FlurryAgent.logEvent("event_name");
FlurryAgent.logEvent("event_name", params); // params is Map<String, String>
FlurryAgent.logEvent("timed_event", params, true); // timed event
FlurryAgent.endTimedEvent("timed_event");
FlurryAgent.logPayment("product", "id", 1, 9.99, "USD", "txn_id", null);
```
### User Identity
```java
FlurryAgent.setUserId("user_id");
FlurryAgent.setAge(25);
FlurryAgent.setGender(Constants.MALE);
```

View File

@ -0,0 +1,93 @@
# Tracker SDK Catalog
Quick-reference for detecting analytics/tracker SDKs in decompiled Android apps.
## Firebase Analytics
| Field | Value |
|---|---|
| **Package** | `com.google.firebase.analytics` |
| **Key classes** | `FirebaseAnalytics`, `AppMeasurement`, `AppMeasurementService` |
| **Manifest markers** | `<service android:name="com.google.android.gms.measurement.AppMeasurementService"/>`, `<receiver android:name="com.google.android.gms.measurement.AppMeasurementReceiver"/>` |
| **Gradle** | `com.google.firebase:firebase-analytics` |
| **Grep detection** | `FirebaseAnalytics\.getInstance\|AppMeasurement\|app-measurement\.com` |
## Adjust
| Field | Value |
|---|---|
| **Package** | `com.adjust.sdk` |
| **Key classes** | `Adjust`, `AdjustConfig`, `AdjustEvent`, `AdjustAttribution` |
| **Manifest markers** | `<receiver android:name="com.adjust.sdk.AdjustReferrerReceiver"/>` |
| **Gradle** | `com.adjust.sdk:adjust-android` |
| **Grep detection** | `AdjustConfig\|AdjustEvent\|Adjust\.trackEvent\|Adjust\.onCreate` |
## AppsFlyer
| Field | Value |
|---|---|
| **Package** | `com.appsflyer` |
| **Key classes** | `AppsFlyerLib`, `AppsFlyerConversionListener`, `AFInAppEventType` |
| **Manifest markers** | `<receiver android:name="com.appsflyer.SingleInstallBroadcastReceiver"/>`, `<service android:name="com.appsflyer.AppsFlyerJobIntentService"/>` |
| **Gradle** | `com.appsflyer:af-android-sdk` |
| **Grep detection** | `AppsFlyerLib\.getInstance\|AFInAppEventType\|appsflyer\.com` |
## Mixpanel
| Field | Value |
|---|---|
| **Package** | `com.mixpanel.android` |
| **Key classes** | `MixpanelAPI`, `AnalyticsMessages`, `DecideMessages` |
| **Manifest markers** | `<service android:name="com.mixpanel.android.mpmetrics.MixpanelFCMMessagingService"/>` |
| **Gradle** | `com.mixpanel.android:mixpanel-android` |
| **Grep detection** | `MixpanelAPI\.getInstance\|com\.mixpanel\.android\|api\.mixpanel\.com` |
## Amplitude
| Field | Value |
|---|---|
| **Package** | `com.amplitude.api` (v2), `com.amplitude.android` (v1) |
| **Key classes** | `Amplitude`, `AmplitudeClient`, `Revenue`, `Identify` |
| **Manifest markers** | (none specific — uses standard Android components) |
| **Gradle** | `com.amplitude:analytics-android` (v2), `com.amplitude:android-sdk` (v1) |
| **Grep detection** | `Amplitude\.getInstance\|AmplitudeClient\|api\.amplitude\.com` |
## Segment
| Field | Value |
|---|---|
| **Package** | `com.segment.analytics` |
| **Key classes** | `Analytics`, `TrackPayload`, `IdentifyPayload`, `ScreenPayload`, `Traits` |
| **Manifest markers** | (none specific) |
| **Gradle** | `com.segment.analytics.android:analytics` |
| **Grep detection** | `Analytics\.with\|com\.segment\.analytics\|api\.segment\.io` |
## Braze (formerly Appboy)
| Field | Value |
|---|---|
| **Package** | `com.braze` (v4+), `com.appboy` (legacy) |
| **Key classes** | `Braze`, `BrazeConfig`, `BrazeUser`, `BrazeProperties`; legacy: `Appboy`, `AppboyConfig` |
| **Manifest markers** | `<service android:name="com.braze.push.BrazeFirebaseMessagingService"/>`, `com.braze.BrazeActivityLifecycleCallbackListener` |
| **Gradle** | `com.braze:android-sdk-ui` |
| **Grep detection** | `Braze\.configure\|BrazeConfig\|logCustomEvent\|com\.braze\.\|com\.appboy\.` |
## CleverTap
| Field | Value |
|---|---|
| **Package** | `com.clevertap.android.sdk` |
| **Key classes** | `CleverTapAPI`, `ActivityLifecycleCallback` |
| **Manifest markers** | `<meta-data android:name="CLEVERTAP_ACCOUNT_ID"/>`, `<service android:name="com.clevertap.android.sdk.pushnotification.fcm.FcmMessageListenerService"/>` |
| **Gradle** | `com.clevertap.android:clevertap-android-sdk` |
| **Grep detection** | `CleverTapAPI\.getDefaultInstance\|pushEvent\|onUserLogin\|wzrkt\.com` |
## Flurry
| Field | Value |
|---|---|
| **Package** | `com.flurry.android` |
| **Key classes** | `FlurryAgent`, `FlurryConfig`, `FlurryPerformance` |
| **Manifest markers** | `<service android:name="com.flurry.android.agent.FlurryService"/>` |
| **Gradle** | `com.flurry.android:analytics` |
| **Grep detection** | `FlurryAgent\.logEvent\|FlurryAgent\.Builder\|data\.flurry\.com` |

View File

@ -0,0 +1,231 @@
#!/usr/bin/env bash
# find-trackers.sh — Search decompiled source for tracker/analytics SDK usage
set -euo pipefail
usage() {
cat <<EOF
Usage: find-trackers.sh <source-dir> [OPTIONS]
Search decompiled Java/Kotlin source for analytics and tracker SDK usage.
Arguments:
<source-dir> Path to the decompiled sources directory
Options:
--firebase Search only for Firebase Analytics
--adjust Search only for Adjust SDK
--appsflyer Search only for AppsFlyer SDK
--mixpanel Search only for Mixpanel SDK
--amplitude Search only for Amplitude SDK
--segment Search only for Segment SDK
--braze Search only for Braze/Appboy SDK
--clevertap Search only for CleverTap SDK
--flurry Search only for Flurry SDK
--generic Search only for generic cross-SDK patterns
--endpoints Search only for known tracker endpoints
--manifest Search only for AndroidManifest.xml markers
--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_FIREBASE=false
SEARCH_ADJUST=false
SEARCH_APPSFLYER=false
SEARCH_MIXPANEL=false
SEARCH_AMPLITUDE=false
SEARCH_SEGMENT=false
SEARCH_BRAZE=false
SEARCH_CLEVERTAP=false
SEARCH_FLURRY=false
SEARCH_GENERIC=false
SEARCH_ENDPOINTS=false
SEARCH_MANIFEST=false
SEARCH_ALL=true
while [[ $# -gt 0 ]]; do
case "$1" in
--firebase) SEARCH_FIREBASE=true; SEARCH_ALL=false; shift ;;
--adjust) SEARCH_ADJUST=true; SEARCH_ALL=false; shift ;;
--appsflyer) SEARCH_APPSFLYER=true; SEARCH_ALL=false; shift ;;
--mixpanel) SEARCH_MIXPANEL=true; SEARCH_ALL=false; shift ;;
--amplitude) SEARCH_AMPLITUDE=true; SEARCH_ALL=false; shift ;;
--segment) SEARCH_SEGMENT=true; SEARCH_ALL=false; shift ;;
--braze) SEARCH_BRAZE=true; SEARCH_ALL=false; shift ;;
--clevertap) SEARCH_CLEVERTAP=true; SEARCH_ALL=false; shift ;;
--flurry) SEARCH_FLURRY=true; SEARCH_ALL=false; shift ;;
--generic) SEARCH_GENERIC=true; SEARCH_ALL=false; shift ;;
--endpoints) SEARCH_ENDPOINTS=true; SEARCH_ALL=false; shift ;;
--manifest) SEARCH_MANIFEST=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
}
run_grep_xml() {
local pattern="$1"
grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
}
# --- Firebase Analytics ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_FIREBASE" == true ]]; then
section "Firebase Analytics — Initialization"
run_grep '(FirebaseAnalytics\.getInstance|FirebaseAnalytics\.newInstance|FirebaseApp\.initializeApp)'
section "Firebase Analytics — Event Logging"
run_grep '(\.logEvent\s*\(|\.setDefaultEventParameters|FirebaseAnalytics\.Event\.|FirebaseAnalytics\.Param\.)'
section "Firebase Analytics — User Identity"
run_grep '(\.setUserId\s*\(|\.setUserProperty\s*\(|\.setAnalyticsCollectionEnabled)'
section "Firebase Analytics — Consent"
run_grep '(\.setConsent\s*\(|ConsentType\.|ConsentStatus\.|setAnalyticsCollectionEnabled\s*\(\s*false)'
fi
# --- Adjust ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_ADJUST" == true ]]; then
section "Adjust — Initialization"
run_grep '(AdjustConfig\s*\(|Adjust\.onCreate\s*\(|Adjust\.create\s*\(|AdjustConfig\.ENVIRONMENT_)'
section "Adjust — Event Tracking"
run_grep '(AdjustEvent\s*\(|Adjust\.trackEvent\s*\(|\.setRevenue\s*\(|\.setCallbackId\s*\()'
section "Adjust — Attribution & User"
run_grep '(\.setOnAttributionChangedListener|AdjustAttribution|Adjust\.addSessionCallbackParameter|Adjust\.getAdid\s*\()'
fi
# --- AppsFlyer ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_APPSFLYER" == true ]]; then
section "AppsFlyer — Initialization"
run_grep '(AppsFlyerLib\.getInstance|AppsFlyerConversionListener|\.init\s*\(.*AF_DEV_KEY|\.start\s*\()'
section "AppsFlyer — Event Tracking"
run_grep '(\.logEvent\s*\(.*AppsFlyerLib|AFInAppEventType\.|AFInAppEventParameterName\.|\.trackEvent\s*\()'
section "AppsFlyer — User Identity"
run_grep '(\.setCustomerUserId\s*\(|\.setCustomerIdAndLogSession|\.getAppsFlyerUID\s*\(|\.setAdditionalData\s*\()'
fi
# --- Mixpanel ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_MIXPANEL" == true ]]; then
section "Mixpanel — Initialization"
run_grep '(MixpanelAPI\.getInstance\s*\(|MixpanelAPI\.init\s*\()'
section "Mixpanel — Event Tracking"
run_grep '(\.track\s*\(|\.trackMap\s*\(|\.timeEvent\s*\(|\.registerSuperProperties\s*\()'
section "Mixpanel — User Identity & Profile"
run_grep '(\.identify\s*\(|\.alias\s*\(|\.getPeople\s*\(|\.getDistinctId\s*\(|\.set\s*\(|\.increment\s*\()'
fi
# --- Amplitude ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_AMPLITUDE" == true ]]; then
section "Amplitude — Initialization"
run_grep '(Amplitude\.getInstance\s*\(|AmplitudeClient|\.initialize\s*\(.*amplitude|amplitude\.init\s*\()'
section "Amplitude — Event Tracking"
run_grep '(\.logEvent\s*\(|\.logRevenue\s*\(|\.setEventProperties|EventOptions\s*\()'
section "Amplitude — User Identity"
run_grep '(\.setUserId\s*\(|\.setUserProperties\s*\(|\.setDeviceId\s*\(|\.setGroup\s*\(|Identify\s*\(\))'
fi
# --- Segment ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_SEGMENT" == true ]]; then
section "Segment — Initialization"
run_grep '(Analytics\.with\s*\(|Analytics\.Builder|Analytics\.setSingletonInstance|com\.segment\.analytics)'
section "Segment — Event Tracking"
run_grep '(\.track\s*\(|\.screen\s*\(|\.group\s*\(|TrackPayload|ScreenPayload)'
section "Segment — User Identity"
run_grep '(\.identify\s*\(|\.alias\s*\(|Traits\s*\(\)|IdentifyPayload)'
fi
# --- Braze (formerly Appboy) ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_BRAZE" == true ]]; then
section "Braze — Initialization"
run_grep '(Braze\.configure\s*\(|BrazeConfig\.Builder|Appboy\.configure\s*\(|AppboyConfig\.Builder)'
section "Braze — Event & Purchase Logging"
run_grep '(\.logCustomEvent\s*\(|\.logPurchase\s*\(|BrazeProperties\s*\(|\.logClick\s*\()'
section "Braze — User Identity"
run_grep '(\.getCurrentUser\s*\(|\.changeUser\s*\(|BrazeUser|\.setEmail\s*\(|\.setCustomUserAttribute\s*\()'
fi
# --- CleverTap ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_CLEVERTAP" == true ]]; then
section "CleverTap — Initialization"
run_grep '(CleverTapAPI\.getDefaultInstance|CleverTapAPI\.changeCredentials|ActivityLifecycleCallback\.register)'
section "CleverTap — Event Tracking"
run_grep '(\.pushEvent\s*\(|\.recordEvent\s*\(|\.pushChargedEvent\s*\()'
section "CleverTap — User Identity & Profile"
run_grep '(\.pushProfile\s*\(|\.onUserLogin\s*\(|\.profilePush\s*\(|\.getCleverTapID\s*\()'
fi
# --- Flurry ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_FLURRY" == true ]]; then
section "Flurry — Initialization"
run_grep '(FlurryAgent\.Builder|FlurryAgent\.init\s*\(|FlurryAgent\.onStartSession\s*\(|\.withLogEnabled\s*\()'
section "Flurry — Event Tracking"
run_grep '(FlurryAgent\.logEvent\s*\(|FlurryAgent\.endTimedEvent|FlurryAgent\.logPayment\s*\()'
section "Flurry — User Identity"
run_grep '(FlurryAgent\.setUserId\s*\(|FlurryAgent\.setAge\s*\(|FlurryAgent\.setGender\s*\()'
fi
# --- Generic Cross-SDK Patterns ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_GENERIC" == true ]]; then
section "Generic — Event Tracking Methods"
run_grep '\.(trackEvent|logEvent|recordEvent|pushEvent|logCustomEvent)\s*\('
section "Generic — User Identification"
run_grep '\.(setUserId|setCustomerUserId|identify|setDistinctId|onUserLogin|changeUser)\s*\('
section "Generic — Analytics Collection Toggle"
run_grep '(setAnalyticsCollectionEnabled|setOptOut|setDataCollectionEnabled|setSendingEnabled|optOut|gdprForgetMe)\s*\('
fi
# --- Known Tracker Endpoints ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_ENDPOINTS" == true ]]; then
section "Known Tracker Endpoints"
run_grep '"https?://[^"]*(app-measurement\.com|firebase-settings\.crashlytics\.com|firebaselogging|google-analytics\.com/collect)'
run_grep '"https?://[^"]*(app\.adjust\.(com|io|world)|cdn\.adjust\.com)'
run_grep '"https?://[^"]*(appsflyer\.com|appsflyersdk\.com|onelink\.me)'
run_grep '"https?://[^"]*(api\.mixpanel\.com|decide\.mixpanel\.com|api-js\.mixpanel\.com)'
run_grep '"https?://[^"]*(api\.amplitude\.com|api2\.amplitude\.com|cdn\.amplitude\.com)'
run_grep '"https?://[^"]*(api\.segment\.(io|com)|cdn-settings\.segment\.com)'
run_grep '"https?://[^"]*(sdk\.iad-[0-9]+\.braze\.com|rest\.iad-[0-9]+\.braze\.com|appboy\.com)'
run_grep '"https?://[^"]*(wzrkt\.com|clevertap-prod\.com|in\.clevertap\.com)'
run_grep '"https?://[^"]*(data\.flurry\.com|flurry\.com/sdk)'
fi
# --- AndroidManifest.xml Markers ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_MANIFEST" == true ]]; then
section "AndroidManifest — Tracker Services & Receivers"
run_grep_xml '(com\.google\.firebase\.analytics|com\.google\.android\.gms\.measurement|com\.google\.firebase\.iid)'
run_grep_xml '(com\.adjust\.|com\.appsflyer\.|com\.mixpanel\.|com\.amplitude\.|com\.segment\.)'
run_grep_xml '(com\.braze\.|com\.appboy\.|com\.clevertap\.|com\.flurry\.)'
section "AndroidManifest — Tracker Permissions"
run_grep_xml '(AD_ID|com\.google\.android\.gms\.permission\.AD_ID|ACCESS_FINE_LOCATION|ACCESS_COARSE_LOCATION|GET_ACCOUNTS)'
section "AndroidManifest — Tracker Meta-data"
run_grep_xml '(com\.google\.firebase\.analytics|firebase_analytics_collection_enabled|google_analytics_adid_collection_enabled)'
run_grep_xml '(ADJUST_|APPSFLYER_|MIXPANEL_|AMPLITUDE_|SEGMENT_|BRAZE_|CLEVERTAP_|FLURRY_)'
fi
echo
echo "=== Search complete ==="