From c7678f8d1df30e44a934e8a2412d38edfb4d5870 Mon Sep 17 00:00:00 2001 From: Illhm <194127535+Illhm@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:23:21 +0000 Subject: [PATCH 1/2] feat: Implement Hook Discovery and WebView Request Blocking - Added `HookDiscovery` to actively log unidentified view elements (bypassing specific frameworks) via the `addView` method when the module operates in discovery mode, reducing logcat spam using concurrent sets. - Created `WebViewRequestBlocking` extending `shouldInterceptRequest` across multiple android API implementations. It safely cancels requests and produces an empty web resource response when matching ad patterns. - Handled hook exceptions silently in the custom module files so they will absolutely never crash the target application. - Added comprehensive documentation `docs/HOOK_DISCOVERY.md`. --- .../tw/fatminmin/xposed/minminguard/Main.java | 7 ++ .../minminguard/blocker/HookDiscovery.java | 80 +++++++++++++ .../minminguard/blocker/NameBlocking.java | 4 +- .../blocker/WebViewRequestBlocking.java | 111 ++++++++++++++++++ docs/HOOK_DISCOVERY.md | 68 +++++++++++ 5 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/HookDiscovery.java create mode 100644 app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/WebViewRequestBlocking.java create mode 100644 docs/HOOK_DISCOVERY.md diff --git a/app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java b/app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java index 57fe530..6c84c87 100644 --- a/app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java +++ b/app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java @@ -22,9 +22,11 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; import tw.fatminmin.xposed.minminguard.blocker.ApiBlocking; import tw.fatminmin.xposed.minminguard.blocker.Blocker; +import tw.fatminmin.xposed.minminguard.blocker.HookDiscovery; import tw.fatminmin.xposed.minminguard.blocker.NameBlocking; import tw.fatminmin.xposed.minminguard.blocker.UrlFiltering; import tw.fatminmin.xposed.minminguard.blocker.Util; +import tw.fatminmin.xposed.minminguard.blocker.WebViewRequestBlocking; import tw.fatminmin.xposed.minminguard.blocker.adnetwork.Ad2iction; import tw.fatminmin.xposed.minminguard.blocker.adnetwork.AdMarvel; import tw.fatminmin.xposed.minminguard.blocker.adnetwork.Adbert; @@ -299,8 +301,13 @@ protected void afterHookedMethod(MethodHookParam param) if (pref.getBoolean(packageName + "_url", false)) { UrlFiltering.removeWebViewAds(packageName, lpparam); + WebViewRequestBlocking.handle(packageName, lpparam); } } + + if (pref != null && pref.getBoolean(packageName + "_discovery", false)) { + HookDiscovery.enableDiscovery(packageName, lpparam); + } } }); } catch (Throwable t) { diff --git a/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/HookDiscovery.java b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/HookDiscovery.java new file mode 100644 index 0000000..aca24f0 --- /dev/null +++ b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/HookDiscovery.java @@ -0,0 +1,80 @@ +package tw.fatminmin.xposed.minminguard.blocker; + +import android.view.View; +import android.view.ViewGroup; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; + +public class HookDiscovery { + + private static final Set loggedClasses = Collections.synchronizedSet(new HashSet()); + + public static void enableDiscovery(final String packageName, final LoadPackageParam lpparam) { + if (lpparam == null || lpparam.classLoader == null || packageName == null) return; + + try { + Util.hookAllMethods("android.view.ViewGroup", lpparam.classLoader, "addView", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (param.args == null || param.args.length == 0 || param.args[0] == null) return; + + try { + View child = (View) param.args[0]; + ViewGroup parent = (ViewGroup) param.thisObject; + + if (child == null || parent == null) return; + + String childClass = child.getClass().getName(); + String parentClass = parent.getClass().getName(); + + if (isSuspicious(childClass)) { + logDiscovery(packageName, "Suspicious child view added", childClass); + } + if (isSuspicious(parentClass)) { + logDiscovery(packageName, "Suspicious parent view container", parentClass); + } + + } catch (Throwable t) { + // ignore errors during discovery to prevent crashing the target app + } + } + }); + Util.log("HookDiscovery", "Enabled discovery mode for " + packageName); + } catch (Throwable t) { + Util.log("HookDiscovery", "Failed to hook addView for discovery in " + packageName + ": " + t.getMessage()); + } + } + + private static boolean isSuspicious(String className) { + if (className == null) return false; + + // Filter out safe core android namespaces + if (className.startsWith("android.") || + className.startsWith("androidx.") || + className.startsWith("com.google.android.material.")) { + return false; + } + + String lowerClass = className.toLowerCase(); + return lowerClass.contains("ad") || + lowerClass.contains("ads") || + lowerClass.contains("banner") || + lowerClass.contains("native") || + lowerClass.contains("interstitial") || + lowerClass.contains("reward") || + lowerClass.contains("splash"); + } + + private static void logDiscovery(String packageName, String message, String className) { + String key = packageName + ":" + className; + if (!loggedClasses.contains(key)) { + loggedClasses.add(key); + Util.log("HookDiscovery", "[" + packageName + "] " + message + ": " + className); + } + } +} diff --git a/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/NameBlocking.java b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/NameBlocking.java index 78216de..ce2f7a8 100644 --- a/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/NameBlocking.java +++ b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/NameBlocking.java @@ -8,7 +8,7 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage; import tw.fatminmin.xposed.minminguard.Main; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.Map; /** @@ -16,7 +16,7 @@ */ public final class NameBlocking { - private static Map cache = new HashMap<>(); + private static Map cache = new ConcurrentHashMap<>(); private static boolean matchBannerName(String clazzName, String banner, String bannerPrefix) { diff --git a/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/WebViewRequestBlocking.java b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/WebViewRequestBlocking.java new file mode 100644 index 0000000..669c259 --- /dev/null +++ b/app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/WebViewRequestBlocking.java @@ -0,0 +1,111 @@ +package tw.fatminmin.xposed.minminguard.blocker; + +import android.net.Uri; +import android.os.Build; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; + +import java.io.ByteArrayInputStream; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; +import tw.fatminmin.xposed.minminguard.Main; + +public class WebViewRequestBlocking { + + public static void handle(final String packageName, final LoadPackageParam lpparam) { + if (lpparam == null || lpparam.classLoader == null || packageName == null) return; + + try { + Class webViewClientClass = XposedHelpers.findClassIfExists("android.webkit.WebViewClient", lpparam.classLoader); + if (webViewClientClass == null) return; + + // Hook for older APIs (API < 21) + XposedBridge.hookAllMethods(webViewClientClass, "shouldInterceptRequest", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + try { + if (param.args == null || param.args.length < 2) return; + + // the signature is shouldInterceptRequest(WebView view, String url) + if (param.args[1] instanceof String) { + String url = (String) param.args[1]; + if (shouldBlock(url)) { + param.setResult(createEmptyResponse()); + } + } + } catch (Throwable t) { + // ignore + } + } + }); + + // Hook for newer APIs (API >= 21) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + XposedBridge.hookAllMethods(webViewClientClass, "shouldInterceptRequest", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + try { + if (param.args == null || param.args.length < 2) return; + + // the signature is shouldInterceptRequest(WebView view, WebResourceRequest request) + if (param.args[1] instanceof WebResourceRequest) { + WebResourceRequest request = (WebResourceRequest) param.args[1]; + Uri uri = request.getUrl(); + if (uri != null) { + String url = uri.toString(); + if (shouldBlock(url)) { + param.setResult(createEmptyResponse()); + } + } + } + } catch (Throwable t) { + // ignore + } + } + }); + } + + Util.log("WebViewRequestBlocking", "Enabled WebView request interception for " + packageName); + + } catch (Throwable t) { + Util.log("WebViewRequestBlocking", "Failed to hook WebViewClient in " + packageName + ": " + t.getMessage()); + } + } + + private static boolean shouldBlock(String url) { + if (url == null) return false; + + try { + Uri uri = Uri.parse(url); + String host = uri.getHost(); + if (host != null) { + for (String adUrl : Main.patterns) { + if (host.contains(adUrl) || url.contains(adUrl)) { + return true; + } + } + } else { + for (String adUrl : Main.patterns) { + if (url.contains(adUrl)) { + return true; + } + } + } + } catch (Throwable t) { + // fallback if URI parse fails + for (String adUrl : Main.patterns) { + if (url.contains(adUrl)) { + return true; + } + } + } + return false; + } + + private static WebResourceResponse createEmptyResponse() { + return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream("".getBytes())); + } +} diff --git a/docs/HOOK_DISCOVERY.md b/docs/HOOK_DISCOVERY.md new file mode 100644 index 0000000..0d075a8 --- /dev/null +++ b/docs/HOOK_DISCOVERY.md @@ -0,0 +1,68 @@ +# Hook Discovery Guide + +This document explains how to use MinMinGuard's Hook Discovery feature to find and block unsupported ad networks. + +## 1. Enabling Discovery Mode + +To enable discovery mode for a specific app: +1. Ensure the app is checked in the MinMinGuard UI. +2. Manually add a boolean flag `[package_name]_discovery` set to `true` in MinMinGuard's `shared_prefs` XML file. +3. Restart the target app. + +## 2. Reading Logcat + +Once enabled, MinMinGuard will log suspicious class names when they are added to the view hierarchy (`ViewGroup.addView`). + +Run the following command via ADB to see the discovery logs: +```bash +adb logcat | grep HookDiscovery +``` + +You should see output similar to: +``` +[HookDiscovery] [com.example.app] Suspicious child view added: com.newadnetwork.ads.BannerView +``` + +## 3. Creating a New Blocker + +Once you have identified the package or class prefix of the new ad network, create a new `Blocker` subclass in `app/src/main/java/tw/fatminmin/xposed/minminguard/blocker/adnetwork/`. + +Example `NewAdNetwork.java`: +```java +package tw.fatminmin.xposed.minminguard.blocker.adnetwork; + +import tw.fatminmin.xposed.minminguard.blocker.Blocker; + +public class NewAdNetwork extends Blocker { + @Override + public String getBanner() { + return null; // Exact class name + } + + @Override + public String getBannerPrefix() { + return "com.newadnetwork.ads."; // Prefix to block + } +} +``` + +## 4. Registering the Blocker + +Open `app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java`. +Import your new class and add it to the `blockers` array: + +```java +import tw.fatminmin.xposed.minminguard.blocker.adnetwork.NewAdNetwork; + +public static Blocker[] blockers = { + // ... existing blockers ... + new NewAdNetwork(), +}; +``` + +## 5. Testing with Debug APK + +1. Build a debug APK with `./gradlew clean :app:assembleDebug`. +2. Install the new APK on your device. +3. Verify that the new ad network views are now blocked successfully and spaces are removed. +4. Disable discovery mode in preferences when done testing. From 734e428c573caf0313559284c96af5ee071d4469 Mon Sep 17 00:00:00 2001 From: Illhm Date: Sun, 7 Jun 2026 08:42:36 +0700 Subject: [PATCH 2/2] Delete .github/workflows/android-ci.yml --- .github/workflows/android-ci.yml | 61 -------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 .github/workflows/android-ci.yml diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml deleted file mode 100644 index a04f256..0000000 --- a/.github/workflows/android-ci.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Android Legacy CI - -on: - push: - branches: - - master - - main - pull_request: - branches: - - master - - main - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - name: Build debug APK - runs-on: ubuntu-latest - - env: - ANDROID_SDK_ROOT: ${{ github.workspace }}/android-sdk - ANDROID_HOME: ${{ github.workspace }}/android-sdk - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up JDK 8 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "8" - cache: gradle - - - name: Install Android SDK - run: | - mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" - cd "$ANDROID_SDK_ROOT/cmdline-tools" - curl -fsSL -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip - unzip -q cmdline-tools.zip - mv cmdline-tools latest - - yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses - "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" \ - "platform-tools" \ - "platforms;android-29" \ - "build-tools;29.0.0" - - - name: Make Gradle executable - run: chmod +x ./gradlew - - - name: Build debug - run: ./gradlew clean :app:assembleDebug --no-daemon --stacktrace - - - name: Upload debug APK - uses: actions/upload-artifact@v4 - with: - name: MinMinGuard-debug-apk - path: app/build/outputs/apk/debug/*.apk