├── app
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ └── xposed_init
│ │ ├── res
│ │ └── values
│ │ │ ├── strings.xml
│ │ │ └── arrays.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── io
│ │ └── github
│ │ └── a13e300
│ │ └── tools
│ │ └── ifo
│ │ ├── Logger.java
│ │ ├── IFOConfig.java
│ │ ├── Utils.java
│ │ ├── XposedEntry.java
│ │ └── IFOManager.java
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | io.github.a13e300.tools.ifo.XposedEntry
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/5ec1cff/Intent-Filter-Overrider/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ifo
3 | Intent Filter Overlay
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | keystore.properties
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - android
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 28 12:14:51 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven("https://api.xposed.info/")
14 | }
15 | }
16 | rootProject.name = "ifo"
17 | include(":app")
18 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
11 |
14 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keep class io.github.a13e300.tools.ifo.XposedEntry
24 |
25 | -repackageclasses
26 | -allowaccessmodification
27 | -overloadaggressively
28 | -renamesourcefileattribute
29 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/a13e300/tools/ifo/Logger.java:
--------------------------------------------------------------------------------
1 | package io.github.a13e300.tools.ifo;
2 |
3 | import de.robv.android.xposed.XposedBridge;
4 |
5 | public class Logger {
6 | private static final String TAG = "IFO";
7 |
8 | public static void i(String text) {
9 | XposedBridge.log("[" + TAG + "][I] " + text);
10 | // Log.d(TAG, text);
11 | }
12 |
13 | public static void d(String text) {
14 | XposedBridge.log("[" + TAG + "][D] " + text);
15 | // Log.d(TAG, text);
16 | }
17 |
18 | public static void e(String text, Throwable t) {
19 | XposedBridge.log("[" + TAG + "][E] " + text);
20 | XposedBridge.log(t);
21 | // Log.e(TAG, text, t);
22 | }
23 |
24 | public static void e(String text) {
25 | XposedBridge.log("[" + TAG + "][E] " + text);
26 | // Log.e(TAG, text);
27 | }
28 |
29 | public static void w(String text) {
30 | XposedBridge.log("[" + TAG + "][W] " + text);
31 | // Log.w(TAG, text);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
22 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.Properties
3 |
4 | plugins {
5 | id("com.android.application")
6 | }
7 |
8 | val keystorePropertiesFile: File = rootProject.file("keystore.properties")
9 | val keystoreProperties = if (keystorePropertiesFile.exists() && keystorePropertiesFile.isFile) {
10 | Properties().apply {
11 | load(FileInputStream(keystorePropertiesFile))
12 | }
13 | } else null
14 |
15 | android {
16 | namespace = "io.github.a13e300.tools.ifo"
17 | compileSdk = 34
18 |
19 | defaultConfig {
20 | applicationId = "io.github.a13e300.tools.ifo"
21 | minSdk = 30
22 | targetSdk = 34
23 | versionCode = 1
24 | versionName = "1.0"
25 | setProperty("archivesBaseName", "IFO-$versionName-$versionCode")
26 | }
27 |
28 | signingConfigs {
29 | if (keystoreProperties != null) {
30 | create("release") {
31 | keyAlias = keystoreProperties["keyAlias"] as String
32 | keyPassword = keystoreProperties["keyPassword"] as String
33 | storeFile = file(keystoreProperties["storeFile"] as String)
34 | storePassword = keystoreProperties["storePassword"] as String
35 | }
36 | }
37 | }
38 |
39 | buildTypes {
40 | release {
41 | // We don't need to do R8 since it is a really small module
42 | isMinifyEnabled = false // true
43 | // isShrinkResources = true
44 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
45 | val releaseSig = signingConfigs.findByName("release")
46 | signingConfig = if (releaseSig != null) releaseSig else {
47 | println("use debug signing config")
48 | signingConfigs["debug"]
49 | }
50 | }
51 | }
52 | compileOptions {
53 | sourceCompatibility = JavaVersion.VERSION_11
54 | targetCompatibility = JavaVersion.VERSION_11
55 | }
56 | }
57 |
58 | dependencies {
59 | compileOnly("de.robv.android.xposed:api:82")
60 | compileOnly("androidx.annotation:annotation:1.8.1")
61 | }
62 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 意图过滤器重载
2 |
3 | 修改应用 Activity 的 Intent-Filter 的 Xposed 模块
4 |
5 | **仅支持 Android 11-14**
6 |
7 | ## 说明
8 |
9 | 1. 支持移除本身的 intent filter 和增加新的 intent-filter 。
10 | 2. 你可以通过 XML 配置,类似 [IFW](https://bbs.letitfly.me/d/395) 。XML 配置文件目录位于 `/data/system/ifo` ,你可以放置多个 XML 配置文件。
11 | 3. XML 的根标签是 `` ,包含若干子标签 `` ,attribute name 指定 Activity 组件名(被 `ComponentName.unflatterFromString` 处理)
12 | 4. 每个 activity 标签可以包含若干 `add` 或 `remove` 标签,其子标签为若干 `intent-filter` ,如何解析取决于系统的 `IntentFilter.readFromXml` 实现。
13 | 5. intent-filter 标签一般来说包含 `action` 、 `cat` 、 `scheme` 、 `auth` 等子标签,对应于 intent-filter 的 action, categories, scheme, authority 。
14 | 6. 对于 remove 中的 intent-filter ,只要所写的项目全部属于 Activity 的某个原 intent filter ,则会被匹配并从 activity 的 intent-filter 列表移除。
15 | 7. 对于 add 中的 intent-filter ,它会被直接添加到 activity 的 intent-filter 列表。
16 | 8. 修改 xml 和安装 app 都会触发重载的更新。如果不需要重载,可以删除 XML 配置,或者修改后缀名为非 xml ,这样相应的重载配置会被还原。
17 |
18 | ## XML 示例
19 |
20 | ```xml
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | ```
72 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/a13e300/tools/ifo/IFOConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.a13e300.tools.ifo;
2 |
3 | import android.content.ComponentName;
4 | import android.content.IntentFilter;
5 | import android.util.ArrayMap;
6 |
7 | import org.xmlpull.v1.XmlPullParser;
8 | import org.xmlpull.v1.XmlPullParserException;
9 |
10 | import java.io.IOException;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.Map;
14 |
15 | public final class IFOConfig {
16 | private static final String TAG_ACTIVITY = "activity";
17 |
18 | public Map overrides;
19 |
20 | public static final class OverrideForActivity {
21 | private static final String TAG_INTENT_FILTER = "intent-filter";
22 | private static final String TAG_ADD = "add";
23 | private static final String TAG_REMOVE = "remove";
24 | private static final String ATTRIBUTE_NAME = "name";
25 |
26 | public ComponentName activity;
27 | public List adds;
28 | public List removes;
29 |
30 | public OverrideForActivity() {
31 | adds = new ArrayList<>();
32 | removes = new ArrayList<>();
33 | }
34 |
35 | public void readFromXml(XmlPullParser parser) throws XmlPullParserException, IOException {
36 | String name = parser.getAttributeValue(null, ATTRIBUTE_NAME);
37 | activity = ComponentName.unflattenFromString(name);
38 |
39 | int outerDepth = parser.getDepth();
40 | int type;
41 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
42 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
43 | if (type == XmlPullParser.END_TAG
44 | || type == XmlPullParser.TEXT) {
45 | continue;
46 | }
47 |
48 | String tagName = parser.getName();
49 | if (TAG_ADD.equals(tagName)) {
50 | readIntentFiltersFromXml(adds, parser);
51 | } else if (TAG_REMOVE.equals(tagName)) {
52 | readIntentFiltersFromXml(removes, parser);
53 | }
54 | }
55 | }
56 |
57 | private static void readIntentFiltersFromXml(List l, XmlPullParser parser) throws XmlPullParserException, IOException {
58 | int outerDepth = parser.getDepth();
59 | int type;
60 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
61 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
62 | if (type == XmlPullParser.END_TAG
63 | || type == XmlPullParser.TEXT) {
64 | continue;
65 | }
66 |
67 | String tagName = parser.getName();
68 | if (TAG_INTENT_FILTER.equals(tagName)) {
69 | IntentFilter intent = new IntentFilter();
70 | intent.readFromXml(parser);
71 | l.add(intent);
72 | }
73 | }
74 | }
75 | }
76 |
77 | public IFOConfig() {
78 | overrides = new ArrayMap<>();
79 | }
80 |
81 | public void readFromXml(XmlPullParser parser) throws XmlPullParserException, IOException {
82 | int outerDepth = parser.getDepth();
83 | int type;
84 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
85 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
86 | if (type == XmlPullParser.END_TAG
87 | || type == XmlPullParser.TEXT) {
88 | continue;
89 | }
90 |
91 | String tagName = parser.getName();
92 | if (TAG_ACTIVITY.equals(tagName)) {
93 | var o = new OverrideForActivity();
94 | o.readFromXml(parser);
95 | overrides.put(o.activity, o);
96 | }
97 | }
98 | }
99 |
100 | public void merge(IFOConfig other) {
101 | overrides.putAll(other.overrides);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/a13e300/tools/ifo/Utils.java:
--------------------------------------------------------------------------------
1 | package io.github.a13e300.tools.ifo;
2 |
3 | import android.content.IntentFilter;
4 | import android.os.PatternMatcher;
5 | import android.text.TextUtils;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.annotation.Nullable;
9 |
10 | import java.util.ArrayList;
11 | import java.util.Iterator;
12 | import java.util.function.BiFunction;
13 |
14 | public class Utils {
15 | private static boolean isInclude(
16 | @Nullable Iterator from,
17 | @Nullable Iterator to,
18 | @Nullable BiFunction equalsFn
19 | ) {
20 | if (from == null || !from.hasNext()) return true;
21 | if (to == null || !to.hasNext()) return false;
22 | var s = new ArrayList();
23 | to.forEachRemaining(s::add);
24 | while (from.hasNext()) {
25 | boolean exists = false;
26 | var e1 = from.next();
27 | for (var e2 : s) {
28 | if ((equalsFn != null && equalsFn.apply(e1, e2)) || (equalsFn == null && e1 != null && e1.equals(e2))) {
29 | exists = true;
30 | break;
31 | }
32 | }
33 | if (!exists) return false;
34 | }
35 | return true;
36 | }
37 |
38 | private static final BiFunction PATTERN_MATCHER_EQUALS
39 | = (e1, e2) -> TextUtils.equals(e1.getPath(), e2.getPath()) && e1.getType() == e2.getType();
40 |
41 | public static boolean isIntentFilterMatch(@NonNull IntentFilter from, @NonNull IntentFilter to) {
42 | return isInclude(from.actionsIterator(), to.actionsIterator(), null)
43 | && isInclude(from.categoriesIterator(), to.categoriesIterator(), null)
44 | && isInclude(from.typesIterator(), to.typesIterator(), null)
45 | && isInclude(from.authoritiesIterator(), to.authoritiesIterator(), null)
46 | && isInclude(from.schemesIterator(), to.schemesIterator(), null)
47 | && isInclude(from.pathsIterator(), to.pathsIterator(), PATTERN_MATCHER_EQUALS)
48 | && isInclude(from.schemeSpecificPartsIterator(), to.schemeSpecificPartsIterator(), PATTERN_MATCHER_EQUALS);
49 | }
50 |
51 | public static void fillIntentFilter(IntentFilter dst, IntentFilter src) {
52 | dst.setPriority(src.getPriority());
53 | var actionsIterator = src.actionsIterator();
54 | if (actionsIterator != null) actionsIterator.forEachRemaining(dst::addAction);
55 | var categoriesIterator = src.categoriesIterator();
56 | if (categoriesIterator != null) categoriesIterator.forEachRemaining(dst::addCategory);
57 | var typesIterator = src.typesIterator();
58 | if (typesIterator != null) typesIterator.forEachRemaining(s -> {
59 | try {
60 | dst.addDataType(s);
61 | } catch (IntentFilter.MalformedMimeTypeException e) {
62 | Logger.e("", e);
63 | }
64 | });
65 | var schemesIterator = src.schemesIterator();
66 | if (schemesIterator != null) schemesIterator.forEachRemaining(dst::addDataScheme);
67 | var authoritiesIterator = src.authoritiesIterator();
68 | if (authoritiesIterator != null) authoritiesIterator.forEachRemaining((e) -> dst.addDataAuthority(e.getHost(), String.valueOf(e.getPort())));
69 | var pathsIterator = src.pathsIterator();
70 | if (pathsIterator != null) pathsIterator.forEachRemaining((e) -> dst.addDataPath(e.getPath(), e.getType()));
71 | var schemeSpecificPartsIterator = src.schemeSpecificPartsIterator();
72 | if (schemeSpecificPartsIterator != null) schemeSpecificPartsIterator.forEachRemaining((e) -> dst.addDataSchemeSpecificPart(e.getPath(), e.getType()));
73 | }
74 |
75 | public static String dumpIntentFilter(IntentFilter src) {
76 | StringBuilder sb = new StringBuilder();
77 | sb.append("IntentFilter{");
78 | sb.append(src.hashCode());
79 | sb.append(",actions=[");
80 | var actionsIterator = src.actionsIterator();
81 | if (actionsIterator != null) actionsIterator.forEachRemaining(s -> sb.append(s).append(","));
82 | sb.append("],categories=[");
83 | var categoriesIterator = src.categoriesIterator();
84 | if (categoriesIterator != null) categoriesIterator.forEachRemaining(s -> sb.append(s).append(","));
85 | var typesIterator = src.typesIterator();
86 | sb.append("],types=[");
87 | if (typesIterator != null) typesIterator.forEachRemaining(s -> sb.append(s).append(","));
88 | sb.append("],schemes=[");
89 | var schemesIterator = src.schemesIterator();
90 | if (schemesIterator != null) schemesIterator.forEachRemaining(s -> sb.append(s).append(","));
91 | sb.append("],auths=[");
92 | var authoritiesIterator = src.authoritiesIterator();
93 | if (authoritiesIterator != null) authoritiesIterator.forEachRemaining((e) -> sb.append(e.getHost()).append(":").append(e.getPort()));
94 | var pathsIterator = src.pathsIterator();
95 | sb.append("],paths=[");
96 | if (pathsIterator != null) pathsIterator.forEachRemaining((e) -> sb.append(e.getPath()).append("(type=").append(e.getType()).append(")"));
97 | var schemeSpecificPartsIterator = src.schemeSpecificPartsIterator();
98 | sb.append("],schemeSpecificParts=[");
99 | if (schemeSpecificPartsIterator != null) schemeSpecificPartsIterator.forEachRemaining((e) -> sb.append(e.getPath()).append("(type=").append(e.getType()).append(")"));
100 | sb.append("]}");
101 | return sb.toString();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/a13e300/tools/ifo/XposedEntry.java:
--------------------------------------------------------------------------------
1 | package io.github.a13e300.tools.ifo;
2 |
3 | import android.content.IntentFilter;
4 |
5 | import java.lang.reflect.Field;
6 | import java.lang.reflect.InvocationHandler;
7 | import java.lang.reflect.Method;
8 | import java.lang.reflect.Proxy;
9 | import java.util.List;
10 |
11 | import de.robv.android.xposed.IXposedHookLoadPackage;
12 | import de.robv.android.xposed.XC_MethodHook;
13 | import de.robv.android.xposed.XposedBridge;
14 | import de.robv.android.xposed.XposedHelpers;
15 | import de.robv.android.xposed.callbacks.XC_LoadPackage;
16 |
17 | public class XposedEntry implements IXposedHookLoadPackage {
18 | static class AndroidPackageProxy implements InvocationHandler {
19 | Object mActivities;
20 | Object mOrig;
21 | AndroidPackageProxy(Object orig, Object newActivities) {
22 | mOrig = orig;
23 | mActivities = newActivities;
24 | }
25 | @Override
26 | public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
27 | if ("getActivities".equals(method.getName())) {
28 | return mActivities;
29 | }
30 | return method.invoke(mOrig, objects);
31 | }
32 | }
33 |
34 | static Class> classParsedIntentInfo;
35 | static boolean classParsedIntentInfoExtendsIntentFilter = false;
36 | static Class> classParsedActivity;
37 | static Class> classAndroidPackage;
38 | static Field intentsField;
39 | static ClassLoader classLoader;
40 | static Method methodAddAllComponents;
41 | static boolean newMethodAddAllComponents;
42 |
43 | static abstract class MethodHook extends XC_MethodHook {
44 | @Override
45 | protected final void beforeHookedMethod(MethodHookParam param) throws Throwable {
46 | try {
47 | before(param);
48 | } catch (Throwable t) {
49 | Logger.e("error on hook before " + param.method.getName(), t);
50 | }
51 | }
52 |
53 | @Override
54 | protected final void afterHookedMethod(MethodHookParam param) throws Throwable {
55 | try {
56 | after(param);
57 | } catch (Throwable t) {
58 | Logger.e("error on hook after " + param.method.getName(), t);
59 | }
60 | }
61 |
62 | protected void before(MethodHookParam param) throws Throwable {}
63 | protected void after(MethodHookParam param) throws Throwable {}
64 | }
65 |
66 | private static Class> findClass(String ...names) throws ClassNotFoundException {
67 | for (var name: names) {
68 | try {
69 | return XposedHelpers.findClass(name, classLoader);
70 | } catch (XposedHelpers.ClassNotFoundError ignored) {
71 |
72 | }
73 | }
74 | throw new ClassNotFoundException("class not found: " + String.join(",", names));
75 | }
76 |
77 | @Override
78 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
79 | if (!lpparam.packageName.equals("android") || !lpparam.processName.equals("android")) {
80 | // Logger.e("not target (" + lpparam.packageName + "/" + lpparam.processName + ")");
81 | return;
82 | }
83 | Logger.i("IFO inject into android");
84 | classLoader = lpparam.classLoader;
85 | classParsedIntentInfo = findClass("com.android.internal.pm.pkg.component.ParsedIntentInfoImpl", "com.android.server.pm.pkg.component.ParsedIntentInfoImpl", "android.content.pm.parsing.component.ParsedIntentInfo");
86 | classParsedIntentInfoExtendsIntentFilter = IntentFilter.class.isAssignableFrom(classParsedIntentInfo);
87 | classParsedActivity = findClass("com.android.internal.pm.pkg.component.ParsedActivityImpl", "com.android.server.pm.pkg.component.ParsedActivityImpl", "android.content.pm.parsing.component.ParsedActivity");
88 | intentsField = XposedHelpers.findField(classParsedActivity, "intents");
89 | classAndroidPackage = findClass("com.android.server.pm.pkg.AndroidPackage", "com.android.server.pm.parsing.pkg.AndroidPackage");
90 | var classComponentResolver = XposedHelpers.findClass("com.android.server.pm.resolution.ComponentResolver", lpparam.classLoader);
91 | XposedBridge.hookAllConstructors(
92 | XposedHelpers.findClass("com.android.server.pm.PackageManagerService", lpparam.classLoader),
93 | new MethodHook() {
94 | @Override
95 | protected void before(MethodHookParam param) throws Throwable {
96 | IFOManager.getInstance().beforePMSLoad(param.thisObject);
97 | }
98 |
99 | @Override
100 | protected void after(MethodHookParam param) throws Throwable {
101 | IFOManager.getInstance().afterPMSLoad(param.thisObject);
102 | }
103 | }
104 | );
105 | Method addActivitiesLocked = null;
106 | Method removeAllComponentsLocked = null;
107 | int addActivitiesLockedPackageIdx = -1;
108 | int removeAllComponentsLockedPackageIdx = -1;
109 | for (var m: classComponentResolver.getDeclaredMethods()) {
110 | if ("addActivitiesLocked".equals(m.getName())) {
111 | addActivitiesLocked = m;
112 | var types = m.getParameterTypes();
113 | for (var t: types) {
114 | addActivitiesLockedPackageIdx++;
115 | if (t.equals(classAndroidPackage)) break;
116 | }
117 | } else if ("removeAllComponentsLocked".equals(m.getName())) {
118 | removeAllComponentsLocked = m;
119 | var types = m.getParameterTypes();
120 | for (var t: types) {
121 | removeAllComponentsLockedPackageIdx++;
122 | if (t.equals(classAndroidPackage)) break;
123 | }
124 | } else if ("addAllComponents".equals(m.getName())) {
125 | methodAddAllComponents = m;
126 | newMethodAddAllComponents = m.getParameterCount() == 4;
127 | }
128 | }
129 | int finalAddActivitiesLockedPackageIdx = addActivitiesLockedPackageIdx;
130 | XposedBridge.hookMethod(
131 | addActivitiesLocked,
132 | new MethodHook() {
133 | @Override
134 | protected void before(MethodHookParam param) throws Throwable {
135 | // AndroidPackage
136 | var p = param.args[finalAddActivitiesLockedPackageIdx];
137 | if (p == null) return;
138 | // List
139 | var activities = (List>) XposedHelpers.callMethod(p, "getActivities");
140 | if (activities == null || activities.isEmpty()) return;
141 | var packageName = (String) XposedHelpers.callMethod(p, "getPackageName");
142 | var list = IFOManager.getInstance().overrideForPackage(
143 | packageName,
144 | activities,
145 | false
146 | );
147 | if (list != null) {
148 | var handler = new AndroidPackageProxy(p, list);
149 | param.args[finalAddActivitiesLockedPackageIdx] = Proxy.newProxyInstance(lpparam.classLoader, new Class>[] { classAndroidPackage }, handler);
150 | }
151 | }
152 | }
153 | );
154 | int finalRemoveAllComponentsLockedPackageIdx = removeAllComponentsLockedPackageIdx;
155 | XposedBridge.hookMethod(
156 | removeAllComponentsLocked,
157 | new MethodHook() {
158 | @Override
159 | protected void before(MethodHookParam param) throws Throwable {
160 | // AndroidPackage
161 | var p = param.args[finalRemoveAllComponentsLockedPackageIdx];
162 | if (p == null) return;
163 | // List
164 | var activities = (List>) XposedHelpers.callMethod(p, "getActivities");
165 | if (activities == null || activities.isEmpty()) return;
166 | var packageName = (String) XposedHelpers.callMethod(p, "getPackageName");
167 | var list = IFOManager.getInstance().overrideForPackage(
168 | packageName,
169 | activities,
170 | true
171 | );
172 | if (list != null) {
173 | var handler = new AndroidPackageProxy(p, list);
174 | param.args[finalRemoveAllComponentsLockedPackageIdx] = Proxy.newProxyInstance(lpparam.classLoader, new Class>[] { classAndroidPackage }, handler);
175 | }
176 | }
177 | }
178 | );
179 | }
180 |
181 | public static void callAddAllComponents(Object self, Object p, Object pms) throws Throwable {
182 | if (newMethodAddAllComponents) {
183 | methodAddAllComponents.invoke(self, p, false, XposedHelpers.getObjectField(pms, "mSetupWizardPackage"), XposedHelpers.callMethod(pms, "snapshotComputer"));
184 | } else {
185 | methodAddAllComponents.invoke(self, p, false);
186 | }
187 | }
188 |
189 | public static IntentFilter getIntentFilterForInfo(Object info) {
190 | if (classParsedIntentInfoExtendsIntentFilter) return (IntentFilter) info;
191 | else return (IntentFilter) XposedHelpers.getObjectField(info, "mIntentFilter");
192 | }
193 |
194 | public static void setIntentInfoHasDefault(Object info, boolean has) {
195 | if (classParsedIntentInfoExtendsIntentFilter) return;
196 | XposedHelpers.callMethod(info, "setHasDefault", has);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/a13e300/tools/ifo/IFOManager.java:
--------------------------------------------------------------------------------
1 | package io.github.a13e300.tools.ifo;
2 |
3 | import android.content.ComponentName;
4 | import android.content.Intent;
5 | import android.os.FileObserver;
6 | import android.os.Handler;
7 | import android.os.HandlerThread;
8 | import android.os.Looper;
9 | import android.os.Message;
10 | import android.util.ArraySet;
11 | import android.util.Xml;
12 |
13 | import androidx.annotation.NonNull;
14 |
15 | import org.xmlpull.v1.XmlPullParser;
16 |
17 | import java.io.File;
18 | import java.io.FileInputStream;
19 | import java.util.ArrayList;
20 | import java.util.Collections;
21 | import java.util.HashMap;
22 | import java.util.List;
23 | import java.util.Map;
24 |
25 | import de.robv.android.xposed.XposedHelpers;
26 |
27 | public class IFOManager {
28 | private static final String CONFIG_PATH = "/data/system/ifo";
29 | private IFOManager() {}
30 |
31 | private static final IFOManager sInstance;
32 |
33 | private IFOConfig mConfig;
34 |
35 | private boolean beforeCalled = false;
36 | private boolean afterCalled = false;
37 |
38 | private Object mPMS;
39 | private Object mPMSLock;
40 | private Map mPMSPackages;
41 | private Object mComponentResolver;
42 | private RuleObserver mObserver;
43 | // on Android 14, ParsedActivity, ParsedIntentInfo, etc don't implement hashCode and equals
44 | // so we need to cache those result so that we can remove them properly
45 | private final Map overrideCache = new HashMap<>();
46 |
47 | // https://android.googlesource.com/platform/frameworks/base/+/fe011e06b5f1caf35e87c43ce5306234914d5c8c/services/core/java/com/android/server/firewall/IntentFirewall.java
48 | private class RuleObserver extends FileObserver {
49 | private static final int MONITORED_EVENTS = FileObserver.CREATE|FileObserver.MOVED_TO|
50 | FileObserver.CLOSE_WRITE|FileObserver.DELETE|FileObserver.MOVED_FROM;
51 |
52 | private final IFOHandler mHandler;
53 |
54 | public RuleObserver(File monitoredDir, IFOHandler handler) {
55 | super(monitoredDir.getAbsolutePath(), MONITORED_EVENTS);
56 | mHandler = handler;
57 | }
58 |
59 | @Override
60 | public void onEvent(int event, String path) {
61 | if (path != null && path.endsWith(".xml")) {
62 | // we wait 250ms before taking any action on an event, in order to dedup multiple
63 | // events. E.g. a delete event followed by a create event followed by a subsequent
64 | // write+close event
65 | mHandler.removeMessages(0);
66 | mHandler.sendEmptyMessageDelayed(0, 250);
67 | }
68 | }
69 | }
70 |
71 | private class IFOThread extends HandlerThread {
72 | public IFOThread() {
73 | super("IFO");
74 | }
75 |
76 | @Override
77 | protected void onLooperPrepared() {
78 | super.onLooperPrepared();
79 | Logger.d("starting file observer ...");
80 | mObserver = new RuleObserver(new File(CONFIG_PATH), new IFOHandler(getLooper()));
81 | mObserver.startWatching();
82 | }
83 | }
84 |
85 | private class IFOHandler extends Handler {
86 | IFOHandler(Looper looper) {
87 | super(looper);
88 | }
89 | @Override
90 | public void handleMessage(@NonNull Message msg) {
91 | new Thread(IFOManager.this::updateConfig).start();
92 | }
93 | }
94 |
95 | static {
96 | sInstance = new IFOManager();
97 | }
98 |
99 | void beforePMSLoad(Object pms) {
100 | if (beforeCalled) throw new IllegalStateException("before has been called!");
101 | beforeCalled = true;
102 | mConfig = readConfigs();
103 | mPMS = pms;
104 | }
105 |
106 | void afterPMSLoad(Object pms) {
107 | if (afterCalled) throw new IllegalStateException("after has been called!");
108 | afterCalled = true;
109 | mPMSLock = XposedHelpers.getObjectField(pms, "mLock");
110 | mPMSPackages = (Map) XposedHelpers.getObjectField(pms, "mPackages");
111 | mComponentResolver = XposedHelpers.getObjectField(pms, "mComponentResolver");
112 | new IFOThread().start();
113 | }
114 |
115 | public static IFOManager getInstance() {
116 | return sInstance;
117 | }
118 |
119 | private static IFOConfig readConfigs() {
120 | IFOConfig c = new IFOConfig();
121 | var p = new File(CONFIG_PATH);
122 | if (!p.exists() || !p.isDirectory()) {
123 | if (!p.mkdirs()) {
124 | Logger.e("failed to make dir for config path");
125 | return c;
126 | }
127 | }
128 | var files = p.listFiles();
129 | if (files == null) {
130 | Logger.e("config path does not exist");
131 | return c;
132 | }
133 | for (var f: files) {
134 | if (!f.isFile() || !f.canRead() || !f.getName().endsWith(".xml")) continue;
135 | var parser = Xml.newPullParser();
136 | var config = new IFOConfig();
137 | try {
138 | Logger.d("reading config: " + f);
139 | parser.setInput(new FileInputStream(f), "utf-8");
140 | int outerDepth = parser.getDepth();
141 | int type;
142 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
143 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
144 | if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
145 | continue;
146 | }
147 | if ("intent-filter-override".equals(parser.getName())) {
148 | config.readFromXml(parser);
149 | }
150 | }
151 | } catch (Throwable t) {
152 | Logger.e("failed to read " + f + ", skip", t);
153 | continue;
154 | }
155 | c.merge(config);
156 | }
157 | Logger.d("read " + c.overrides.size() + " rules");
158 | return c;
159 | }
160 |
161 | private void updateConfig() {
162 | synchronized (mPMSLock) {
163 | try {
164 | Logger.d("updating config");
165 | Logger.d("removing old overrides");
166 | var packagesToAdd = new ArraySet();
167 | for (var name : new ArraySet<>(overrideCache.keySet())) {
168 | var p = mPMSPackages.get(name);
169 | packagesToAdd.add(name);
170 | if (p == null) {
171 | Logger.w(name + " does not exists, skip remove");
172 | continue;
173 | }
174 | XposedHelpers.callMethod(mComponentResolver, "removeAllComponents", p, false);
175 | }
176 | overrideCache.clear();
177 | mConfig = readConfigs();
178 | for (var c : mConfig.overrides.keySet()) {
179 | packagesToAdd.add(c.getPackageName());
180 | }
181 | Logger.d("adding new overrides for packages: " + packagesToAdd);
182 | for (var name : packagesToAdd) {
183 | var p = mPMSPackages.get(name);
184 | if (p == null) {
185 | Logger.w(name + " does not exists, skip add");
186 | continue;
187 | }
188 | XposedEntry.callAddAllComponents(mComponentResolver, p, mPMS);
189 | }
190 | } catch (Throwable t) {
191 | Logger.e("failed to update config", t);
192 | }
193 | }
194 | }
195 |
196 | List overrideForPackage(String packageName, List activities, boolean isRemove) {
197 | if (isRemove) {
198 | return overrideCache.remove(packageName);
199 | }
200 | var result = new ArrayList<>();
201 | int nOverride = 0;
202 | for (var activity : activities) {
203 | var cn = (ComponentName) XposedHelpers.callMethod(activity, "getComponentName");
204 | var override = mConfig.overrides.get(cn);
205 | if (override == null) {
206 | result.add(activity);
207 | continue;
208 | }
209 | Logger.d("overriding for activity " + cn + " (isRemove=" + isRemove + ")");
210 | // List
211 | var intents = (List) XposedHelpers.getObjectField(activity, "intents");
212 | if (intents == null) intents = Collections.emptyList();
213 | boolean useNewIntents = false;
214 | var newIntents = new ArrayList<>();
215 | try {
216 | var intentsToRemove = new ArrayList<>();
217 | for (var remove : override.removes) {
218 | int j = 0;
219 | for (var i : intents) {
220 | var intentFilter = XposedEntry.getIntentFilterForInfo(i);
221 | if (Utils.isIntentFilterMatch(remove, intentFilter)) {
222 | Logger.d("removed:" + Utils.dumpIntentFilter(intentFilter));
223 | intentsToRemove.add(i);
224 | j++;
225 | }
226 | if (j > 1) {
227 | Logger.d("warning: multiple intent-filters matched to remove: " + remove);
228 | }
229 | }
230 | }
231 | Logger.d("remove " + intentsToRemove.size() + " intents for activity " + cn);
232 | for (var i : intents) {
233 | if (!intentsToRemove.contains(i)) newIntents.add(i);
234 | }
235 | for (var add : override.adds) {
236 | var intentInfo = XposedHelpers.newInstance(
237 | XposedEntry.classParsedIntentInfo
238 | );
239 | Utils.fillIntentFilter(XposedEntry.getIntentFilterForInfo(intentInfo), add);
240 | XposedEntry.setIntentInfoHasDefault(intentInfo, add.hasCategory(Intent.CATEGORY_DEFAULT));
241 | newIntents.add(intentInfo);
242 | }
243 | Logger.d("added " + override.adds.size() + " intents for activity " + cn);
244 | if (!intentsToRemove.isEmpty() || !override.adds.isEmpty()) {
245 | useNewIntents = true;
246 | /*
247 | Logger.d("final intents:");
248 | for (var i : newIntents) {
249 | Logger.d(Utils.dumpIntentFilter((IntentFilter) i));
250 | }*/
251 | }
252 | } catch (Throwable t) {
253 | Logger.e("error occurred while overriding for " + cn, t);
254 | throw t;
255 | }
256 | if (useNewIntents) {
257 | nOverride++;
258 | var newActivity = XposedHelpers.newInstance(XposedEntry.classParsedActivity, activity);
259 | try {
260 | XposedEntry.intentsField.set(newActivity, newIntents);
261 | } catch (Throwable t) {
262 | Logger.e("failed to set", t);
263 | }
264 | result.add(newActivity);
265 | } else {
266 | result.add(activity);
267 | }
268 | }
269 | if (nOverride > 0) {
270 | overrideCache.put(packageName, result);
271 | return result;
272 | }
273 | return null;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------