├── 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 | --------------------------------------------------------------------------------