| name | covagent-overcoming-30-curse |
| description | Boost Android app test coverage beyond the 30% activity ceiling using agentic static analysis of Smali code, component transition graphs, activation condition inference, and Frida dynamic instrumentation script generation. Use when: 'increase Android app test coverage', 'generate Frida scripts for unreachable activities', 'analyze APK activation conditions', 'break through coverage barriers in mobile testing', 'instrument Android app for GUI fuzzing', 'find unreachable activities in Android app'. |
CovAgent: Breaking the 30% Activity Coverage Ceiling in Android Testing
This skill enables Claude to act as a two-phase agentic system for Android app test coverage improvement. Following the CovAgent framework, Claude analyzes decompiled APK code (Smali bytecode) and component transition graphs to identify why GUI fuzzers cannot reach certain activities, then generates Frida dynamic instrumentation scripts that satisfy those activation conditions at runtime -- without modifying the app's source code. This approach consistently doubles or triples activity coverage compared to state-of-the-art fuzzers alone.
When to Use
- When a user has an Android APK and wants to understand which activities are unreachable by their GUI fuzzer (Fastbot, APE, Monkey, LLMDroid) and why
- When the user needs Frida scripts to bypass guard conditions (authentication checks, hardware requirements, feature flags) blocking activity transitions
- When analyzing an Android app's component transition graph to map caller-callee relationships between activities
- When a user asks to decompile an APK and inspect Smali code for activation conditions that gate activity launches
- When the user wants to inject navigation widgets into a running app to reach isolated activities during fuzz testing
- When investigating why test coverage plateaus below 30-40% on a real-world Android app
Key Technique
The 30% Problem. Standard GUI fuzzers explore Android apps by generating random or guided UI events. They consistently plateau at ~30% activity coverage because many activities are gated by activation conditions -- programmatic checks in onCreate/onStart/onResume that terminate the activity if prerequisites are unmet. These include: Intent extras with specific values, SharedPreferences flags set by prior workflows, external resource checks (SD card, network, GPS), authentication tokens, and server-side state. No amount of random clicking can satisfy a if (!hasCompletedPayment) finish() guard.
Two-Agent Architecture. CovAgent uses an Analyzer Agent and an Instrumenter Agent in sequence. The Analyzer performs bidirectional static analysis on decompiled Smali code: forward analysis inspects lifecycle methods (onCreate, onResume) to find guard conditions and exception-raising sub-procedures; backward analysis traces startActivity/startActivityForResult call sites to understand how Intents are constructed and what extras they carry. The agent reasons via chain-of-thought about what conditions must hold for the activity to survive initialization. The Instrumenter Agent then takes these inferred conditions and generates Frida JavaScript scripts that hook target methods in memory -- overriding return values, injecting Intent extras, or faking device state -- so the activity launches successfully without any source code changes.
Fuzzer-Agnostic Integration. The generated Frida scripts are injected via a pop-up widget overlay: for each unreachable target activity, a button is added to its nearest reachable source activity (determined from the component transition graph). The GUI fuzzer runs concurrently and can tap these buttons to trigger instrumented transitions. This preserves realistic navigation paths while making previously impossible transitions accessible.
Step-by-Step Workflow
-
Decompile the APK to Smali. Use apktool d target.apk -o output_dir to obtain Smali bytecode and AndroidManifest.xml. Parse the manifest to extract the full list of declared activities, their intent filters, and exported status.
-
Build the Component Transition Graph (CTG). Run ICCBot or a similar ICC analysis tool to extract caller-callee relationships between components. Alternatively, grep Smali code for startActivity, startActivityForResult, and startService invocations to build a lightweight CTG mapping source activities to target activities and the methods that trigger transitions.
-
Identify unreachable activities. Run the baseline fuzzer for a fixed duration and collect its activity coverage log. Diff against the manifest's declared activities to produce the set of unreachable activities. Prioritize activities with no incoming edges in the CTG (isolated nodes) and those with complex activation conditions.
-
Forward-analyze lifecycle methods for guard conditions. For each unreachable activity, read its onCreate, onStart, and onResume Smali methods. Identify conditional branches (if-eqz, if-nez, if-eq) that lead to finish(), System.exit(), or exception throws. Trace the checked variables backward to their source: Intent extras (getStringExtra, getIntExtra), SharedPreferences reads, permission checks, hardware state queries, or network calls.
-
Backward-analyze Intent construction at call sites. For each unreachable activity, find all startActivity calls targeting it (from the CTG). Read the calling method's Smali to determine what extras are putExtra'd into the Intent, what conditions gate the startActivity call itself, and what preceding user flows populate required state.
-
Infer activation conditions in natural language. Synthesize findings into a structured description: (a) what the activity expects in its Intent bundle, (b) what SharedPreferences or global state must exist, (c) what external resources or device features are checked, (d) what authentication or authorization is required. Classify each condition by type: data dependency, external resource, device state, authentication, or usage pattern.
-
Generate a Frida instrumentation script. Write a Frida JavaScript snippet that hooks the identified guard methods and overrides their behavior. The script should: hook the class containing the guard, replace the return value of the check method (e.g., force isSDCardConnected() to return true), inject required Intent extras via Intent.putExtra hooks, and then trigger navigation to the target activity via startActivity. Wrap everything in Java.perform() with proper error handling.
-
Validate the script with a feedback loop (up to 5 iterations). Inject the script using frida -U -f <package> -l script.js. Check three failure modes: (a) Frida runtime errors (class/method not found -- fix the hook target), (b) app crashes with stack traces (unhandled null or state issue -- add more hooks), (c) silent failure to transition (wrong condition identified -- revisit analysis). Refine the script based on logcat and Frida console output.
-
Build the widget injection map. Using the CTG, assign each unreachable target to its nearest reachable source activity. If no reachable source exists, assign it to the app's main/launcher activity. Generate a Frida script that injects a floating button overlay into each source activity; tapping the button executes the target's instrumentation script.
-
Run the augmented fuzzing session. Launch the GUI fuzzer concurrently with Frida instrumentation active. The fuzzer explores normally but can now also tap injected buttons to reach previously unreachable activities. Collect coverage metrics (activity, class, method, line) and compare against the baseline.
Concrete Examples
Example 1: Bypassing an SD Card Check
User: "My Fastbot run only covers 18% of activities in this file manager app. The FileTransferActivity never gets reached. Here's the decompiled Smali for its onCreate."
Approach:
- Read the
onCreate Smali. Find the guard: invoke-virtual {v0}, Lcom/app/util/DeviceUtils;->isSDCardMounted()Z followed by if-eqz v1, :cond_finish leading to invoke-virtual {p0}, Landroid/app/Activity;->finish()V.
- Backward-analyze: the CTG shows
SettingsActivity.onItemClick calls startActivity with Intent("com.app.FILE_TRANSFER") and putExtra("source_path", selectedPath).
- Infer activation conditions: (a) SD card must appear mounted, (b) Intent must carry
source_path string extra.
- Generate Frida script:
Java.perform(function() {
var DeviceUtils = Java.use("com.app.util.DeviceUtils");
DeviceUtils.isSDCardMounted.implementation = function() {
console.log("[*] Hooked isSDCardMounted -> true");
return true;
};
var Activity = Java.use("android.app.Activity");
var Intent = Java.use("android.content.Intent");
var currentApp = Java.use("android.app.ActivityThread")
.currentApplication().getApplicationContext();
var intent = Intent.$new(currentApp,
Java.use("com.app.FileTransferActivity").class);
intent.putExtra("source_path", "/sdcard/Documents");
intent.addFlags(0x10000000);
currentApp.startActivity(intent);
});
- Validate: Frida loads cleanly,
FileTransferActivity opens without crash. Coverage now includes 3 additional activities reachable from FileTransferActivity.
Example 2: Bypassing Authentication Gate for Order History
User: "The OrderHistoryActivity in this food delivery app is unreachable during testing. How can I instrument past the login check?"
Approach:
- Forward-analyze
onCreate: finds invoke-virtual {v0}, Lcom/app/auth/SessionManager;->isLoggedIn()Z with if-eqz branching to finish(). Also checks getIntent().getStringExtra("user_id") with null check.
- Backward-analyze:
HomeActivity.onOrderHistoryClick constructs Intent with putExtra("user_id", currentUser.getId()).
- Activation conditions: (a)
SessionManager.isLoggedIn() must return true, (b) Intent must contain non-null user_id extra.
Java.perform(function() {
var SessionManager = Java.use("com.app.auth.SessionManager");
SessionManager.isLoggedIn.implementation = function() {
console.log("[*] Hooked isLoggedIn -> true");
return true;
};
SessionManager.getToken.implementation = function() {
return "instrumented_test_token_001";
};
var ctx = Java.use("android.app.ActivityThread")
.currentApplication().getApplicationContext();
var Intent = Java.use("android.content.Intent");
var intent = Intent.$new(ctx,
Java.use("com.app.OrderHistoryActivity").class);
intent.putExtra("user_id", "test_user_42");
intent.addFlags(0x10000000);
ctx.startActivity(intent);
});
Example 3: Analyzing Activation Conditions from Smali Without Script Generation
User: "Can you analyze this Smali code and tell me why PromoActivity is unreachable?"
Approach:
- Read the
onCreate method. Identify three guards:
SharedPreferences.getBoolean("has_first_purchase", false) -- must be true
getIntent().getLongExtra("promo_expiry", 0) compared against System.currentTimeMillis() -- promo must not be expired
invoke-static {}, Lcom/app/FeatureFlags;->isPromoEnabled()Z -- feature flag must be on
- Report structured findings:
Activation Conditions for PromoActivity:
1. [Data Dependency] SharedPreferences key "has_first_purchase" must be true
-> Set during CheckoutActivity.onPaymentSuccess()
2. [Data Dependency] Intent extra "promo_expiry" (long) must be > current time
-> Constructed in HomeActivity.showPromoCard() with server-provided timestamp
3. [Feature Flag] FeatureFlags.isPromoEnabled() must return true
-> Reads from remote config; defaults to false
- Recommend hooking all three: override
SharedPreferences.getBoolean for the specific key, set promo_expiry to a future timestamp, and hook isPromoEnabled to return true.
Best Practices
- Do: Start with the AndroidManifest to enumerate all declared activities before analyzing code. This gives you the ground truth denominator for coverage metrics.
- Do: Use the CTG to preserve realistic navigation paths. Assign instrumented transitions to the nearest reachable source activity rather than always injecting into the launcher.
- Do: Hook at the narrowest scope possible. Override a single method's return value rather than replacing entire class implementations. This minimizes side effects on downstream app behavior.
- Do: Include
try/catch blocks in every Frida script and log errors to console. Silent failures are the hardest to debug.
- Avoid: Generating scripts that modify persistent state (databases, files on disk). Frida hooks should operate in-memory only to keep tests reproducible.
- Avoid: Assuming a single guard condition per activity. Real apps often stack 2-4 checks in
onCreate alone. Analyze the entire method before scripting.
- Avoid: Skipping the backward analysis phase. Understanding how the calling activity constructs the Intent is often more informative than reading the target's
onCreate alone.
Error Handling
| Failure Mode | Symptom | Resolution |
|---|
| Class not found in Frida | Error: java.lang.ClassNotFoundException | The class may be obfuscated or loaded dynamically. Check the actual class name in the running process with Java.enumerateLoadedClasses(). |
| Method signature mismatch | Error: method not found | Smali uses JNI-style signatures. Verify parameter types match exactly: overload('java.lang.String', 'int'). |
| App crashes after hook | NullPointerException in logcat | The hooked method's caller may depend on side effects. Hook additional downstream methods that expect real return objects. |
| Activity launches but immediately finishes | No error, activity disappears | There are additional guard conditions in onStart or onResume not caught in onCreate analysis. Analyze all lifecycle methods. |
| Frida detaches from process | Process terminated | The app may have anti-instrumentation (root detection, Frida detection). Hook common detection methods like detectFrida() or isRooted() first. |
Limitations
- Obfuscated code (ProGuard/R8) makes Smali analysis significantly harder. Class and method names become meaningless single letters, requiring pattern matching on method signatures and call structures instead of names.
- Server-side gating cannot be bypassed by client-side instrumentation alone. If an activity requires a valid server response (e.g., payment confirmation), the Frida script can only fake the client's perception of it, which may cause inconsistent downstream state.
- Dynamic class loading (DexClassLoader, multidex) means some classes are not visible in the initial APK decompilation. These require runtime enumeration.
- Native code guards (JNI/NDK) are outside Frida's Java-layer hooking. Bypassing native checks requires Interceptor-based hooks on
.so libraries, which is a different instrumentation paradigm.
- This approach augments but does not replace GUI fuzzers. It specifically addresses the reachability problem, not the interaction depth problem within reached activities.
Reference
Paper: Wei Minn et al., "CovAgent: Overcoming the 30% Curse of Mobile Application Coverage with Agentic AI and Dynamic Instrumentation" (2026). arXiv:2601.21253v1
What to look for: Section 3 for the full two-agent architecture, Algorithm 1 for the widget injection mapping strategy, Section 4.2 (RQ1) for the coverage improvement breakdown by fuzzer, and Section 4.4 (RQ3) for the activation condition taxonomy with per-category success rates showing where instrumentation works best (data dependencies, feature flags) and where it struggles (external server state).