diff --git a/appstub/src/main/java/org/lsposed/lspatch/appstub/LSPAppComponentFactoryStub.java b/appstub/src/main/java/org/lsposed/lspatch/appstub/LSPAppComponentFactoryStub.java new file mode 100644 index 0000000..317ff49 --- /dev/null +++ b/appstub/src/main/java/org/lsposed/lspatch/appstub/LSPAppComponentFactoryStub.java @@ -0,0 +1,79 @@ +package org.lsposed.lspatch.appstub; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AppComponentFactory; +import android.app.Application; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.util.Log; + +import org.lsposed.lspatch.util.FileUtils; + +import java.io.InputStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import dalvik.system.PathClassLoader; + +@SuppressLint("NewApi") +public class LSPAppComponentFactoryStub extends AppComponentFactory { + private static final String TAG = "LSPatch"; + private static final String ORIGINAL_APK_ASSET_PATH = "assets/origin_apk.bin"; + private static final String ORIGINAL_APP_COMPONENT_FACTORY_ASSET_PATH = "assets/original_app_component_factory.ini"; + + private AppComponentFactory originalAppComponentFactory = null; + + // Proxy appComponentFactory to load the original one + private void initOrigin(ClassLoader cl, ApplicationInfo aInfo) { + final String cacheApkPath = aInfo.dataDir + "/cache/origin_apk.bin"; + final String originalAppComponentFactoryClass = FileUtils.readTextFromInputStream(cl.getResourceAsStream(ORIGINAL_APP_COMPONENT_FACTORY_ASSET_PATH)); + + try { + try (InputStream inputStream = cl.getResourceAsStream(ORIGINAL_APK_ASSET_PATH)) { + Files.copy(inputStream, Paths.get(cacheApkPath)); + } catch (FileAlreadyExistsException ignored) { + } + ClassLoader appClassLoader = new PathClassLoader(cacheApkPath, cl.getParent()); + originalAppComponentFactory = (AppComponentFactory) appClassLoader.loadClass(originalAppComponentFactoryClass).newInstance(); + Log.d(TAG, "appComponentFactory is now switched to " + originalAppComponentFactory); + } catch (Throwable e) { + Log.e(TAG, "initOrigin", e); + } + } + + @Override + public ClassLoader instantiateClassLoader(ClassLoader cl, ApplicationInfo aInfo) { + if (originalAppComponentFactory == null) initOrigin(cl, aInfo); + return originalAppComponentFactory.instantiateClassLoader(cl, aInfo); + } + + @Override + public Application instantiateApplication(ClassLoader cl, String className) throws IllegalAccessException, InstantiationException, ClassNotFoundException { + return originalAppComponentFactory.instantiateApplication(cl, className); + } + + @Override + public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + return originalAppComponentFactory.instantiateActivity(cl, className, intent); + } + + @Override + public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + return originalAppComponentFactory.instantiateReceiver(cl, className, intent); + } + + @Override + public Service instantiateService(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + return originalAppComponentFactory.instantiateService(cl, className, intent); + } + + @Override + public ContentProvider instantiateProvider(ClassLoader cl, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + return originalAppComponentFactory.instantiateProvider(cl, className); + } +} \ No newline at end of file diff --git a/appstub/src/main/java/org/lsposed/lspatch/util/FileUtils.java b/appstub/src/main/java/org/lsposed/lspatch/util/FileUtils.java new file mode 100644 index 0000000..b4f6234 --- /dev/null +++ b/appstub/src/main/java/org/lsposed/lspatch/util/FileUtils.java @@ -0,0 +1,22 @@ +package org.lsposed.lspatch.util; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +public class FileUtils { + + public static String readTextFromInputStream(InputStream is) { + try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader bufferedReader = new BufferedReader(reader)) { + StringBuilder builder = new StringBuilder(); + String str; + while ((str = bufferedReader.readLine()) != null) { + builder.append(str); + } + return builder.toString(); + } catch (Throwable ignored) { + } + return null; + } +} \ No newline at end of file diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index 3c21f7a..4d42d45 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -55,7 +55,8 @@ public class LSPatch { @Parameter(names = {"-f", "--force"}, description = "Force overwrite exists output file") private boolean forceOverwrite = false; - private String proxyName = "org.lsposed.lspatch.appstub.LSPApplicationStub"; + private String proxyApplication = "org.lsposed.lspatch.appstub.LSPApplicationStub"; + private String proxyAppComponentFactory = "org.lsposed.lspatch.appstub.LSPAppComponentFactoryStub"; @Parameter(names = {"-d", "--debuggable"}, description = "Set app to be debuggable") private boolean debuggableFlag = false; @@ -78,9 +79,10 @@ public class LSPatch { @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") private List modules = new ArrayList<>(); + private static final String APP_COMPONENT_FACTORY_ASSET_PATH = "assets/original_app_component_factory.ini"; private static final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini"; private static final String SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini"; - private static final String ORIGIN_APK_ASSET_PATH = "assets/origin_apk.bin"; + private static final String ORIGINAL_APK_ASSET_PATH = "assets/origin_apk.bin"; private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; private static final String RESOURCES_ARSC = "resources.arsc"; private static final HashSet APK_LIB_PATH_ARRAY = new HashSet<>(Arrays.asList( @@ -152,12 +154,12 @@ public class LSPatch { final var alignmentRule = AlignmentRules.compose( AlignmentRules.constantForSuffix(RESOURCES_ARSC, 4), - AlignmentRules.constantForSuffix(ORIGIN_APK_ASSET_PATH, 4096) + AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096) ); zFileOptions.setAlignmentRule(alignmentRule); try (ZFile zFile = ZFile.openReadWrite(tmpApk, zFileOptions)) { // copy origin apk to assets - zFile.add(ORIGIN_APK_ASSET_PATH, new FileInputStream(srcApkFile), false); + zFile.add(ORIGINAL_APK_ASSET_PATH, new FileInputStream(srcApkFile), false); // remove unnecessary files for (StoredEntry storedEntry : zFile.entries()) { @@ -186,13 +188,16 @@ public class LSPatch { throw new PatchError("Provided file is not a valid apk"); // parse the app main application full name from the manifest file - ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestEntry.open()); - if (pair == null) + ManifestParser.Triple triple = ManifestParser.parseManifestFile(manifestEntry.open()); + if (triple == null) throw new PatchError("Failed to parse AndroidManifest.xml"); - String applicationName = pair.applicationName == null ? "" : pair.applicationName; + String applicationName = triple.applicationName == null ? "" : triple.applicationName; + String appComponentFactory = triple.appComponentFactory == null ? "" : triple.appComponentFactory; - if (verbose) + if (verbose) { System.out.println("original application name: " + applicationName); + System.out.println("original appComponentFactory class: " + appComponentFactory); + } System.out.println("Patching apk..."); // modify manifest @@ -202,6 +207,13 @@ public class LSPatch { throw new PatchError("Error when modifying manifest: " + e); } + // save original appComponentFactory name to asset file even its empty + try (var is = new ByteArrayInputStream(appComponentFactory.getBytes(StandardCharsets.UTF_8))) { + zFile.add(APP_COMPONENT_FACTORY_ASSET_PATH, is); + } catch (Throwable e) { + throw new PatchError("Error when saving appComponentFactory class: " + e); + } + // save original main application name to asset file even its empty try (var is = new ByteArrayInputStream(applicationName.getBytes(StandardCharsets.UTF_8))) { zFile.add(APPLICATION_NAME_ASSET_PATH, is); @@ -301,7 +313,7 @@ public class LSPatch { System.out.print("Embedding module "); - ManifestParser.Pair pair = null; + ManifestParser.Triple triple = null; try (JarFile jar = new JarFile(file)) { var manifest = jar.getEntry(ANDROID_MANIFEST_XML); if (manifest == null) { @@ -309,22 +321,22 @@ public class LSPatch { System.err.println(file.getAbsolutePath() + " is not a valid apk file."); continue; } - pair = ManifestParser.parseManifestFile(jar.getInputStream(manifest)); - if (pair == null) { + triple = ManifestParser.parseManifestFile(jar.getInputStream(manifest)); + if (triple == null) { System.out.println(); System.err.println(file.getAbsolutePath() + " is not a valid apk file."); continue; } - System.out.println(pair.packageName); + System.out.println(triple.packageName); } catch (Throwable e) { System.out.println(); System.err.println(e.getMessage()); } - if (pair != null) { + if (triple != null) { try (var is = new FileInputStream(file)) { - zFile.add("assets/modules/" + pair.packageName, is); + zFile.add("assets/modules/" + triple.packageName, is); } catch (Throwable e) { - System.err.println("Embed " + pair.packageName + " with error: " + e.getMessage()); + System.err.println("Embed " + triple.packageName + " with error: " + e.getMessage()); } } } @@ -335,7 +347,8 @@ public class LSPatch { ModificationProperty property = new ModificationProperty(); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); - property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, proxyName)); + property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, proxyApplication)); + property.addApplicationAttribute(new AttributeItem("appComponentFactory", proxyAppComponentFactory)); var os = new ByteArrayOutputStream(); (new ManifestEditor(is, os, property)).processManifest(); diff --git a/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java b/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java index ae5b191..97de2aa 100644 --- a/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java +++ b/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java @@ -14,10 +14,11 @@ import wind.v1.XmlPullParserException; */ public class ManifestParser { - public static Pair parseManifestFile(InputStream is) throws IOException { + public static Triple parseManifestFile(InputStream is) throws IOException { AXmlResourceParser parser = new AXmlResourceParser(); String packageName = null; String applicationName = null; + String appComponentFactory = null; try { parser.open(is); @@ -43,10 +44,15 @@ public class ManifestParser { if ("name".equals(attrName)) { applicationName = parser.getAttributeValue(i); } + if ("appComponentFactory".equals(attrName)) { + appComponentFactory = parser.getAttributeValue(i); + } } - if (packageName != null && packageName.length() > 0 && applicationName != null && applicationName.length() > 0) { - return new Pair(packageName, applicationName); + if (packageName != null && packageName.length() > 0 && + applicationName != null && applicationName.length() > 0 && + appComponentFactory != null && appComponentFactory.length() > 0) { + return new Triple(packageName, applicationName, appComponentFactory); } } } else if (type == XmlPullParser.END_TAG) { @@ -56,26 +62,28 @@ public class ManifestParser { } catch (XmlPullParserException | IOException e) { return null; } - return new Pair(packageName, applicationName); + return new Triple(packageName, applicationName, appComponentFactory); } /** * Get the package name and the main application name from the manifest file */ - public static Pair parseManifestFile(String filePath) throws IOException { + public static Triple parseManifestFile(String filePath) throws IOException { File file = new File(filePath); try (var is = new FileInputStream(file)) { return parseManifestFile(is); } } - public static class Pair { + public static class Triple { public String packageName; public String applicationName; + public String appComponentFactory; - public Pair(String packageName, String applicationName) { + public Triple(String packageName, String applicationName, String appComponentFactory) { this.packageName = packageName; this.applicationName = applicationName; + this.appComponentFactory = appComponentFactory; } }