Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 0 additions & 61 deletions .github/workflows/android-ci.yml

This file was deleted.

7 changes: 7 additions & 0 deletions app/src/main/java/tw/fatminmin/xposed/minminguard/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> loggedClasses = Collections.synchronizedSet(new HashSet<String>());

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
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;

/**
* Created by fatminmin on 2015/10/27.
*/
public final class NameBlocking
{
private static Map<String, Boolean> cache = new HashMap<>();
private static Map<String, Boolean> cache = new ConcurrentHashMap<>();

private static boolean matchBannerName(String clazzName, String banner, String bannerPrefix)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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()));
}
}
68 changes: 68 additions & 0 deletions docs/HOOK_DISCOVERY.md
Original file line number Diff line number Diff line change
@@ -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.
Loading