An LSPosed/Xposed module for com.facebook.katana that removes ads using structural DexKit discovery plus guarded, version-specific fast paths.
Current target: Facebook 576.0.0.42.73, module 1.8. Older versions (571 and below) are no longer supported.
- News Feed sponsored units
- Story ads and in-disc story ads
- Reels / upstream ad-backed story append paths
- Quicksilver game ad requests
- Audience Network and Neko playable ad activities used by games
- Obfuscated names such as
AiD,A84,A8t,ADF, orAueare too unstable to hardcode. They changed across builds and caused broken hooks. - Stable strings and structural signatures are much more reliable than direct obfuscated names.
- Feed ads are inserted at multiple layers. Blocking only one layer is not enough.
- The main News Feed request is a mixed GraphQL payload containing organic and sponsored units. Blocking its host or request would also block the organic feed.
- The earliest safe client boundary found so far is the dedicated story-ad store layer identified by
AdsPaginatingNetworkAdBucketFetcher,FbStoryAdInDiscStoreImpl,IN_DISC_METADATA_KEY, andAD_BUCKETS_KEY. The module blocks fetch, merge, deferred-update, and insertion methods there before ad units enter feed pools. The telemetry labelsads_deletion/ads_insertionare deliberately NOT used as class selectors anymore: unrelated story viewer classes log those labels, and hooking them blanks the story viewer (576'sX.BAlwas the story viewer's ownonDataChangedhandler). - Game ads are not a single pipeline either. Quicksilver request hooks, postMessage hooks, and UI activity fallbacks all matter.
- Blocking
AudienceNetworkActivityatstartActivity(...)was too early and caused game hangs. Letting it launch and closing it immediately from activity lifecycle hooks worked better.
- Resolve classes with DexKit using stable strings and method shapes.
- Resolve every matching story-ad provider instead of assuming one provider class; Facebook may split this pipeline between releases.
- Install feed, Reels, and game hooks independently so a changed Reels target cannot prevent feed-source filtering from loading.
- Remove ad-backed stories from the upstream list builder append path.
- Sanitize feed CSR filter inputs and outputs.
- Sanitize late-stage feed lists before they reach rendering.
- Block sponsored entries from the sponsored pool and story pool.
- Block story ad providers by intercepting merge/fetch/update style methods.
- Keep marker-based view removal as a last-resort safety net, not the primary News Feed path.
- The feed component pair (576: wrapper
X.2q8, componentX.2q4) is discovered by Litho component name, not by obfuscated class name. Litho generated components pass a stable spec name to their base class constructor —"NewsFeedFeedUnitComponent"for the feed unit component and"LoggingComponent"for the generic wrapper Litho renders feed units through — and those strings survive Facebook's obfuscator. The class-load notifier reads the name reflectively (the generated base stores it in a final String field filled by a String constructor; the class is instantiated through its no-arg constructor to read it), and the full DexKit pass finds the same classes with an exactusingStringsmatch as a backstop. - The cached initial News Feed (including a sponsored slot) assembles and renders within ~2s of a cold start — before any DexKit scan can finish. To win that race, the discovered guard pair is persisted in the host's
cacheDirkeyed by the Facebook version, and later launches load it right afterApplication.attach. The cached class names still failClass.forNameat attach time (the secondary dex is not configured yet), so every timed guard attempt re-tries registering them; the guard then installs ~200ms after attach, before the cached feed renders. This is what removes the "second feed item is a sponsored post" on force-close/reopen. A Facebook update changes the version key and falls back to the DexKit discovery, which then rewrites the cache. - The wrapper renders via
A1Fonly (noA1H), so the guard matches Litho layout entry points by shape — instance methods taking the Litho context (X.3Qp) with a non-primitive return — instead of requiring a method literally namedA1H. Static builder factories with the same shape are excluded. - The edge and wrapper-child fields are resolved structurally: the component's edge field is the one whose type is (or implements) the feed-item contract exposing
GraphQLFeedStoryCategory(576:X.3yVviaB9B()); the wrapper's child field is the one assignable to the component class. StoryAdsInDiscno longer exists anywhere in 576, and the story ad store moved toX.BEC. See the selector change above.- The feed-item contract hooks (
X.3YX/X.3Xkon 576) and the CSR/network/pool hooks all resolve structurally via DexKit (X.21rCSR filters,X.21esponsored pool,X.BECstory ad store, late feed list hooks). The hardcoded 571 contract-class hints (X.3YX/X.3Xk), the Audience Network listener names (X.mGv/X.mGo), the Quicksilver handler name (X.edO), and theX.2Jyfeed-object hint were removed entirely — the inspector and edge-field resolution work structurally (GraphQL edge class name,GraphQLFeedUnitEdge/GraphQL+Feedname matching, feed-story-category enum constants), and the AN reward no longer depends on them since the webview-delivery rewrite delivers the reward. - The 571 hardcoded fast paths (
X.21p.Ani,X.1fM.A0B,X.21O.A03,X.2mm.A3F,X.1vr.addNewEdgeToCollection, theX.9xH-style curated story-ad class list) were removed: they were all dead on 576, and the curated list even matched a network-connectivity helper (X.9xH) whose shape coincidentally fit the deferred-update rule. The seeded component guard seeds (X.2q4/X.2q8) were removed with the Litho-name discovery above. - The global
addViewsafety-net hook must never callView.createAccessibilityNodeInfo()on freshly added views. On 576, building the accessibility node mid-mount runs Facebook's custom-view accessibility code with side effects, and page-profile header text ("Sign up", "Followers", "posts") ends up blank after pull-to-refresh.collectViewMarkerTextstherefore reads onlycontentDescriptionandtext.
Facebook 571 stores most application bytecode in 18 Superpack secondary dex files. The small libraries visible directly in the APK, including libfbunwindstack.so, are not the feed-ad source. Networking may ultimately use native transports, but host-level blocking is too coarse because feed ads share the normal GraphQL request.
The preferred interception point is therefore after GraphQL data has been decoded but before dedicated ad providers merge it into the feed. Native or KernelSU hooks should only be considered if runtime logs show that the ads_deletion / ads_insertion provider hooks no longer resolve or fire.
- Resolve Quicksilver ad request methods by their stable JSON error strings.
- Hook the Quicksilver
postMessage(String, String)bridge as a second request-layer fallback. - The runtime delegate the game webview actually uses (576:
X.q10) is NOT the DexKit-discovered service delegate (X.gJA); it is caught at registration by hookingWebView.addJavascriptInterface. It is a thin delegate with no promise-resolve helper on its class, so request payloads can only be snapshotted there, not resolved. - The promise result is delivered back into the webview as
evaluateJavascript("e = new Event('message');e.data = {...};window.dispatchEvent(e);"). The module rewrites that JSON in place: for rewarded requests (getrewardedvideoasync/getrewardedinterstitialasync, andshowadasyncwith a rewarded ad instance) error fields are dropped andsuccess/completed/didComplete/watched/rewarded+completionGesture:"post"are forced, so the game grants the reward with no ad shown. The rewrite also coversloadUrlandpostWebMessagedeliveries. Note the envelopetypestays"rejectpromise"on the rewrittenshowadasyncresponses — the game reads the outcome fields indata, and converting the envelope is unnecessary. - Close
AudienceNetworkActivity,AudienceNetworkRemoteActivity, andNekoPlayableAdActivityfrom lifecycle hooks as UI-level fallbacks. - Only hard-block the playable activity launch path directly; Audience Network activity launches are allowed so their internal close/error flow can run before the activity is closed.
- Runtime logs are debug-only.
Patches.ktandModule.javanow gate logging behindBuildConfig.DEBUG.- Release builds should stay quiet unless you re-enable logging yourself.
The startup line:
DexKit groups: ... feedCsr=0 ...
is normal in the current implementation.
That number is only the result of the initial batch string-group search. Feed CSR hooks are also resolved by later structural and fallback matchers, so feedCsr=0 does not mean feed CSR filtering is disabled.
The line that actually matters is:
Resolved feed CSR filters=...
If that later line contains resolved classes, the CSR filtering path is active even when the earlier batch count is zero.
- Android app module:
app - Host package:
com.facebook.katana - Application ID:
tn.loukious.facebookappadsremover
Build the debug APK with:
./gradlew :app:assembleDebug- Prefer stable strings, type signatures, and runtime structure over obfuscated identifiers.
- Keep debug instrumentation available in debug builds only.
- Treat feed, story, and game ads as separate pipelines with separate fallbacks.