diff --git a/.gitignore b/.gitignore index b075fd30..74773ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,16 @@ /local.properties /.idea/workspace.xml /.idea/libraries +/.idea/misc.xml .DS_Store /build build +.idea/ /captures .settings +build-cache +build-cache.lock +/dump.txt +dump.txt +.cxx +.externalNativeBuild diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 60f31796..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Android-Plugin-Framework \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43ef..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf33..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ce1721d8..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 615128f6..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b0d1ceb8..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460d..00000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Android-Plugin-Framework.iml b/Android-Plugin-Framework.iml deleted file mode 100644 index 1948fbaf..00000000 --- a/Android-Plugin-Framework.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/FairyPlugin/.gitignore b/FairyPlugin/.gitignore new file mode 100644 index 00000000..9bfafdc9 --- /dev/null +++ b/FairyPlugin/.gitignore @@ -0,0 +1,3 @@ +/build +local.properties +/*.iml \ No newline at end of file diff --git a/FairyPlugin/agp2_3_3/host.gradle b/FairyPlugin/agp2_3_3/host.gradle new file mode 100644 index 00000000..3361ee23 --- /dev/null +++ b/FairyPlugin/agp2_3_3/host.gradle @@ -0,0 +1,820 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//在宿主脚本中apply此脚本, 此脚本一共做了6件事 +//1、编译完成后导出所有资源id,即aaptOptions.additionalParameters这个配置干的事情, 下面第4步需要此文件 +//2、编译宿主资源时插入public.xml,用来控制宿主资源id分组 +//3、指定插件进程名称,导出.ap_文件 +//4、编译完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 +//5、编译完成后导出宿主的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时需要此包 +//6、编译完成后导出宿主混淆后的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时若插件需要混淆则需要此包 +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +configurations { + innerPlugin +} + +def AndroidGradlePluginVersion = getPlugins().findPlugin("com.android.application").androidBuilder.createdBy + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +//第1件事 +android.aaptOptions.additionalParameters("-P", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + configurations.innerPlugin.files.each { file -> + //收集插件文件地址 + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName + + def scope = variant.getVariantData().getScope() + def mergeTask = tasks.getByName(scope.getMergeResourcesTask().name) + + //第2件事 + mergeTask.doLast { + String destPath = mergeTask.outputDir.absolutePath + '/values/'; + //如果是通过本地路径引入的此脚本 + if (buildscript.sourceFile != null) { + + def publicXmlPath = buildscript.sourceFile.getParentFile().absolutePath + '/public.xml' + println '编译宿主资源时插入' + publicXmlPath + ' 到 ' + destPath + ', 用来控制宿主资源id分组' + + if(!file(publicXmlPath).exists()) { + throw new FileNotFoundException("public.xml 文件未找到,请检查路径:" + (buildscript.sourceFile.getParentFile().absolutePath + '/public.xml')) + } + + copy { + from(buildscript.sourceFile.getParentFile()) { + include 'public.xml' + } + into(destPath) + } + } else { + String url = buildscript.sourceURI.toString().replaceFirst("[a-zA-Z\\.]*\$", "public.xml") + println '编译宿主资源时插入' + url + ' 到' + destPath + ', 用来控制宿主资源id分组' + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + OutputStream ouput =new FileOutputStream(new File(destPath, "public.xml")) + byte[] buffer = new byte[8*1024] + int size = -1 + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } + + if (!file(destPath + "public.xml").exists()) { + throw new FileNotFoundException("文件不存在:" + destPath + "public.xml") + } + } + + def mergeAssetsTask = tasks.getByName(scope.getMergeAssetsTask().name) + mergeAssetsTask.setOnlyIf { true } + mergeAssetsTask.outputs.upToDateWhen { false } + mergeAssetsTask.doLast { + + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + println '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + mergeAssetsTask.outputDir + from files(innerPluginApkList) + into(mergeAssetsTask.outputDir) + } + } + + //第3件事 + for (baseVariant in variant.outputs) { + + def manifestFilePath = baseVariant.processResources.manifestFile.absolutePath; + + baseVariant.processManifest.doLast { + + //android gradle3.+的属性 + //println "$manifestOutputDirectory" + + File manifestFile = new File(manifestFilePath) + + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + + //实际上最新版sdk和gradle可以直接指定apk了, 不需要.ap_文件 + def processResourcesTask = baseVariant.getProcessResources(); + //def processResFullName = baseVariant.apkData.fullName + processResourcesTask.doLast { + copy { + println '编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + + //println com.android.builder.Version.ANDROID_GRADLE_PLUGIN_VERSION + //Android Gradle Plugin Version 3.x + if (!AndroidGradlePluginVersion.startsWith("Android Gradle 2")) { + println processResourcesTask.resPackageOutputFolder + from processResourcesTask.resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } else { + //Android Gradle Plugin Version 2.x + println processResourcesTask.packageOutputFile + from processResourcesTask.packageOutputFile + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + } + } + + //第5件事 + def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile; + javaCompile.doLast { + + println "Merge Jar After Task " + javaCompile.name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + javaCompile.classpath.each { jarPath -> + jarMerger.addJar(jarPath); + //jarMerger.addFolder(directoryInput.getFile()); + } + + String buildClassesTaskName1 = "jar" + varName.capitalize() + "Classes"; + String classesPath1 = 'intermediates/packaged/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar"; + + String buildClassesTaskName2 = "bundleAppClasses" + varName.capitalize(); + String classesPath2 = 'intermediates/classes-jar/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar" + + def buildClassesTaskName; + def classesPath; + + if (tasks.findByName(buildClassesTaskName1)) { + buildClassesTaskName = buildClassesTaskName1; + classesPath = classesPath1; + } else if (tasks.findByName(buildClassesTaskName2)) { + buildClassesTaskName = buildClassesTaskName2; + classesPath = classesPath2; + } else { + throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本") + } + + File classes = new File(buildDir, classesPath); + + println "classes path is " + classes.absolutePath + println "buildClassesTaskName is " + buildClassesTaskName + + if (!classes.exists()) { + try { + tasks.getByName(buildClassesTaskName).execute() + } catch(Exception e) { + println "fail to create jar for task " + javaCompile.name + " " + buildClassesTaskName + } + } else { + println "classes path already exists: " + classes.absolutePath + } + if (classes.exists()) { + jarMerger.addJar(classes) + } + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + + //第6件事 + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}") + if (proguardTask) { + proguardTask.doFirst { + println "开始混淆任务:" + varName.capitalize() + } + proguardTask.doLast { + println "混淆完成:" + varName.capitalize() + boolean isFind = false; + proguardTask.outputs.files.files.each { File file-> + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + project.logger.error "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + + isFind = true; + def sourceHostObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + def hostObfuscatedJar = "host_obfuscated.jar" + println "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar + + copy { + from sourceHostObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return hostObfuscatedJar + } + }) + } + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + + //第4件事 + tasks.findByName("generate${varName.capitalize()}Sources").doFirst { + + println '编译宿主ap完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 flavorName = ' + flavorName + ", buildTypeName = " + buildTypeName + + createThemePatch(flavorName, buildTypeName); + + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 +task makeHostBaseLine(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + from zipTree("build/outputs/resources.ap_") + from('build/outputs') { + exclude '*.ap_' + } +} + +//导出主题patch +def createThemePatch(String flavor, String buildType) { + + File patchDir = new File(project.buildDir.absolutePath + "/outputs/theme_patch/" + buildType); + patchDir.mkdirs(); + + File generatedRes = new File(project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml"); + File dest = new File(patchDir, "patch_theme.xml") + + println "export from " + generatedRes + " to " + dest + + if (!generatedRes.exists()) { + throw new FileNotFoundException("File Not Found : " + generatedRes.absolutePath) + } + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + ThemeProcessor.exportThemeStyle(generatedRes, dest, packageName) + + String mergedResDir = "${project.buildDir}/intermediates/res/merged/" + (flavor.equals("")?"":(flavor + "/")) + buildType + "/"; + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', 'values-v1*/values-v1*.xml', 'values-v2*/values-v2*.xml', 'values-*-v1*/values-*-v1*.xml', 'values-*-v4/values-*-v4.xml', 'values-land/values-land.xml', 'values-*-v2*/values-*-v2*.xml', 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + dest = new File(patchDir, 'patch_' + itemFile.name) + + println "export from " + itemFile + " to " + dest + + ThemeProcessor.exportThemeStyle(itemFile, dest, packageName) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName) { + this.destFile = destFile; + this.packageName = packageName; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + } + } else { + outXmlStream.write(tag); + } + } else { + outXmlStream.write(tag); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + + this.jarOutputStream.putNextEntry(newEntry); + + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} \ No newline at end of file diff --git a/FairyPlugin/agp2_3_3/plugin.gradle b/FairyPlugin/agp2_3_3/plugin.gradle new file mode 100644 index 00000000..bd476f65 --- /dev/null +++ b/FairyPlugin/agp2_3_3/plugin.gradle @@ -0,0 +1,1004 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.* + +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//在插件脚本中apply此脚本, 此脚本一共做了3件事 +//1、编译非独立插件时引入编译宿主时导出的资源包.ap_ +//2、编译非独立插件时引入编译宿主时导出的主题包 +//3、扁平处理declare-style,使非独立插件支持declare-style配置 +//4、修正layout文件中的自定义属性的枚举用法 +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// + +configurations { + baselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +//基线包路径 +def host_patch_path = "" +//基线包解压路径 +def host_patch_extract_dir = project.buildDir.absolutePath + File.separator + 'outputs' + File.separator + 'baselinePatch' +def hostClassesJar = host_patch_extract_dir + File.separator + "host_classes.jar" + +//基线包文件名 +def host_patch_fileName = "host.bar" + + +def ApplicationPlugin = getPlugins().findPlugin("com.android.application") +def LibraryPlugin = getPlugins().findPlugin("com.android.library") +def isApplicationModule +def AndroidGradlePluginVersion +if (ApplicationPlugin != null) { + isApplicationModule = true; + AndroidGradlePluginVersion = ApplicationPlugin.androidBuilder.createdBy +} else { + isApplicationModule = false; + AndroidGradlePluginVersion = LibraryPlugin.androidBuilder.createdBy +} +def fairyMinifyEnabled = false + +android{ +} + +dependencies { + + if (AndroidGradlePluginVersion.startsWith("Android Gradle 2")) { + if (!fairyMinifyEnabled) { + provided files(hostClassesJar) + } else { + compile files(hostClassesJar) + } + } + +} + +//第一件事 将宿主资源添加到编译时,仅参与编译,不参与打包 +android.aaptOptions.additionalParameters('-I', host_patch_extract_dir + "/" + host_patch_fileName) + +HashMap>> declareStyleMap = new HashMap>>(); + +afterEvaluate { + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + + configurations.each { config -> + if (config.name.equals("baselinePatch")) { + config.files.each { patch -> + + //读取宿主基线包文件路径 + host_patch_path = patch.absolutePath; + println project.name + "发现宿主基线配置指向位置:" + host_patch_path + + if (!AndroidGradlePluginVersion.startsWith("Android Gradle 2")) { + + //读取混淆开关配置 + gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.startsWith("assemble")) { + if (startTaskName.contains("Debug")) { + fairyMinifyEnabled = android.buildTypes.debug.minifyEnabled + } else if (startTaskName.contains("Release")) { + fairyMinifyEnabled = android.buildTypes.release.minifyEnabled + } + } + } + + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + println project.name + "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + fairyMinifyEnabled + if (!fairyMinifyEnabled) { + configurations.compileOnly.dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations.implementation.dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + } + } + } + } + + def moduleVariants = isApplicationModule?android.applicationVariants:android.libraryVariants + for (variant in moduleVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName + + def generateDebugSourcesTask = tasks.findByName("generate${varName.capitalize()}Resources") + if (generateDebugSourcesTask) { + generateDebugSourcesTask.setOnlyIf { true } + generateDebugSourcesTask.outputs.upToDateWhen { false } + generateDebugSourcesTask.doFirst { + + println "检查宿主基线patch文件:" + host_patch_path + + if (!file(host_patch_path).exists()) { + file(host_patch_extract_dir).deleteDir(); + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path) + } + copy { + println "正在解压宿主基线patch文件:" + host_patch_path + def zipFile = file(host_patch_path);//host.bar + def outputDir = file(host_patch_extract_dir)//outputs/baselinePatch + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + host_patch_fileName + } + } + into outputDir + } + } + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + + println "开始merge插件工程资源:" + host_patch_path + + if (!file(host_patch_path).exists()) { + file(host_patch_extract_dir).deleteDir(); + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path) + } + + File propFile = file(host_patch_extract_dir + "/HostInfo.prop"); + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + def hostBuildType = props.getProperty("host.buildType") + + //第二件事 + applyThemePatch(hostBuildType, buildTypeName, host_patch_extract_dir); + + //第三件事<一> + File originVaules = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName + "/values/values.xml"); + if (originVaules.exists()) { + + File valuesProcessed = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName + "/values/values_processed.xml"); + + println "fixDeclareStyle " + originVaules + ", " + valuesProcessed.absolutePath + + StyleProcessor styleProcessor = StyleProcessor.fixDeclareStyle(originVaules, valuesProcessed); + + SortedMap> map = styleProcessor.attrList; + if (map != null) { + declareStyleMap.put(buildTypeName, map) + } + originVaules.delete() + + //第四件事,修正layout文件中的自定义属性的枚举用法 + File resDir = new File(project.buildDir, "intermediates/res/merged/" + buildTypeName); + resDir.eachFileRecurse({file -> + if (file.absolutePath.contains("/layout") && file.name.endsWith(".xml")) { + String fileText = file.text; + Iterator> itr = styleProcessor.enumItemList.iterator(); + println "try fix enum attr for " + file.absolutePath + while(itr.hasNext()) { + Map.Entry entryItem = itr.next(); + fileText = fileText.replaceAll(entryItem.key, entryItem.value) + } + + println "try fix flag attr for " + file.absolutePath + fileText = StyleProcessor.fixAttrFlag(fileText, styleProcessor.attrFlagMap); + + file.write(fileText); + } + } + ) + } else { + println "没有需要处理的DeclareStyle资源" + } + } + + def generateSourcesTask = tasks.findByName("generate${varName.capitalize()}Sources"); + generateSourcesTask.doLast { + //第三件事<二> + FileTree rfiles = fileTree(dir: project.buildDir.absolutePath + "/generated/source/r/"+ buildTypeName, include: ['**/R.java']) + rfiles.each { File itemFile -> + + File newR = new File(itemFile.getAbsolutePath()+ "_temp"); + File originR = itemFile; + + println "处理R.java文件,生成decalre-style , \n" + newR.absolutePath + ", \n" + originR.absolutePath + + newR.delete() + SortedMap> map = declareStyleMap.get(buildTypeName); + + if (originR.exists() && map != null) { + BufferedReader br = new BufferedReader(new FileReader(originR)); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newR), "UTF-8")); + String srcLine = null; + boolean hasStyleable = false; + String packageStr = ""; + while((srcLine = br.readLine()) != null) { + if (srcLine.equals("public final class R {")) { + bw.writeLine(srcLine); + + bw.writeLine(" public static final class styleable {"); + Iterator>> itr = map.iterator(); + while(itr.hasNext()) { + + Map.Entry> item = itr.next(); + String key = item.key; + StringBuilder sb = new StringBuilder(); + + println "Generate styleable " + key + + for(int i = 0; i < item.value.size(); i++) { + bw.writeLine(" public static int " + key + "_" + item.value.get(i) + "=" + i + ";"); + sb.append(packageStr).append(".R.attr.").append(item.value.get(i)).append(",") + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length()-1) + } + bw.writeLine(" public static int[] " + key + " = {" + sb.toString() + "};"); + } + bw.writeLine(" }") + } else if (srcLine.contains("public static final class styleable {")) { + hasStyleable = true; + } else if (hasStyleable && srcLine.trim().equals("}")) { + hasStyleable = false; + } else { + if (srcLine.startsWith("package ") && srcLine.endsWith(";")) { + if(isApplicationModule) { + packageStr = android.defaultConfig.applicationId + } else { + packageStr = srcLine.replace("package ", "").replace(";", ""); + } + } + bw.writeLine(srcLine); + } + } + bw.close() + br.close() + + originR.delete() + newR.renameTo(originR) + } else { + println "Nothing to Fix for " + originR.absolutePath + } + } + } + + if (isApplicationModule) { + for (baseVariant in variant.outputs) { + + baseVariant.processResources.doFirst { + println "开始处理插件工程资源.." + if (!new File(host_patch_path).exists()) { + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_path) + } + if (!file(host_patch_extract_dir).exists()) { + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", host_patch_extract_dir) + } + } + + baseVariant.processManifest.doLast { + File propFile = file(host_patch_extract_dir + "/HostInfo.prop"); + def hostVersionName = null + def hostVersionCode = null + def hostApplicationId = null + def hostBuildType = null + def hostFlavorName = null + if (propFile.exists()) { + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + hostVersionName = props.getProperty("host.versionName") + hostVersionCode = props.getProperty("host.versionCode") + hostBuildType = props.getProperty("host.buildType") + hostFlavorName = props.getProperty("host.flavorName") + hostApplicationId = props.getProperty("host.applicationId") + + println "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName + + } else { + throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath) + } + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest文件中插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插件Manifest文件中插入非独立插件标识 requiredHostApplicationId=" + hostApplicationId + def manifestFile + if (AndroidGradlePluginVersion.startsWith("Android Gradle 2")) { + manifestFile = new File(properties.get("manifestOutputFile").absolutePath) + } else { + manifestFile = new File("$manifestOutputDirectory/AndroidManifest.xml") + } + println "插件Manifest:" + manifestFile.absolutePath + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + println "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + isFind = true; + def pluginObfuscatedJar = "plugin_obfuscated.jar" + def sourcePluginObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + //保留一个备份 + println "导出混淆后的插件jar包 " + println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar + println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar + copy { + from sourcePluginObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return pluginObfuscatedJar + } + }) + } + + diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar); + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + } + } +} + +def diffJar(String plugin, String host) { + + + println "通过基线混淆包和插件混淆包做diff清理重复的class" + + println "插件:" + plugin + ", 宿主:" + host + + println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/" + + ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/") + List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/") + + println "再删掉插件的源 " + plugin + new File(plugin).delete() + + println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName() + ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses) + println "重新生成的插件的源 " + plugin + + //备份diff后的包 + copy { + println "备份diff后重新生成的插件的源包到插件out目录" + println "From " + plugin + println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar" + from plugin + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return "plugin_obfuscated_after_diff.jar" + } + }) + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} + +def applyThemePatch(hostBuildType, buildTypeName, hostPatchExtractDir) { + + def fromDir = hostPatchExtractDir + "/theme_patch/" + hostBuildType; + def resourceDir = project.buildDir.absolutePath + "/intermediates/res/merged/" + buildTypeName; + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + resourceDir); + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + + String dest; + if (itemFile.name.equals('patch_theme.xml')) { + dest = resourceDir + '/values/' + } else { + dest = resourceDir + '/' + itemFile.name.replace('patch_', '').replace('.xml', '') + "/" + } + + println "apply theme_patch from " + itemFile + " to " + dest + copy { + from(fromDir) { + include itemFile.name + } + into(dest) + } + } +} + +/** fix provided config for aar dependences + ext.plist = [] + configurations.provided.dependencies.each {dep -> + println dep + gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.startsWith("assemble") || startTaskName.startsWith("build")) { + if (dep.version.equals('unspecified')) { + + String buildType = startTaskName.replace("assemble", "").replace("build", "") + if (buildType.length() == 0) { + buildType = "debug"; + } + + //为依赖库插入jar任务 + dep.dependencyProject.tasks.getByName("compile" + buildType + "Sources").doLast { + String jarTaskName = "jar" + buildType.substring(0, 1).toUpperCase()+ buildType.replaceFirst("\\w","") + "Classes" + println '执行task:' + jarTaskName; + dep.dependencyProject.tasks.getByName(jarTaskName).execute() + } + + //src的jar + File srcJar = new File(dep.dependencyProject.buildDir, "intermediates/packaged/" + buildType + "/classes.jar") + addProvidedDependences(configurations.provided.dependencies, srcJar); + + //处理aar依赖 + File aarDir = new File(dep.dependencyProject.buildDir, "intermediates/exploded-aar") + addProvidedDependences(configurations.provided.dependencies, aarDir) + + //处理libs/jar依赖 + //TODO + + } + } + } + } + + configurations.provided.dependencies.clear() + + ext.plist.each { path -> + configurations.provided.dependencies.add(project.dependencies.create(files(path))) + println "try add provided jar to plugin project : " + path + } + + def addProvidedDependences(DependencySet providedDepSet, File root) { + //宿主编译前文件可能不存在, 所以要先编译宿主 再编译插件 + if (root.getName().endsWith(".jar")) { + ext.plist +=[root.getAbsolutePath()] + //providedDepSet.add(project.dependencies.create(files(root.getAbsolutePath()))) + //println "try add provided jar to plugin project : " + root.getAbsolutePath() + } else { + File[] subFiles = root.listFiles(); + if (subFiles != null) { + for (File subFile : subFiles) { + addProvidedDependences(providedDepSet, subFile); + } + } + } + } + */ + + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} \ No newline at end of file diff --git a/PluginMain/public.xml b/FairyPlugin/agp2_3_3/public.xml similarity index 70% rename from PluginMain/public.xml rename to FairyPlugin/agp2_3_3/public.xml index 52cb34df..7316a33f 100644 --- a/PluginMain/public.xml +++ b/FairyPlugin/agp2_3_3/public.xml @@ -16,7 +16,7 @@ - + @@ -24,4 +24,12 @@ + + + + + + + + diff --git a/FairyPlugin/agp3_0_1/host.gradle b/FairyPlugin/agp3_0_1/host.gradle new file mode 100644 index 00000000..ef3e4be8 --- /dev/null +++ b/FairyPlugin/agp3_0_1/host.gradle @@ -0,0 +1,816 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//在宿主脚本中apply此脚本, 此脚本一共做了6件事 +//1、编译完成后导出所有资源id,即aaptOptions.additionalParameters这个配置干的事情, 下面第4步需要此文件 +//2、编译宿主资源时插入public.xml,用来控制宿主资源id分组 +//3、指定插件进程名称,导出.ap_文件 +//4、编译完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 +//5、编译完成后导出宿主的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时需要此包 +//6、编译完成后导出宿主混淆后的jar,包括宿主的src和其依赖的所有class, 编译非独立插件时若插件需要混淆则需要此包 +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +//第1件事 +android.aaptOptions.additionalParameters("-P", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + configurations.innerPlugin.files.each { file -> + //收集插件文件地址 + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : ' + varName + " " + buildTypeName + " " + flavorName + " " + varDirName + + def scope = variant.getVariantData().getScope() + def mergeTask = tasks.getByName(scope.getMergeResourcesTask().name) + + //第2件事 + mergeTask.doLast { + String destPath = mergeTask.outputDir.absolutePath + '/values/'; + //如果是通过本地路径引入的此脚本 + if (buildscript.sourceFile != null) { + + def publicXmlPath = buildscript.sourceFile.getParentFile().absolutePath + '/public.xml' + println '编译宿主资源时插入' + publicXmlPath + ' 到 ' + destPath + ', 用来控制宿主资源id分组' + + if(!file(publicXmlPath).exists()) { + throw new FileNotFoundException("public.xml 文件未找到,请检查路径:" + (buildscript.sourceFile.getParentFile().absolutePath + '/public.xml')) + } + + copy { + from(buildscript.sourceFile.getParentFile()) { + include 'public.xml' + } + into(destPath) + } + } else { + String url = buildscript.sourceURI.toString().replaceFirst("[a-zA-Z\\.]*\$", "public.xml") + println '编译宿主资源时插入' + url + ' 到' + destPath + ', 用来控制宿主资源id分组' + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + File destPublicXmlFile = new File(destPath, "public.xml") + File parentFile = destPublicXmlFile.getParentFile() + if (!parentFile.exists()) { + parentFile.mkdirs() + } + + OutputStream ouput =new FileOutputStream(destPublicXmlFile) + byte[] buffer = new byte[8*1024] + int size = -1 + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } + + if (!file(destPath + "public.xml").exists()) { + throw new FileNotFoundException("文件不存在:" + destPath + "public.xml") + } + } + + def mergeAssetsTask = tasks.getByName(scope.getMergeAssetsTask().name) + mergeAssetsTask.setOnlyIf { true } + mergeAssetsTask.outputs.upToDateWhen { false } + mergeAssetsTask.doLast { + + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + println '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + mergeAssetsTask.outputDir + from files(innerPluginApkList) + into(mergeAssetsTask.outputDir) + } + } + + //第3件事 + for (baseVariant in variant.outputs) { + + def manifestFilePath = baseVariant.processResources.manifestFile.absolutePath; + + baseVariant.processManifest.doLast { + + //android gradle3.+的属性 + //println "$manifestOutputDirectory" + + File manifestFile = new File(manifestFilePath) + + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + + //实际上最新版sdk和gradle可以直接指定apk了, 不需要.ap_文件 + def processResourcesTask = baseVariant.getProcessResources(); + //def processResFullName = baseVariant.apkData.fullName + processResourcesTask.doLast { + copy { + println '编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println processResourcesTask.resPackageOutputFolder + from processResourcesTask.resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + } + + //第5件事 + def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile; + javaCompile.doLast { + HashMap jarSets = new HashMap(); + jarSets.put("android.jar", "") + + println "Merge Jar After Task " + javaCompile.name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + javaCompile.classpath.each { jarPath -> + String jarName = jarPath.getName(); + if(!jarSets.keySet().contains(jarName)) { + println "addJar path: $jarPath" + jarMerger.addJar(jarPath); + if (!jarName.equals("classes.jar") && jarPath.absolutePath.endsWith("/libs/" + jarName)) { + jarSets.put(jarName, jarPath.absolutePath) + } + } else { + println "ignore jar: $jarPath \n\\------ because of : " + jarSets.get(jarName) + } + } + + String buildClassesTaskName1 = "jar" + varName.capitalize() + "Classes"; + String classesPath1 = 'intermediates/packaged/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar"; + + String buildClassesTaskName2 = "bundleAppClasses" + varName.capitalize(); + String classesPath2 = 'intermediates/classes-jar/' + (flavorName.equals("")?"":(flavorName + "/")) + buildTypeName + "/classes.jar" + + def buildClassesTaskName; + def classesPath; + + if (tasks.findByName(buildClassesTaskName1)) { + buildClassesTaskName = buildClassesTaskName1; + classesPath = classesPath1; + } else if (tasks.findByName(buildClassesTaskName2)) { + buildClassesTaskName = buildClassesTaskName2; + classesPath = classesPath2; + } else { + throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本") + } + + File classes = new File(buildDir, classesPath); + + println "classes path is " + classes.absolutePath + println "buildClassesTaskName is " + buildClassesTaskName + + if (!classes.exists()) { + try { + tasks.getByName(buildClassesTaskName).execute() + } catch(Exception e) { + println "fail to create jar for task " + javaCompile.name + " " + buildClassesTaskName + } + } else { + println "classes path already exists: " + classes.absolutePath + } + if (classes.exists()) { + println "addJar path: $classes.absolutePath" + jarMerger.addJar(classes) + } + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + + //第6件事 + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}") + if (proguardTask) { + proguardTask.doFirst { + println "开始混淆任务:" + varName.capitalize() + } + proguardTask.doLast { + println "混淆完成:" + varName.capitalize() + boolean isFind = false; + proguardTask.outputs.files.files.each { File file-> + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + project.logger.error "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + + isFind = true; + def sourceHostObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + def hostObfuscatedJar = "host_obfuscated.jar" + println "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar + + copy { + from sourceHostObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return hostObfuscatedJar + } + }) + } + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + + //第4件事 + tasks.findByName("generate${varName.capitalize()}Sources").doFirst { + + println '编译宿主ap完成后根据资源中间文件以及导出的资源id表生成一份主题patch包,编译非独立插件时需要此包 flavorName = ' + flavorName + ", buildTypeName = " + buildTypeName + + createThemePatch(flavorName, buildTypeName); + + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 +task makeHostBaseLine(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + from zipTree("build/outputs/resources.ap_") + from('build/outputs') { + exclude '*.ap_' + } +} + +//导出主题patch +def createThemePatch(String flavor, String buildType) { + + File patchDir = new File(project.buildDir.absolutePath + "/outputs/theme_patch/" + buildType); + patchDir.mkdirs(); + + File generatedRes = new File(project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml"); + File dest = new File(patchDir, "patch_theme.xml") + + println "export from " + generatedRes + " to " + dest + + if (!generatedRes.exists()) { + throw new FileNotFoundException("File Not Found : " + generatedRes.absolutePath) + } + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + ThemeProcessor.exportThemeStyle(generatedRes, dest, packageName) + + String mergedResDir = "${project.buildDir}/intermediates/res/merged/" + (flavor.equals("")?"":(flavor + "/")) + buildType + "/"; + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', 'values-v1*/values-v1*.xml', 'values-v2*/values-v2*.xml', 'values-*-v1*/values-*-v1*.xml', 'values-*-v4/values-*-v4.xml', 'values-land/values-land.xml', 'values-*-v2*/values-*-v2*.xml', 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + dest = new File(patchDir, 'patch_' + itemFile.name) + + println "export from " + itemFile + " to " + dest + + ThemeProcessor.exportThemeStyle(itemFile, dest, packageName) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName) { + this.destFile = destFile; + this.packageName = packageName; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + } + } else { + outXmlStream.write(tag); + } + } else { + outXmlStream.write(tag); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + + this.jarOutputStream.putNextEntry(newEntry); + + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} \ No newline at end of file diff --git a/FairyPlugin/agp3_0_1/plugin.gradle b/FairyPlugin/agp3_0_1/plugin.gradle new file mode 100644 index 00000000..d37dab2e --- /dev/null +++ b/FairyPlugin/agp3_0_1/plugin.gradle @@ -0,0 +1,920 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.* + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.aaptOptions.additionalParameters( + '-I', currentSelectedBar) + +HashMap>> declareStyleMap = new HashMap>>(); + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + + println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName + + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", hostBarExtraRootDir) + } + + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + def hostBuildType = props.getProperty("host.buildType") + + //第二件事 + applyThemePatch(hostBuildType, varDirName, hostBarExtraRootDir); + + //第三件事<一> + File originVaules = new File(project.buildDir, "intermediates/res/merged/" + varDirName + "/values/values.xml"); + if (originVaules.exists()) { + + File valuesProcessed = new File(project.buildDir, "intermediates/res/merged/" + varDirName + "/values/values_processed.xml"); + + println "fixDeclareStyle " + originVaules + ", " + valuesProcessed.absolutePath + + StyleProcessor styleProcessor = StyleProcessor.fixDeclareStyle(originVaules, valuesProcessed); + + SortedMap> map = styleProcessor.attrList; + if (map != null) { + declareStyleMap.put(varDirName, map) + } + originVaules.delete() + + //第四件事,修正layout文件中的自定义属性的枚举用法 + File resDir = new File(project.buildDir, "intermediates/res/merged/" + varDirName); + resDir.eachFileRecurse({file -> + if (file.absolutePath.contains("/layout") && file.name.endsWith(".xml")) { + String fileText = file.text; + Iterator> itr = styleProcessor.enumItemList.iterator(); + println "try fix enum attr for " + file.absolutePath + while(itr.hasNext()) { + Map.Entry entryItem = itr.next(); + fileText = fileText.replaceAll(entryItem.key, entryItem.value) + } + + println "try fix flag attr for " + file.absolutePath + fileText = StyleProcessor.fixAttrFlag(fileText, styleProcessor.attrFlagMap); + + file.write(fileText); + } + } + ) + } else { + println "没有需要处理的DeclareStyle资源" + } + } + + def generateSourcesTask = tasks.findByName("generate${varName.capitalize()}Sources"); + generateSourcesTask.doLast { + //第三件事<二> + FileTree rfiles = fileTree(dir: project.buildDir.absolutePath + "/generated/source/r/"+ varDirName, include: ['**/R.java']) + rfiles.each { File itemFile -> + + File newR = new File(itemFile.getAbsolutePath()+ "_temp"); + File originR = itemFile; + + println "处理R.java文件,生成decalre-style , \n" + newR.absolutePath + ", \n" + originR.absolutePath + + newR.delete() + SortedMap> map = declareStyleMap.get(varDirName); + + if (originR.exists() && map != null) { + BufferedReader br = new BufferedReader(new FileReader(originR)); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newR), "UTF-8")); + String srcLine = null; + boolean hasStyleable = false; + String packageStr = ""; + while((srcLine = br.readLine()) != null) { + if (srcLine.equals("public final class R {")) { + bw.writeLine(srcLine); + + bw.writeLine(" public static final class styleable {"); + Iterator>> itr = map.iterator(); + while(itr.hasNext()) { + + Map.Entry> item = itr.next(); + String key = item.key; + StringBuilder sb = new StringBuilder(); + + println "Generate styleable " + key + + for(int i = 0; i < item.value.size(); i++) { + bw.writeLine(" public static int " + key + "_" + item.value.get(i) + "=" + i + ";"); + sb.append(packageStr).append(".R.attr.").append(item.value.get(i)).append(",") + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length()-1) + } + bw.writeLine(" public static int[] " + key + " = {" + sb.toString() + "};"); + } + bw.writeLine(" }") + } else if (srcLine.contains("public static final class styleable {")) { + hasStyleable = true; + } else if (hasStyleable && srcLine.trim().equals("}")) { + hasStyleable = false; + } else { + if (srcLine.startsWith("package ") && srcLine.endsWith(";")) { + if(isApplicationModule) { + packageStr = android.defaultConfig.applicationId + } else { + packageStr = srcLine.replace("package ", "").replace(";", ""); + } + } + bw.writeLine(srcLine); + } + } + bw.close() + br.close() + + originR.delete() + newR.renameTo(originR) + } else { + println "Nothing to Fix for " + originR.absolutePath + } + } + } + + if (isApplicationModule) { + for (baseVariant in variant.outputs) { + + baseVariant.processResources.doFirst { + println "开始处理插件工程资源.." + if (!file(hostBarExtraRootDir).exists()) { + throw new FileNotFoundException("宿主基线patch文件不存在! 请宿主编译完成后,再编译插件,并检查插件编译脚本的baselinePatch依赖配置是否正确: \n", hostBarExtraRootDir) + } + } + + baseVariant.processManifest.doLast { + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + def hostVersionName = null + def hostVersionCode = null + def hostApplicationId = null + def hostBuildType = null + def hostFlavorName = null + if (propFile.exists()) { + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + hostVersionName = props.getProperty("host.versionName") + hostVersionCode = props.getProperty("host.versionCode") + hostBuildType = props.getProperty("host.buildType") + hostFlavorName = props.getProperty("host.flavorName") + hostApplicationId = props.getProperty("host.applicationId") + + println "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName + + } else { + throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath) + } + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest文件中插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插件Manifest文件中插入非独立插件标识 requiredHostApplicationId=" + hostApplicationId + def manifestFile = new File("$manifestOutputDirectory/AndroidManifest.xml") + println "插件Manifest:" + manifestFile.absolutePath + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + println "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + isFind = true; + def pluginObfuscatedJar = "plugin_obfuscated.jar" + def sourcePluginObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + //保留一个备份 + println "导出混淆后的插件jar包 " + println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar + println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar + copy { + from sourcePluginObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return pluginObfuscatedJar + } + }) + } + + diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar); + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + } + } +} + +def diffJar(String plugin, String host) { + + + println "通过基线混淆包和插件混淆包做diff清理重复的class" + + println "插件:" + plugin + ", 宿主:" + host + + println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/" + + ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/") + List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/") + + println "再删掉插件的源 " + plugin + new File(plugin).delete() + + println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName() + ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses) + println "重新生成的插件的源 " + plugin + + //备份diff后的包 + copy { + println "备份diff后重新生成的插件的源包到插件out目录" + println "From " + plugin + println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar" + from plugin + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return "plugin_obfuscated_after_diff.jar" + } + }) + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} + +def applyThemePatch(hostBuildType, varDirName, hostPatchExtractDir) { + + def fromDir = hostPatchExtractDir + "/theme_patch/" + hostBuildType; + def resourceDir = project.buildDir.absolutePath + "/intermediates/res/merged/" + varDirName; + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + resourceDir); + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + + String dest; + if (itemFile.name.equals('patch_theme.xml')) { + dest = resourceDir + '/values/' + } else { + dest = resourceDir + '/' + itemFile.name.replace('patch_', '').replace('.xml', '') + "/" + } + + println "apply theme_patch from " + itemFile + " to " + dest + copy { + from(fromDir) { + include itemFile.name + } + into(dest) + } + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} \ No newline at end of file diff --git a/FairyPlugin/agp3_0_1/public.xml b/FairyPlugin/agp3_0_1/public.xml new file mode 100644 index 00000000..7316a33f --- /dev/null +++ b/FairyPlugin/agp3_0_1/public.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FairyPlugin/agp3_2_1/host.gradle b/FairyPlugin/agp3_2_1/host.gradle new file mode 100644 index 00000000..19373463 --- /dev/null +++ b/FairyPlugin/agp3_2_1/host.gradle @@ -0,0 +1,858 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +if ("false".equals(rootProject.properties.get("android.enableAapt2"))) { + throw new Exception("请开启aapt2编译开关:android.enableAapt2") +} + +//generateSourcess时借此文件生成attrs.xml +android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getMergeResources().doLast { + + project.logger.error '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //宿主资源id分组就靠这个文件了 + def String publicAttrrsXml = "res/values/public_attrs.xml" ; + def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath; + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile); + + if(!file(publicAttrrsXmlFile).exists()) { + throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile) + } + + String mergeResourcesOutPath = outputDir.absolutePath; + + //需要确保aapt2在环境变量中,否则可能会执行失败 + //Android Studio环境中的aapt2和命令行中的可能不是同一个版本, + //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2 + //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2 + //如果不是同一个版本可能出现Android Resource Link错误、magic number错误 + //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3 + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile") + "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor() + + //检查aapt2是否执行成功 + if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) { + throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,请检查路径:$publicAttrrsXmlFile") + } + + //生成values_public_attrs这个文件以后,publicAttrrsXmlFile文件已经无用了, + //接下来修改内容需要为生成一个public attr + String text = file(publicAttrrsXmlFile).text + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + file(publicAttrrsXmlFile).write(text) + } + + def mergeAssetsTask = variant.getMergeAssets() + mergeAssetsTask.setOnlyIf { true } + mergeAssetsTask.outputs.upToDateWhen { false } + mergeAssetsTask.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir + from files(innerPluginApkList) + into(outputDir) + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println "from :" + resPackageOutputFolder + from resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + def org.gradle.api.tasks.compile.JavaCompile javaCompile = variant.javaCompile; + javaCompile.doLast { + + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + //jarMerger.addFolder(directoryInput.getFile()); + } + + def buildClassesTaskName = "packageAppClasses" + varName.capitalize(); + def classesPath= 'intermediates/app_classes/' + varName + "/" + buildClassesTaskName + "/classes.jar" + + if (!tasks.findByName(buildClassesTaskName)) { + throw new IllegalAccessError("未找到打包宿主classes的task,请检查android gradle 插件版本") + } + + File classes = new File(buildDir, classesPath); + + if (!classes.exists()) { + try { + tasks.getByName(buildClassesTaskName).execute() + } catch(Exception e) { + projects.logger.log(LogLevel.ERROR, "fail to create jar for task " + name + " " + buildClassesTaskName) + } + } else { + projects.logger.log(LogLevel.DEBUG, "classes path already exists: " + classes.absolutePath) + } + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + projects.logger.log(LogLevel.ERROR, "Not exists : classes file path is " + classes.absolutePath) + projects.logger.log(LogLevel.ERROR, "Not exists : build classes task name is " + buildClassesTaskName) + } + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}") + if (proguardTask) { + proguardTask.doFirst { + println "开始混淆任务:" + varName.capitalize() + } + proguardTask.doLast { + println "混淆完成:" + varName.capitalize() + boolean isFind = false; + proguardTask.outputs.files.files.each { File file-> + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + project.logger.error "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + + isFind = true; + def sourceHostObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + def hostObfuscatedJar = "host_obfuscated.jar" + project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar + + copy { + from sourceHostObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + project.logger.error "rename:" + s + return hostObfuscatedJar + } + }) + } + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + + //修改public_attrs的内容,填充attr的id + tasks.findByName("generate${varName.capitalize()}Sources").doLast { + def f = file(buildDir.absolutePath + "/outputs/theme_patch/" + varDirName + "/res/values/public_attrs.xml") + project.logger.error "update public attrs ids, file=" + f.absolutePath + def String text = f.text + if (f.exists()) { + file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el-> + if (el.contains(":attr/")) { + String[] attrIdKeyValue = el.split("/")[1].split(" = "); + text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">") + } + } + f.write(text) + } else { + throw new Exception(f.absolutePath + " Not Found!") + } + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy makeHostBaseLine + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 +task makeHostBaseLine(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-land/values-land.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + + //通过values.xml生成publicAttrsXml + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName, + itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + File attrFile; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName, File attrFile) { + this.destFile = destFile; + this.packageName = packageName; + this.attrFile = attrFile; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + + + if (attrFile != null) { + attrFile.getParentFile().mkdirs() + outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8")); + outPublicAttrStream.write(""); + outPublicAttrStream.write("\n"); + outPublicAttrStream.write("\n"); + //唯独attr仍然使用010000开头,是为了和插件中的attr同组 + outPublicAttrStream.write("\n \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n") + } + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + + this.jarOutputStream.putNextEntry(newEntry); + + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} \ No newline at end of file diff --git a/FairyPlugin/agp3_2_1/plugin.gradle b/FairyPlugin/agp3_2_1/plugin.gradle new file mode 100644 index 00000000..587c0e74 --- /dev/null +++ b/FairyPlugin/agp3_2_1/plugin.gradle @@ -0,0 +1,801 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.* + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.aaptOptions.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id') + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + + println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName + + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + def hostVarDirName = props.getProperty("host.varDirName") + if (hostVarDirName == null) { + throw new Exception("HostInfo.prop文件信息不全,请检查此文件内容:" + propFile.absolutePath) + } + //应用宿主的主题包 + applyThemePatch(hostVarDirName, varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + def hostVersionName = null + def hostVersionCode = null + def hostApplicationId = null + def hostBuildType = null + def hostFlavorName = null + def hostVarName = null + def hostVarDirName = null + if (propFile.exists()) { + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + hostVersionName = props.getProperty("host.versionName") + hostVersionCode = props.getProperty("host.versionCode") + hostBuildType = props.getProperty("host.buildType") + hostFlavorName = props.getProperty("host.flavorName") + hostVarName = props.getProperty("host.varName") + hostVarDirName = props.getProperty("host.varDirName") + + hostApplicationId = props.getProperty("host.applicationId") + + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath) + } + } + + fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + println "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + isFind = true; + def pluginObfuscatedJar = "plugin_obfuscated.jar" + def sourcePluginObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + //保留一个备份 + project.logger.error "导出混淆后的插件jar包 " + println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar + println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar + copy { + from sourcePluginObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return pluginObfuscatedJar + } + }) + } + + diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar); + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + } + } +} + +def diffJar(String plugin, String host) { + + + println "通过基线混淆包和插件混淆包做diff清理重复的class" + + println "插件:" + plugin + ", 宿主:" + host + + println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/" + + ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/") + List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/") + + println "再删掉插件的源 " + plugin + new File(plugin).delete() + + println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName() + ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses) + println "重新生成的插件的源 " + plugin + + //备份diff后的包 + copy { + println "备份diff后重新生成的插件的源包到插件out目录" + println "From " + plugin + println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar" + from plugin + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return "plugin_obfuscated_after_diff.jar" + } + }) + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} + +def applyThemePatch(hostVarDirName, varName, varDir, hostPatchExtractDir) { + + def fromDir = hostPatchExtractDir + "/theme_patch/" + hostVarDirName; + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp3_3_1/host.gradle b/FairyPlugin/agp3_3_1/host.gradle new file mode 100644 index 00000000..050be0fc --- /dev/null +++ b/FairyPlugin/agp3_3_1/host.gradle @@ -0,0 +1,1046 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.CRC32 +import java.util.zip.CheckedOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +if ("false".equals(rootProject.properties.get("android.enableAapt2"))) { + throw new Exception("请开启aapt2编译开关:android.enableAapt2") +} + +//generateSourcess时借此文件生成attrs.xml +android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getMergeResourcesProvider().configure { + it.doLast { + + project.logger.error '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //宿主资源id分组就靠这个文件了 + def String publicAttrrsXml = "res/values/public_attrs.xml" ; + def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath; + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile); + + if(!file(publicAttrrsXmlFile).exists()) { + throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile) + } + + String mergeResourcesOutPath = outputDir.absolutePath; + + //需要确保aapt2在环境变量中,否则可能会执行失败 + //Android Studio环境中的aapt2和命令行中的可能不是同一个版本, + //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2 + //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2 + //如果不是同一个版本可能出现Android Resource Link错误、magic number错误 + //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3 + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + //把这个文件插入宿主的资源编译过程中是为了给宿主资源id分组,使得宿主资源id与将来的插件资源id不冲突 + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile") + "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor() + + //检查aapt2是否执行成功 + if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) { + throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,影响宿主资源ID分组设置,请检查路径:$publicAttrrsXmlFile") + } + } + } + + variant.getMergeAssetsProvider().configure { + it.setOnlyIf { true } + it.outputs.upToDateWhen { false } + it.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir + from files(innerPluginApkList) + into(outputDir) + } + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + //修改此文件的内容,主要是设置用于id分组的id值,填充以及attr的值, 将来编译插件时需要此文件 + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + def String publicAttrrsXmlFile = new File(patchDir.absolutePath, "res/values/public_attrs.xml").absolutePath; + project.logger.error "update public attrs ids, file=" + publicAttrrsXmlFile + String text = file(publicAttrrsXmlFile).text + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el-> + if (el.contains(":attr/")) { + String[] attrIdKeyValue = el.split("/")[1].split(" = "); + text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">") + } + } + file(publicAttrrsXmlFile).write(text) + + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println "from :" + resPackageOutputFolder + from resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + variant.javaCompileProvider.configure { + it.doLast { + + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + if (jarPath.isDirectory()) { + println "adding folder " + jarPath + jarMerger.addFolder(jarPath); + } else { + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + } + } + + File classes = new File(destinationDir.getParent(), "classes.jar"); + classes.delete(); + + try { + ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList()) + } catch(Exception e) { + throw new Exception("fail to create jar for app classes ") + } + + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + throw new Exception("Not exists : classes file path is " + classes.absolutePath) + } + + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + } + + if (variant.buildType["minifyEnabled"]) { + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${varName.capitalize()}") + proguardTask.doFirst { + println "开始混淆任务:" + varName.capitalize() + } + proguardTask.doLast { + println "混淆完成:" + varName.capitalize() + boolean isFind = false; + proguardTask.outputs.files.files.each { File file-> + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + project.logger.error "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + + isFind = true; + def sourceHostObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + def hostObfuscatedJar = "host_obfuscated.jar" + project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar + + copy { + from sourceHostObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + project.logger.error "rename:" + s + return hostObfuscatedJar + } + }) + } + } + } + if (!isFind) { + throw new Exception("obfuscated jar file not found, please check.") + } + } + } + + + //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 + task "makeHostBaseLine${varName.capitalize()}"(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + version android.defaultConfig.versionName + classifier "${varName.capitalize()}" + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}" + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-land/values-land.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + + //通过values.xml生成publicAttrsXml + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName, + itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + File attrFile; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName, File attrFile) { + this.destFile = destFile; + this.packageName = packageName; + this.attrFile = attrFile; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + + + if (attrFile != null) { + attrFile.getParentFile().mkdirs() + outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8")); + outPublicAttrStream.write(""); + outPublicAttrStream.write("\n"); + outPublicAttrStream.write("\n"); + //唯独attr仍然使用010000开头,是为了和插件中的attr同组 + outPublicAttrStream.write("\n \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n") + } + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + if (tag.contains("type=\"reference\"")) { + tag = tag.replace("type=\"reference\"", " "); + } + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + + this.jarOutputStream.putNextEntry(newEntry); + + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/FairyPlugin/agp3_3_1/plugin.gradle b/FairyPlugin/agp3_3_1/plugin.gradle new file mode 100644 index 00000000..e3ae8bb6 --- /dev/null +++ b/FairyPlugin/agp3_3_1/plugin.gradle @@ -0,0 +1,811 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.* + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.aaptOptions.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id') + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + + println "开始merge插件工程资源:" + hostBarExtraRootDir + " 到" + varDirName + + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + def hostVarDirName = props.getProperty("host.varDirName") + if (hostVarDirName == null) { + throw new Exception("HostInfo.prop文件信息不全,请检查此文件内容:" + propFile.absolutePath) + } + //应用宿主的主题包 + applyThemePatch(hostVarDirName, varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + tasks.findByName("compile${variant.name.capitalize()}Sources").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/app_classes/' + varName +'/bundle' + varName.capitalize() + 'Classes') { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + def hostVersionName = null + def hostVersionCode = null + def hostApplicationId = null + def hostBuildType = null + def hostFlavorName = null + def hostVarName = null + def hostVarDirName = null + if (propFile.exists()) { + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + hostVersionName = props.getProperty("host.versionName") + hostVersionCode = props.getProperty("host.versionCode") + hostBuildType = props.getProperty("host.buildType") + hostFlavorName = props.getProperty("host.flavorName") + hostVarName = props.getProperty("host.varName") + hostVarDirName = props.getProperty("host.varDirName") + + hostApplicationId = props.getProperty("host.applicationId") + + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("HostInfo.prop文件丢失,请检查此路径:" + propFile.absolutePath) + } + } + + fileTree(manifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + println "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if (file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) { + isFind = true; + def pluginObfuscatedJar = "plugin_obfuscated.jar" + def sourcePluginObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } + //保留一个备份 + project.logger.error "导出混淆后的插件jar包 " + println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录,最新版文件名即是0.jar, 旧版叫main.jar + println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar + copy { + from sourcePluginObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return pluginObfuscatedJar + } + }) + } + + diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar); + } + } + if (!isFind) { + throw "obfuscated jar file not found, please check." + } + } + } + } + } +} + +def diffJar(String plugin, String host) { + + + println "通过基线混淆包和插件混淆包做diff清理重复的class" + + println "插件:" + plugin + ", 宿主:" + host + + println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/" + + ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/") + List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/") + + println "再删掉插件的源 " + plugin + new File(plugin).delete() + + println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + " " + file(plugin).getName() + ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), "0.jar", hostClasses) + println "重新生成的插件的源 " + plugin + + //备份diff后的包 + copy { + println "备份diff后重新生成的插件的源包到插件out目录" + println "From " + plugin + println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar" + from plugin + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return "plugin_obfuscated_after_diff.jar" + } + }) + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} + +def applyThemePatch(hostVarDirName, varName, varDir, hostPatchExtractDir) { + + def fromDir = hostPatchExtractDir + "/theme_patch/" + hostVarDirName; + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp3_3_2/host.gradle b/FairyPlugin/agp3_3_2/host.gradle new file mode 100644 index 00000000..a8200819 --- /dev/null +++ b/FairyPlugin/agp3_3_2/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_3_2/plugin.gradle b/FairyPlugin/agp3_3_2/plugin.gradle new file mode 100644 index 00000000..8b2632df --- /dev/null +++ b/FairyPlugin/agp3_3_2/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_0/host.gradle b/FairyPlugin/agp3_4_0/host.gradle new file mode 100644 index 00000000..a8200819 --- /dev/null +++ b/FairyPlugin/agp3_4_0/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_0/plugin.gradle b/FairyPlugin/agp3_4_0/plugin.gradle new file mode 100644 index 00000000..8b2632df --- /dev/null +++ b/FairyPlugin/agp3_4_0/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_1/host.gradle b/FairyPlugin/agp3_4_1/host.gradle new file mode 100644 index 00000000..a8200819 --- /dev/null +++ b/FairyPlugin/agp3_4_1/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_1/plugin.gradle b/FairyPlugin/agp3_4_1/plugin.gradle new file mode 100644 index 00000000..8b2632df --- /dev/null +++ b/FairyPlugin/agp3_4_1/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_2/host.gradle b/FairyPlugin/agp3_4_2/host.gradle new file mode 100644 index 00000000..a8200819 --- /dev/null +++ b/FairyPlugin/agp3_4_2/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_4_2/plugin.gradle b/FairyPlugin/agp3_4_2/plugin.gradle new file mode 100644 index 00000000..8b2632df --- /dev/null +++ b/FairyPlugin/agp3_4_2/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_5_0/host.gradle b/FairyPlugin/agp3_5_0/host.gradle new file mode 100644 index 00000000..a8200819 --- /dev/null +++ b/FairyPlugin/agp3_5_0/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_5_0/plugin.gradle b/FairyPlugin/agp3_5_0/plugin.gradle new file mode 100644 index 00000000..8b2632df --- /dev/null +++ b/FairyPlugin/agp3_5_0/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp3_5_3/host.gradle b/FairyPlugin/agp3_5_3/host.gradle new file mode 100644 index 00000000..baccaab9 --- /dev/null +++ b/FairyPlugin/agp3_5_3/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/host.gradle" diff --git a/FairyPlugin/agp3_5_3/plugin.gradle b/FairyPlugin/agp3_5_3/plugin.gradle new file mode 100644 index 00000000..8e5a4dda --- /dev/null +++ b/FairyPlugin/agp3_5_3/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp3_3_1/plugin.gradle" diff --git a/FairyPlugin/agp4_1_3/host.gradle b/FairyPlugin/agp4_1_3/host.gradle new file mode 100644 index 00000000..45f0bc2a --- /dev/null +++ b/FairyPlugin/agp4_1_3/host.gradle @@ -0,0 +1,1062 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.CRC32 +import java.util.zip.CheckedOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +if ("false".equals(rootProject.properties.get("android.enableAapt2"))) { + throw new Exception("请开启aapt2编译开关:android.enableAapt2") +} + +//generateSourcess时借此文件生成attrs.xml +android.aaptOptions.additionalParameters("--emit-ids", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getMergeResourcesProvider().configure { + it.doLast { + + project.logger.error '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //宿主资源id分组就靠这个文件了 + def String publicAttrrsXml = "res/values/public_attrs.xml" ; + def String publicAttrrsXmlFile = new File(patchDir.absolutePath, publicAttrrsXml).absolutePath; + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir, publicAttrrsXmlFile); + + if(!file(publicAttrrsXmlFile).exists()) { + throw new FileNotFoundException("createThemePatch 失败,public_attrs.xml 文件未生成,请检查路径:" + publicAttrrsXmlFile) + } + + String mergeResourcesOutPath = outputDir.orNull; + + //需要确保aapt2在环境变量中,否则可能会执行失败 + //Android Studio环境中的aapt2和命令行中的可能不是同一个版本, + //idea使用的版本位于~/.gradle/caches/transforms-1/files-1.1/aapt2-**-osx.jar/**/aapt2-**/aapt2 + //sdk的版本位于~/Library/Android/sdk/build-tools/28.0.3//aapt2 + //如果不是同一个版本可能出现Android Resource Link错误、magic number错误 + //因此还需要保证环境变量中的aapt2和idea中使用的是相同的版本,并不低于28.0.3 + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + //把这个文件插入宿主的资源编译过程中是为了给宿主资源id分组,使得宿主资源id与将来的插件资源id不冲突 + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile") + "$aapt2Exe compile -o $mergeResourcesOutPath $publicAttrrsXmlFile".execute().waitFor() + + //检查aapt2是否执行成功 + if(!file(mergeResourcesOutPath + "/values_public_attrs.arsc.flat").exists()) { + throw new FileNotFoundException("values_public_attrs.arsc.flat 文件未生成,影响宿主资源ID分组设置,请检查路径:$publicAttrrsXmlFile") + } + } + } + + variant.getMergeAssetsProvider().configure { + it.setOnlyIf { true } + it.outputs.upToDateWhen { false } + it.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull + from files(innerPluginApkList) + into(outputDir) + } + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + //修改此文件的内容,主要是设置用于id分组的id值,填充以及attr的值, 将来编译插件时需要此文件 + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + def String publicAttrrsXmlFile = new File(patchDir.absolutePath, "res/values/public_attrs.xml").absolutePath; + project.logger.error "update public attrs ids, file=" + publicAttrrsXmlFile + String text = file(publicAttrrsXmlFile).text + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + file(buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties").eachLine { el-> + if (el.contains(":attr/")) { + String[] attrIdKeyValue = el.split("/")[1].split(" = "); + text = text.replace("name=\"" + attrIdKeyValue[0] +"\">", "name=\"" + attrIdKeyValue[0] +"\" id=\"" + attrIdKeyValue[1] + "\">") + } + } + file(publicAttrrsXmlFile).write(text) + + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println "from :" + resPackageOutputFolder + from resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + variant.javaCompileProvider.configure { + it.doLast { + + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + if (jarPath.isDirectory()) { + println "adding folder " + jarPath + jarMerger.addFolder(jarPath); + } else { + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + } + } + + File classes = new File(destinationDir.getParent(), "classes.jar"); + classes.delete(); + + try { + ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList()) + } catch(Exception e) { + throw new Exception("fail to create jar for app classes ") + } + + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + throw new Exception("Not exists : classes file path is " + classes.absolutePath) + } + + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + } + + if (variant.buildType.minifyEnabled) { + if (!"false".equals(rootProject.properties.get("android.enableR8"))) { + throw new Exception("请关闭R8:android.enableR8=false") + } + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("transformClassesAndResourcesWithProguardFor${varName.capitalize()}") + if (proguardTask == null) { + proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithProguard") + } + if (proguardTask != null) { + proguardTask.doFirst { + println "开始混淆任务:" + varName.capitalize() + } + proguardTask.doLast { + println "混淆完成:" + varName.capitalize() + boolean isFind = false; + proguardTask.outputs.files.files.each { File file-> + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + project.logger.error "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println String.valueOf(file.absolutePath.contains(keyword)) + ", " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if ((file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) + || file.absolutePath.contains("/shrunk_jar/")) { + + isFind = true; + def sourceHostObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourceHostObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } else if (file.absolutePath.endsWith("/minified.jar")) { + sourceHostObfuscatedJar = file.absolutePath + } + def hostObfuscatedJar = "host_obfuscated.jar" + project.logger.error "导出混淆后的宿主jar " + sourceHostObfuscatedJar + " 包到 " + "${project.buildDir}/outputs/" + hostObfuscatedJar + + copy { + from sourceHostObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + project.logger.error "rename:" + s + return hostObfuscatedJar + } + }) + } + } + } + if (!isFind) { + throw new Exception("obfuscated jar file not found, please check.") + } + } + } + } + + + //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 + task "makeHostBaseLine${varName.capitalize()}"(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + version android.defaultConfig.versionName + classifier "${varName.capitalize()}" + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}" + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir, String publicAttrsXmlFile) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-land/values-land.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + + //通过values.xml生成publicAttrsXml + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName, + itemFile.name.equals("values.xml")?new File(publicAttrsXmlFile):null) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName, File attrFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName, attrFile)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + File attrFile; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName, File attrFile) { + this.destFile = destFile; + this.packageName = packageName; + this.attrFile = attrFile; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + + + if (attrFile != null) { + attrFile.getParentFile().mkdirs() + outPublicAttrStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(attrFile), "UTF-8")); + outPublicAttrStream.write(""); + outPublicAttrStream.write("\n"); + outPublicAttrStream.write("\n"); + //唯独attr仍然使用010000开头,是为了和插件中的attr同组 + outPublicAttrStream.write("\n \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n") + } + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + if (tag.contains("type=\"reference\"")) { + tag = tag.replace("type=\"reference\"", " "); + } + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + try { + this.jarOutputStream.putNextEntry(newEntry); + } catch(Exception duplicate){ + //类重复了,先忽略吧 + println "addJar putNextEntry fail: " + name + continue; + } + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} diff --git a/FairyPlugin/agp4_1_3/plugin.gradle b/FairyPlugin/agp4_1_3/plugin.gradle new file mode 100644 index 00000000..eaf28d9b --- /dev/null +++ b/FairyPlugin/agp4_1_3/plugin.gradle @@ -0,0 +1,806 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.* + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.aaptOptions.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id') + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + //读取宿主编译信息 + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + props.keySet().forEach { propKey-> + project.ext.setProperty(propKey, props.getProperty(propKey)) + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + if (!"false".equals(rootProject.properties.get("android.enableR8"))) { + throw new Exception("请关闭R8:android.enableR8=false") + } + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + //应用宿主的主题包 + applyThemePatch(varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + tasks.findByName("compile${variant.name.capitalize()}Sources").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + def hostVersionName = project.ext.getProperty("host.versionName") + def hostVersionCode = project.ext.getProperty("host.versionCode") + def hostApplicationId = project.ext.getProperty("host.applicationId") + def hostBuildType = project.ext.getProperty("host.buildType") + def hostFlavorName = project.ext.getProperty("host.flavorName") + def hostVarName = project.ext.getProperty("host.varName") + def hostVarDirName = project.ext.getProperty("host.varDirName") + if (hostApplicationId != null) { + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("宿主基线包信息丢失") + } + } + + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + //http://blog.csdn.net/sbsujjbcy/article/details/50839263 + //build/intermediates/transforms/proguard/anzhi/release/jars/3/1f/main.jar + //最新版本路径已发生变化;2017-11-12 + println "file outputs=>${file.absolutePath}" + String keyword = File.separator + "transforms" + File.separator + "proguard" + File.separator; + println "contains keyword " + keyword + " " + String.valueOf(file.absolutePath.contains(keyword)) + ", endsWith buildType " + buildTypeName + " " + String.valueOf(file.absolutePath.endsWith(buildTypeName)) + if ((file.absolutePath.contains(keyword) && file.absolutePath.endsWith(buildTypeName)) + || file.absolutePath.contains("/shrunk_jar/")) { + isFind = true; + def pluginObfuscatedJar = "plugin_obfuscated.jar" + def sourcePluginObfuscatedJar + if (new File(file.absolutePath + "/0.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/0.jar" + } else if (new File(file.absolutePath + "/jars/3/1f/main.jar").exists()) { + sourcePluginObfuscatedJar = file.absolutePath + "/jars/3/1f/main.jar" + } else if (file.absolutePath.endsWith("/minified.jar")) { + sourcePluginObfuscatedJar = file.absolutePath + } + //保留一个备份 + project.logger.error "导出混淆后的插件jar包 " + println "From " + sourcePluginObfuscatedJar //此文件是proguard任务的固定输出目录 + println "To " + "${project.buildDir}/outputs/" + pluginObfuscatedJar + copy { + from sourcePluginObfuscatedJar + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return pluginObfuscatedJar + } + }) + } + + diffJar(sourcePluginObfuscatedJar, host_obfuscated_jar); + } + } + if (!isFind) { + throw new Exception("obfuscated jar file not found, please check.") + } + } + } + } + } +} + +def diffJar(String plugin, String host) { + + + println "通过基线混淆包和插件混淆包做diff清理重复的class" + + println "插件:" + plugin + ", 宿主:" + host + + println "先把插件和宿主的都解压 " + "${project.buildDir}/tmp/jarUnzip/plugin/" + ", " + "${project.buildDir}/tmp/jarUnzip/host/" + + ZipUtil.unZip(plugin, "${project.buildDir}/tmp/jarUnzip/plugin/") + List hostClasses = ZipUtil.unZip(host, "${project.buildDir}/tmp/jarUnzip/host/") + + println "再删掉插件的源 " + plugin + new File(plugin).delete() + + println "通过压缩过滤重新生成插件的源,替换之前被删掉的源, host classSize = " + hostClasses.size() + ", " + file(plugin).getName() + ZipUtil.zip("${project.buildDir}/tmp/jarUnzip/plugin", file(plugin).getParent(), file(plugin).getName(), hostClasses) + println "重新生成的插件的源 " + plugin + + //备份diff后的包 + copy { + println "备份diff后重新生成的插件的源包到插件out目录" + println "From " + plugin + println "To " + "${project.buildDir}/outputs/plugin_obfuscated_after_diff.jar" + from plugin + into("${project.buildDir}/outputs/") + rename(new Transformer() { + @Override + String transform(String s) { + return "plugin_obfuscated_after_diff.jar" + } + }) + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} + +def applyThemePatch(varName, varDir, hostPatchExtractDir) { + println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir + + if (!file(hostPatchExtractDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir); + } + + def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName") + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/res/merged/" + varDir).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/res/merged/$varDir $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp4_2_1/host.gradle b/FairyPlugin/agp4_2_1/host.gradle new file mode 100644 index 00000000..32f59967 --- /dev/null +++ b/FairyPlugin/agp4_2_1/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/host.gradle" diff --git a/FairyPlugin/agp4_2_1/plugin.gradle b/FairyPlugin/agp4_2_1/plugin.gradle new file mode 100644 index 00000000..452b73d8 --- /dev/null +++ b/FairyPlugin/agp4_2_1/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/plugin.gradle" diff --git a/FairyPlugin/agp7_0_3/host.gradle b/FairyPlugin/agp7_0_3/host.gradle new file mode 100644 index 00000000..47d7f1cc --- /dev/null +++ b/FairyPlugin/agp7_0_3/host.gradle @@ -0,0 +1,953 @@ +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.CRC32 +import java.util.zip.CheckedOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容 + private String compatibleWithHostVersion = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } +} +extensions.create('fairy', FaiyExt) + +if ("false".equals(rootProject.properties.get("android.enableAapt2"))) { + throw new Exception("请开启aapt2编译开关:android.enableAapt2") +} + +//generateSourcess时借此文件生成attrs.xml +android.androidResources.additionalParameters( + '--emit-ids', project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties", + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_ids.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getMergeResourcesProvider().configure { + it.doLast { + project.logger.error '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir); + } + } + + variant.getMergeAssetsProvider().configure { + it.setOnlyIf { true } + it.outputs.upToDateWhen { false } + it.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull + from files(innerPluginApkList) + into(outputDir) + } + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doFirst { + File public_ids = file(project.buildDir.absolutePath + "/outputs/public_ids.properties") + if (!public_ids.exists()) { + def packageName = android.defaultConfig.applicationId + public_ids.write( + packageName + ":attr/public_static_final_host_attr_ = 0x7f010000\n" + + packageName + ":drawable/public_static_final_host_drawable_ = 0x7f320000\n" + + packageName + ":layout/public_static_final_host_layout_ = 0x7f330000\n" + + packageName + ":anim/public_static_final_host_anim_ = 0x7f340000\n" + + packageName + ":xml/public_static_final_host_xml_ = 0x7f350000\n" + + packageName + ":raw/public_static_final_host_raw_ = 0x7f360000\n" + + packageName + ":dimen/public_static_final_host_dimen_ = 0x7f370000\n" + + packageName + ":string/public_static_final_host_string_ = 0x7f380000\n" + + packageName + ":style/public_static_final_host_style_ = 0x7f390000\n" + + packageName + ":color/public_static_final_host_color_ = 0x7f3a0000\n" + + packageName + ":id/public_static_final_host_id_ = 0x7f3b0000\n" + + packageName + ":bool/public_static_final_host_bool_ = 0x7f3c0000\n" + + packageName + ":integer/public_static_final_host_int_ = 0x7f3d0000\n" + + packageName + ":array/public_static_final_host_array_ = 0x7f3e0000\n" + + packageName + ":menu/public_static_final_host_menu_ = 0x7f3f0000\n" + + packageName + ":mipmap/public_static_final_host_mipmap_ = 0x7f400000\n" + + packageName + ":animator/public_static_final_host_animator_ = 0x7f410000\n" + + packageName + ":fraction/public_static_final_host_fraction_ = 0x7f420000\n" + + packageName + ":font/public_static_final_host_font_ = 0x7f430000\n" + + packageName + ":plurals/public_static_final_host_plurals_ = 0x7f440000\n" + + packageName + ":interpolator/public_static_final_host_interpolator_ = 0x7f450000\n" + + packageName + ":transition/public_static_final_host_transition_ = 0x7f460000\n") + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + copy { + from buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties" + into buildDir.absolutePath + "/outputs/" + rename { "public_attrs.properties" } + filter { String line -> + (line.contains(":attr/") || line.contains("public_static_final_host")) ? line : null + } + } + + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println "from :" + resPackageOutputFolder + from resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + duplicatesStrategy = DuplicatesStrategy.WARN + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + variant.javaCompileProvider.configure { + it.doLast { + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + it.inputs.files.each { inputsFile-> + //println "inputs: " + inputsFile + } + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + if (jarPath.isDirectory()) { + println "adding folder " + jarPath + jarMerger.addFolder(jarPath); + } else { + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + } + } + + File classes = new File(destinationDir.getParent(), "classes.jar"); + classes.delete(); + + try { + ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList()) + } catch(Exception e) { + throw new Exception("fail to create jar for app classes ") + } + + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + throw new Exception("Not exists : classes file path is " + classes.absolutePath) + } + + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + } + + if (variant.buildType.minifyEnabled) { + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + proguardTask.doLast { + proguardTask.outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + copy { + from file.absolutePath + into("${project.buildDir}/outputs/minifyWithR8") + } + } + } + } + } + } + + //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 + task "makeHostBaseLine${varName.capitalize()}"(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + version android.defaultConfig.versionName + classifier "${varName.capitalize()}" + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}" + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-land/values-land.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v8/values-*-v8.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName) { + this.destFile = destFile; + this.packageName = packageName; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + try { + this.jarOutputStream.putNextEntry(newEntry); + } catch(Exception duplicate){ + //类重复了,先忽略吧 + println "addJar putNextEntry fail: " + name + continue; + } + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} diff --git a/FairyPlugin/agp7_0_3/plugin.gradle b/FairyPlugin/agp7_0_3/plugin.gradle new file mode 100644 index 00000000..30e731ff --- /dev/null +++ b/FairyPlugin/agp7_0_3/plugin.gradle @@ -0,0 +1,672 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.security.MessageDigest +import java.util.regex.Matcher +import java.util.regex.Pattern + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.androidResources.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id', + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties") + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + //读取宿主编译信息 + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + props.keySet().forEach { propKey-> + project.ext.setProperty(propKey, props.getProperty(propKey)) + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + //应用宿主的主题包 + applyThemePatch(varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + tasks.findByName("compile${variant.name.capitalize()}Sources").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + def hostVersionName = project.ext.getProperty("host.versionName") + def hostVersionCode = project.ext.getProperty("host.versionCode") + def hostApplicationId = project.ext.getProperty("host.applicationId") + def hostBuildType = project.ext.getProperty("host.buildType") + def hostFlavorName = project.ext.getProperty("host.flavorName") + def hostVarName = project.ext.getProperty("host.varName") + def hostVarDirName = project.ext.getProperty("host.varDirName") + if (hostApplicationId != null) { + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("宿主基线包信息丢失") + } + } + + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + (packageName + ":" + line.split([":"])[1]) + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + } + } + } + } + + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + if (project.ext.has("host.varName")) { + def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt"; + println "发现基线mapping文件: " + baselineMappingFile + } + proguardTask.doLast { + println "混淆完成 " + varName.capitalize() + def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8" + if (!new File(host_obfuscated_dex).exists()) { + project.logger.error "宿主基线混淆包不存在 " + host_obfuscated_dex + throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件") + } + println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex + + outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + diffDexes(file.absolutePath, host_obfuscated_dex) + } + } + } + } + } + } +} + +def String curl(String url, String md5, String dir) { + File file = new File(dir, url.substring(url.lastIndexOf("/") + 1)) + if (file.exists() && file.length() > 0) { + return file.absolutePath + } + println "downloading " + url + new File(dir).mkdirs(); + MessageDigest messageDigest = MessageDigest.getInstance("MD5") + messageDigest.reset() + try { + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + OutputStream ouput =new FileOutputStream(file) + byte[] buffer = new byte[8*1024] + int size + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + messageDigest.update(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } catch(Exception){} + byte[] bs = messageDigest.digest() + StringBuilder stringBuilder = new StringBuilder() + for(byte b : bs) { + String hex = Integer.toHexString(b & 0xFF) + if (hex.length() < 2) { + stringBuilder.append("0") + } + stringBuilder.append(hex) + } + if (!md5.equalsIgnoreCase(stringBuilder.toString())) { + println "download fail: " + url + " " + stringBuilder.toString() + file.delete() + } else { + println "download success: " + url + " " + stringBuilder.toString() + } + return file.absolutePath +} +def ArrayList basksmali(String exe, String src, String dest) throws Exception +{ + ArrayList result = new ArrayList<>() + new File(dest).mkdirs() + File srcDir = new File(src); + for (File dex : srcDir.listFiles()) { + var cmd = ("java -jar " + exe + " " + + "d " + + "-o " + dest + "/" + dex.name + " " + + dex.absolutePath) + consumeStream(cmd.execute().getInputStream()) + fileTree(dest + "/" + dex.name).forEach { it-> + var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "") + result.add(relativeFile) + } + } + return result; +} +def void smali(String exe, String src, String dest) throws Exception +{ + for (File f : file(src).listFiles()) { + file(dest + "/" + f.name).delete() + var cmd = ("java -jar " + exe + " " + + "a " + + "-o " + dest + "/" + f.name + " " + + f.absolutePath) + println cmd + consumeStream(cmd.execute().getInputStream()) + } +} +def consumeStream(InputStream is) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + final byte[] buffer = new byte[256] + int len + try { + while ((len = is.read(buffer)) > 0) { + outputStream.write(buffer, 0, len) + } + } catch(IOException){} + finally { + String msg = outputStream.toString() + if (msg != null && msg.length() > 0) { + println "***exe msg begin***" + println msg + println "***exe msg end***" + } + try { + outputStream.close() + is.close() + } catch(Exception){} + } +} +def deleteDuplicated(String plugin, ArrayList hostClasses) { + for(File dexDir : file(plugin).listFiles()) { + fileTree(dexDir.absolutePath).forEach { + var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "") + if (hostClasses.contains(relativeFile)) { + it.delete() + } + } + } +} +def diffDexes(String plugin, String host) { + println "通过基线混淆包和插件混淆包做diff清理重复的class" + println "插件:" + plugin + println "宿主:" + host + var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin") + ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host") + deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses) + smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin) +} + +def applyThemePatch(varName, varDir, hostPatchExtractDir) { + println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir + + if (!file(hostPatchExtractDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir); + } + + def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName") + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp7_1_3/host.gradle b/FairyPlugin/agp7_1_3/host.gradle new file mode 100644 index 00000000..47ff37b3 --- /dev/null +++ b/FairyPlugin/agp7_1_3/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_0_3/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp7_1_3/plugin.gradle b/FairyPlugin/agp7_1_3/plugin.gradle new file mode 100644 index 00000000..0921b4f6 --- /dev/null +++ b/FairyPlugin/agp7_1_3/plugin.gradle @@ -0,0 +1,672 @@ +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.security.MessageDigest +import java.util.regex.Matcher +import java.util.regex.Pattern + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +android.androidResources.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id', + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties") + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + //读取宿主编译信息 + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + props.keySet().forEach { propKey-> + project.ext.setProperty(propKey, props.getProperty(propKey)) + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + //应用宿主的主题包 + applyThemePatch(varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + tasks.findByName("bundle${varName.capitalize()}Classes").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/app_classes/' + varName) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}Classes") + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + def hostVersionName = project.ext.getProperty("host.versionName") + def hostVersionCode = project.ext.getProperty("host.versionCode") + def hostApplicationId = project.ext.getProperty("host.applicationId") + def hostBuildType = project.ext.getProperty("host.buildType") + def hostFlavorName = project.ext.getProperty("host.flavorName") + def hostVarName = project.ext.getProperty("host.varName") + def hostVarDirName = project.ext.getProperty("host.varDirName") + if (hostApplicationId != null) { + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("宿主基线包信息丢失") + } + } + + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + (packageName + ":" + line.split([":"])[1]) + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + } + } + } + } + + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + if (project.ext.has("host.varName")) { + def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt"; + println "发现基线mapping文件: " + baselineMappingFile + } + proguardTask.doLast { + println "混淆完成 " + varName.capitalize() + def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8" + if (!new File(host_obfuscated_dex).exists()) { + project.logger.error "宿主基线混淆包不存在 " + host_obfuscated_dex + throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件") + } + println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex + + outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + diffDexes(file.absolutePath, host_obfuscated_dex) + } + } + } + } + } + } +} + +def String curl(String url, String md5, String dir) { + File file = new File(dir, url.substring(url.lastIndexOf("/") + 1)) + if (file.exists() && file.length() > 0) { + return file.absolutePath + } + println "downloading " + url + new File(dir).mkdirs(); + MessageDigest messageDigest = MessageDigest.getInstance("MD5") + messageDigest.reset() + try { + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + OutputStream ouput =new FileOutputStream(file) + byte[] buffer = new byte[8*1024] + int size + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + messageDigest.update(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } catch(Exception){} + byte[] bs = messageDigest.digest() + StringBuilder stringBuilder = new StringBuilder() + for(byte b : bs) { + String hex = Integer.toHexString(b & 0xFF) + if (hex.length() < 2) { + stringBuilder.append("0") + } + stringBuilder.append(hex) + } + if (!md5.equalsIgnoreCase(stringBuilder.toString())) { + println "download fail: " + url + " " + stringBuilder.toString() + file.delete() + } else { + println "download success: " + url + " " + stringBuilder.toString() + } + return file.absolutePath +} +def ArrayList basksmali(String exe, String src, String dest) throws Exception +{ + ArrayList result = new ArrayList<>() + new File(dest).mkdirs() + File srcDir = new File(src); + for (File dex : srcDir.listFiles()) { + var cmd = ("java -jar " + exe + " " + + "d " + + "-o " + dest + "/" + dex.name + " " + + dex.absolutePath) + consumeStream(cmd.execute().getInputStream()) + fileTree(dest + "/" + dex.name).forEach { it-> + var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "") + result.add(relativeFile) + } + } + return result; +} +def void smali(String exe, String src, String dest) throws Exception +{ + for (File f : file(src).listFiles()) { + file(dest + "/" + f.name).delete() + var cmd = ("java -jar " + exe + " " + + "a " + + "-o " + dest + "/" + f.name + " " + + f.absolutePath) + println cmd + consumeStream(cmd.execute().getInputStream()) + } +} +def consumeStream(InputStream is) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + final byte[] buffer = new byte[256] + int len + try { + while ((len = is.read(buffer)) > 0) { + outputStream.write(buffer, 0, len) + } + } catch(IOException){} + finally { + String msg = outputStream.toString() + if (msg != null && msg.length() > 0) { + println "***exe msg begin***" + println msg + println "***exe msg end***" + } + try { + outputStream.close() + is.close() + } catch(Exception){} + } +} +def deleteDuplicated(String plugin, ArrayList hostClasses) { + for(File dexDir : file(plugin).listFiles()) { + fileTree(dexDir.absolutePath).forEach { + var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "") + if (hostClasses.contains(relativeFile)) { + it.delete() + } + } + } +} +def diffDexes(String plugin, String host) { + println "通过基线混淆包和插件混淆包做diff清理重复的class" + println "插件:" + plugin + println "宿主:" + host + var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin") + ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host") + deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses) + smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin) +} + +def applyThemePatch(varName, varDir, hostPatchExtractDir) { + println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir + + if (!file(hostPatchExtractDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir); + } + + def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName") + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp7_2_0/host.gradle b/FairyPlugin/agp7_2_0/host.gradle new file mode 100644 index 00000000..22aa55bf --- /dev/null +++ b/FairyPlugin/agp7_2_0/host.gradle @@ -0,0 +1,1064 @@ +import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.CRC32 +import java.util.zip.CheckedOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + //可选配置,用于指定插件进程名。 + //不设置即使用默认的独立进程(:plugin) + //设置为空串或者null即是和宿主同进程 + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + private String compatibleWithHostVersion = null + + //根据宿主的依赖树,生成需要在插件中排除的包配置(插件编译时依赖此配) + //避免插件和宿主同时包含同一个依赖 + private List modulesShouldBeKept = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } + + public boolean shouldBeKept(String moduleIdentifier) { + initDefaultList(); + for(String module : modulesShouldBeKept) { + if (moduleIdentifier.startsWith(module)) { + return true; + } + } + return false; + } + + public void setModulesShouldBeKept(List modules) { + initDefaultList(); + this.modulesShouldBeKept.addAll(modules) + } + + private void initDefaultList() { + if (modulesShouldBeKept == null) { + modulesShouldBeKept = new ArrayList<>() + //允许插件和宿主同时依赖kotlin基础库代码 否则插件kotlin编译会失败 + //插件和宿主同时依赖kotlin基础库在运行时不会造成问题,因为kotlin基础库不包含资源 + modulesShouldBeKept.add("org.jetbrains.kotlin:kotlin-stdlib"); + modulesShouldBeKept.add("org.jetbrains:annotations"); + modulesShouldBeKept.add("androidx.annotation:annotation") + modulesShouldBeKept.add("com.jakewharton:butterknife-annotations"); + } + } +} +extensions.create('fairy', FaiyExt) + +if ("false".equals(rootProject.properties.get("android.enableAapt2"))) { + throw new Exception("请开启aapt2编译开关:android.enableAapt2") +} + +//generateSourcess时借此文件生成attrs.xml +android.androidResources.additionalParameters( + '--emit-ids', project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties", + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_ids.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def rtConfig = variant.runtimeConfiguration + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getPreBuildProvider().configure { + it.doLast { + if (rtConfig.isCanBeResolved() && rtConfig.getResolutionAlternatives() == null) { + TreeSet hashSet = new TreeSet() + rtConfig.getIncoming().getResolutionResult().getRoot().getDependencies().each() { dependencyResult-> + if (dependencyResult instanceof ResolvedDependencyResult) { + add(hashSet, dependencyResult, false) + } + } + File dependenciesScript = new File("${project.buildDir}/outputs/host_dependencies.gradle") + dependenciesScript.getParentFile().mkdirs() + dependenciesScript.write("// This file is automatically generated by Host Script\n") + dependenciesScript.append("// variant is ${varName}\n") + dependenciesScript.append("configurations {\n") + hashSet.each { moduleFullName-> + dependenciesScript.append(moduleFullName + "\n") + } + dependenciesScript.append("}\n") + } + } + } + + variant.getMergeResourcesProvider().configure { + it.doLast { + project.logger.error '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(project.buildDir.absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir); + } + } + + variant.getMergeAssetsProvider().configure { + it.setOnlyIf { true } + it.outputs.upToDateWhen { false } + it.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + project.logger.info "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:", innerAPK + } + } + + copy { + project.logger.error '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull + from files(innerPluginApkList) + into(outputDir) + } + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${project.buildDir}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doFirst { + File public_ids = file(project.buildDir.absolutePath + "/outputs/public_ids.properties") + if (!public_ids.exists()) { + def packageName = android.defaultConfig.applicationId + public_ids.write( + packageName + ":attr/public_static_final_host_attr_ = 0x7f010000\n" + + packageName + ":drawable/public_static_final_host_drawable_ = 0x7f320000\n" + + packageName + ":layout/public_static_final_host_layout_ = 0x7f330000\n" + + packageName + ":anim/public_static_final_host_anim_ = 0x7f340000\n" + + packageName + ":xml/public_static_final_host_xml_ = 0x7f350000\n" + + packageName + ":raw/public_static_final_host_raw_ = 0x7f360000\n" + + packageName + ":dimen/public_static_final_host_dimen_ = 0x7f370000\n" + + packageName + ":string/public_static_final_host_string_ = 0x7f380000\n" + + packageName + ":style/public_static_final_host_style_ = 0x7f390000\n" + + packageName + ":color/public_static_final_host_color_ = 0x7f3a0000\n" + + packageName + ":id/public_static_final_host_id_ = 0x7f3b0000\n" + + packageName + ":bool/public_static_final_host_bool_ = 0x7f3c0000\n" + + packageName + ":integer/public_static_final_host_int_ = 0x7f3d0000\n" + + packageName + ":array/public_static_final_host_array_ = 0x7f3e0000\n" + + packageName + ":menu/public_static_final_host_menu_ = 0x7f3f0000\n" + + packageName + ":mipmap/public_static_final_host_mipmap_ = 0x7f400000\n" + + packageName + ":animator/public_static_final_host_animator_ = 0x7f410000\n" + + packageName + ":fraction/public_static_final_host_fraction_ = 0x7f420000\n" + + packageName + ":font/public_static_final_host_font_ = 0x7f430000\n" + + packageName + ":plurals/public_static_final_host_plurals_ = 0x7f440000\n" + + packageName + ":interpolator/public_static_final_host_interpolator_ = 0x7f450000\n" + + packageName + ":transition/public_static_final_host_transition_ = 0x7f460000\n") + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + copy { + from buildDir.absolutePath + "/outputs/generated_exported_all_resouces.properties" + into buildDir.absolutePath + "/outputs/" + rename { "public_attrs.properties" } + filter { String line -> + (line.contains(":attr/") || line.contains("public_static_final_host")) ? line : null + } + } + + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + project.logger.error name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + println "from :" + resPackageOutputFolder + from resPackageOutputFolder + include("*.ap_") + into("${project.buildDir}/outputs/") + duplicatesStrategy = DuplicatesStrategy.WARN + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + project.logger.error "rename: " + s + return "resources.ap_" + } + }) + } + } + + tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/compile_app_classes_jar/' + varName) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar") + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + variant.javaCompileProvider.configure { + it.doLast { + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + it.inputs.files.each { inputsFile-> + //println "inputs: " + inputsFile + } + + File jarFile = new File(project.buildDir, "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + if (jarPath.isDirectory()) { + println "adding folder " + jarPath + jarMerger.addFolder(jarPath); + } else if (jarPath.isFile()) { + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + } else { + println "not exist, ignore: " + jarPath + } + } + + File classes = new File(destinationDir.getParent(), "classes.jar"); + classes.delete(); + + try { + ZipUtil.zip(destinationDir.absolutePath, destinationDir.getParent(), "classes.jar", new ArrayList()) + } catch(Exception e) { + throw new Exception("fail to create jar for app classes ") + } + + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + throw new Exception("Not exists : classes file path is " + classes.absolutePath) + } + + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + } + + if (variant.buildType.minifyEnabled) { + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + proguardTask.doLast { + proguardTask.outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + copy { + from file.absolutePath + into("${project.buildDir}/outputs/minifyWithR8") + } + } + } + } + } + } + + //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 + task "makeHostBaseLine${varName.capitalize()}"(type: Zip) { + extension "bar" //Baseline Application Resource + baseName 'host' + version android.defaultConfig.versionName + classifier "${varName.capitalize()}" + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}" + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +def add(Set set, ResolvedDependencyResult dependencyResult, boolean childKept) { + if (!(dependencyResult.getSelected().getId() instanceof DefaultProjectComponentIdentifier)) {//源码依赖的不处理 + String displayName = dependencyResult.getSelected().getId().displayName; + String moduleIdentifier = dependencyResult.getSelected().getId().moduleIdentifier; + String group = dependencyResult.getSelected().getId().group; + String module = dependencyResult.getSelected().getId().module; + String version = dependencyResult.getSelected().getId().version; + if (!childKept && !fairy.shouldBeKept(moduleIdentifier)) { + boolean succ = set.add(" all*.exclude group: '" + group + "', module: '" + module + "' //, version: '" + version + "'") + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, childKept) + } + } + } + } else { + boolean succ = set.add(" all { resolutionStrategy.force '" + displayName + "' }") + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, true) + } + } + } + } + } else { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, childKept) + } + } + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/" + varName + "/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-v3*/values-v3*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-*-v8/values-*-v8.xml', + 'values-land/values-land.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName) { + this.destFile = destFile; + this.packageName = packageName; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + try { + this.jarOutputStream.putNextEntry(newEntry); + } catch(Exception duplicate){ + //类重复了,先忽略吧 + println "addJar putNextEntry fail: " + name + continue; + } + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} diff --git a/FairyPlugin/agp7_2_0/plugin.gradle b/FairyPlugin/agp7_2_0/plugin.gradle new file mode 100644 index 00000000..ffac4a9e --- /dev/null +++ b/FairyPlugin/agp7_2_0/plugin.gradle @@ -0,0 +1,725 @@ +import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.security.MessageDigest +import java.util.regex.Matcher +import java.util.regex.Pattern + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } + +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostDependencyFile = "host_dependencies.gradle" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def defaultHostDependencies = defaultExtraDir + "/" + hostDependencyFile +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + defaultHostDependencies = hostBarRootDir + "debug" + "/" + hostDependencyFile + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + defaultHostDependencies = hostBarRootDir + "release" + "/" + hostDependencyFile + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +if (file(defaultHostDependencies).exists()) { + println "apply from: " + defaultHostDependencies + apply from: defaultHostDependencies +} + +android.androidResources.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id', + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties") + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def rtConfig = variant.runtimeConfiguration + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + project.logger.error '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + //读取宿主编译信息 + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + props.keySet().forEach { propKey-> + project.ext.setProperty(propKey, props.getProperty(propKey)) + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + //应用宿主的主题包 + applyThemePatch(varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + variant.getPreBuildProvider().configure { + it.doLast { + if (rtConfig.isCanBeResolved() && rtConfig.getResolutionAlternatives() == null) { + TreeSet hashSet = new TreeSet() + //rtConfig.getIncoming().beforeResolve(new Action() {}) + rtConfig.getIncoming().getResolutionResult().getRoot().getDependencies().each() { dependencyResult-> + if (dependencyResult instanceof ResolvedDependencyResult) { + add(hashSet, dependencyResult) + } + } + File dependenciesTxt = new File("${project.buildDir}/outputs/plugin_dependencies.txt") + dependenciesTxt.getParentFile().mkdirs() + dependenciesTxt.write("// This file is automatically generated by Plugin Script\n") + dependenciesTxt.append("// variant is ${varName}\n") + hashSet.each { moduleFullName-> + dependenciesTxt.append(moduleFullName) + dependenciesTxt.append("\n") + } + dependenciesTxt.append("\n") + } + } + } + tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/compile_app_classes_jar/' + varName) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar") + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + def hostVersionName = project.ext.getProperty("host.versionName") + def hostVersionCode = project.ext.getProperty("host.versionCode") + def hostApplicationId = project.ext.getProperty("host.applicationId") + def hostBuildType = project.ext.getProperty("host.buildType") + def hostFlavorName = project.ext.getProperty("host.flavorName") + def hostVarName = project.ext.getProperty("host.varName") + def hostVarDirName = project.ext.getProperty("host.varDirName") + if (hostApplicationId != null) { + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("宿主基线包信息丢失") + } + } + + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + (packageName + ":" + line.split([":"])[1]) + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + } + } + } + } + + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + if (project.ext.has("host.varName")) { + def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt"; + println "发现基线mapping文件: " + baselineMappingFile + } + proguardTask.doLast { + println "混淆完成 " + varName.capitalize() + def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8" + if (!new File(host_obfuscated_dex).exists()) { + project.logger.error "宿主基线混淆包不存在 " + host_obfuscated_dex + throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件") + } + println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex + + outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + diffDexes(file.absolutePath, host_obfuscated_dex) + } + } + } + } + } + } +} + +def add(Set set, ResolvedDependencyResult dependencyResult) { + if (!(dependencyResult.getSelected().getId() instanceof DefaultProjectComponentIdentifier)) {//源码依赖的不处理 + boolean succ = set.add(dependencyResult.getSelected().getId().displayName) + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult) + } + } + } + } else { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult) + } + } + } +} + +def String curl(String url, String md5, String dir) { + File file = new File(dir, url.substring(url.lastIndexOf("/") + 1)) + if (file.exists() && file.length() > 0) { + return file.absolutePath + } + println "downloading " + url + new File(dir).mkdirs(); + MessageDigest messageDigest = MessageDigest.getInstance("MD5") + messageDigest.reset() + try { + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + OutputStream ouput =new FileOutputStream(file) + byte[] buffer = new byte[8*1024] + int size + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + messageDigest.update(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } catch(Exception){} + byte[] bs = messageDigest.digest() + StringBuilder stringBuilder = new StringBuilder() + for(byte b : bs) { + String hex = Integer.toHexString(b & 0xFF) + if (hex.length() < 2) { + stringBuilder.append("0") + } + stringBuilder.append(hex) + } + if (!md5.equalsIgnoreCase(stringBuilder.toString())) { + println "download fail: " + url + " " + stringBuilder.toString() + file.delete() + } else { + println "download success: " + url + " " + stringBuilder.toString() + } + return file.absolutePath +} +def ArrayList basksmali(String exe, String src, String dest) throws Exception +{ + ArrayList result = new ArrayList<>() + new File(dest).mkdirs() + File srcDir = new File(src); + for (File dex : srcDir.listFiles()) { + var cmd = ("java -jar " + exe + " " + + "d " + + "-o " + dest + "/" + dex.name + " " + + dex.absolutePath) + consumeStream(cmd.execute().getInputStream()) + fileTree(dest + "/" + dex.name).forEach { it-> + var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "") + result.add(relativeFile) + } + } + return result; +} +def void smali(String exe, String src, String dest) throws Exception +{ + for (File f : file(src).listFiles()) { + file(dest + "/" + f.name).delete() + var cmd = ("java -jar " + exe + " " + + "a " + + "-o " + dest + "/" + f.name + " " + + f.absolutePath) + println cmd + consumeStream(cmd.execute().getInputStream()) + } +} +def consumeStream(InputStream is) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + final byte[] buffer = new byte[256] + int len + try { + while ((len = is.read(buffer)) > 0) { + outputStream.write(buffer, 0, len) + } + } catch(IOException){} + finally { + String msg = outputStream.toString() + if (msg != null && msg.length() > 0) { + println "***exe msg begin***" + println msg + println "***exe msg end***" + } + try { + outputStream.close() + is.close() + } catch(Exception){} + } +} +def deleteDuplicated(String plugin, ArrayList hostClasses) { + for(File dexDir : file(plugin).listFiles()) { + fileTree(dexDir.absolutePath).forEach { + var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "") + if (hostClasses.contains(relativeFile)) { + it.delete() + } + } + } +} +def diffDexes(String plugin, String host) { + println "通过基线混淆包和插件混淆包做diff清理重复的class" + println "插件:" + plugin + println "宿主:" + host + var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin") + ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host") + deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses) + smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin) +} + +def applyThemePatch(varName, varDir, hostPatchExtractDir) { + println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir + + if (!file(hostPatchExtractDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir); + } + + def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName") + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/agp7_4_2/host.gradle b/FairyPlugin/agp7_4_2/host.gradle new file mode 100644 index 00000000..1ccf31da --- /dev/null +++ b/FairyPlugin/agp7_4_2/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_2_0/host.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp7_4_2/plugin.gradle b/FairyPlugin/agp7_4_2/plugin.gradle new file mode 100644 index 00000000..ea24a35a --- /dev/null +++ b/FairyPlugin/agp7_4_2/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_2_0/plugin.gradle" \ No newline at end of file diff --git a/FairyPlugin/agp8_13_0/host.gradle b/FairyPlugin/agp8_13_0/host.gradle new file mode 100644 index 00000000..202f3419 --- /dev/null +++ b/FairyPlugin/agp8_13_0/host.gradle @@ -0,0 +1,1060 @@ +import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier +import org.xml.sax.Attributes +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.regex.Pattern +import java.util.zip.CRC32 +import java.util.zip.CheckedOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +configurations { + innerPlugin +} + +def innerPluginApkList = [] + +class FaiyExt { + //可选配置,用于指定插件进程名。 + //不设置即使用默认的独立进程(:plugin) + //设置为空串或者null即是和宿主同进程 + private String pluginProcess = ":plugin" + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + private String compatibleWithHostVersion = null + + //根据宿主的依赖树,生成需要在插件中排除的包配置(插件编译时依赖此配) + //避免插件和宿主同时包含同一个依赖 + private List modulesShouldBeKept = null + + public String getPluginProcess() { + return pluginProcess + } + + public void setPluginProcess(String process) { + this.pluginProcess = process + } + + public String getCompatibleWithHostVersion() { + return compatibleWithHostVersion + } + + public void setCompatibleWithHostVersion(String compatibleVersion) { + this.compatibleWithHostVersion = compatibleVersion + } + + public boolean shouldBeKept(String moduleIdentifier) { + initDefaultList(); + for(String module : modulesShouldBeKept) { + if (moduleIdentifier.startsWith(module)) { + return true; + } + } + return false; + } + + public void setModulesShouldBeKept(List modules) { + initDefaultList(); + this.modulesShouldBeKept.addAll(modules) + } + + private void initDefaultList() { + if (modulesShouldBeKept == null) { + modulesShouldBeKept = new ArrayList<>() + //允许插件和宿主同时依赖kotlin基础库代码 否则插件kotlin编译会失败 + //插件和宿主同时依赖kotlin基础库在运行时不会造成问题,因为kotlin基础库不包含资源 + modulesShouldBeKept.add("org.jetbrains.kotlin:kotlin-stdlib"); + modulesShouldBeKept.add("org.jetbrains:annotations"); + modulesShouldBeKept.add("androidx.annotation:annotation") + } + } +} +extensions.create('fairy', FaiyExt) + +//generateSourcess时借此文件生成attrs.xml +android.androidResources.additionalParameters( + '--emit-ids', getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/outputs/generated_exported_all_resouces.properties", + '--stable-ids', getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/outputs/public_ids.properties") + +afterEvaluate { + + if (android.defaultConfig.applicationId == null) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //收集需要内置的插件文件地址 + configurations.innerPlugin.files.each { file -> + innerPluginApkList << file.absolutePath + } + + for (variant in android.applicationVariants) { + + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def rtConfig = variant.runtimeConfiguration + + println 'Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + variant.getPreBuildProvider().configure { + it.doLast { + if (rtConfig.isCanBeResolved() && rtConfig.getResolutionAlternatives() == null) { + TreeSet hashSet = new TreeSet() + rtConfig.getIncoming().getResolutionResult().getRoot().getDependencies().each() { dependencyResult-> + if (dependencyResult instanceof ResolvedDependencyResult) { + add(hashSet, dependencyResult, false) + } + } + File dependenciesScript = new File("${project.buildDir}/outputs/host_dependencies.gradle") + dependenciesScript.getParentFile().mkdirs() + dependenciesScript.write("// This file is automatically generated by Host Script\n") + dependenciesScript.append("// variant is ${varName}\n") + dependenciesScript.append("configurations {\n") + hashSet.each { moduleFullName-> + dependenciesScript.append(moduleFullName + "\n") + } + dependenciesScript.append("}\n") + } + } + } + + variant.getMergeResourcesProvider().configure { + it.doLast { + println '生成一份主题patch包,编译非独立插件时需要此包' + + File patchDir = new File(getLayout().getBuildDirectory().getAsFile().get().absolutePath, "outputs/theme_patch/" + varDirName); + patchDir.mkdirs(); + + //导出一份主题包备用,编译非独立插件时需要 + createThemePatch(varName, buildTypeName, patchDir); + } + } + + variant.getMergeAssetsProvider().configure { + it.setOnlyIf { true } + it.outputs.upToDateWhen { false } + it.doLast { + + //检查内置插件坐标是否存在,不存在给出提示 + innerPluginApkList.each { innerAPK -> + if (!file(innerAPK).exists()) { + println "引用的插件apk文件不存在,可能插件apk还未编译完成,或者宿主innerPlugin配置的路径错误:" + innerAPK + } + } + + copy { + println '复制宿主依赖的内置插件到merge后的assets目录\n' + innerPluginApkList + "\n" + outputDir.orNull + from files(innerPluginApkList) + into(outputDir) + } + } + } + + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + println '正在检查Manifest中的插件配置是否正确' + manifestFile.absolutePath + + def originManifestContent = manifestFile.getText('UTF-8') + if (originManifestContent.contains("{applicationId}")) { + throw new Exception("宿主build.gradle未配置android.defaultConfig.applicationId") + } + + //生成prop文件 + def pattern = Pattern.compile("versionName\\s*=\\s*\"(.+?)\""); + def matcher = pattern.matcher(originManifestContent); + if (matcher.find()) { + def versionName = matcher.group(1) + //File hostInfo = new File("${project.buildDir}/outputs/HostInfo-" + tastName.replace("process","").replace("Resources", "") + ".prop") + println 'HostInfo.prop没有单独命名,有多个favor时文件会生成多个并覆盖,如果不同favor间版本号不同会导致基线包信息生成错误!!' + File hostInfo = new File("${getLayout().getBuildDirectory().getAsFile().get()}/outputs/HostInfo.prop") + if (hostInfo.exists()) { + hostInfo.delete() + } + //没有单独命名,有多个abi时文件会覆盖 + println '正在生成文件' + hostInfo.absolutePath + hostInfo.write("#Host Manifest CREATED AT " + new Date().format("yyyy-MM-dd HH:mm::ss")) + hostInfo.append("\nhost.versionCode=" + android.defaultConfig.versionCode) + //versionName可能有后缀,所以以Manifest中为准 + hostInfo.append("\nhost.versionName=" + versionName) + hostInfo.append("\nhost.buildType=" + buildTypeName) + hostInfo.append("\nhost.flavorName=" + flavorName) + hostInfo.append("\nhost.varName=" + varName) + hostInfo.append("\nhost.varDirName=" + varDirName) + hostInfo.append("\nhost.minifyEnabled=" + variant.buildType.minifyEnabled) + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildTypeName].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildTypeName].applicationIdSuffix; + } + + hostInfo.append("\nhost.applicationId=" + packageName) + } + + //指定插件进程名,设置为空串或者null即是和宿主同进程 + //不设置即使用默认进程(:plugin) + def pluginProcessName = fairy.pluginProcess + if (!":plugin".equals(pluginProcessName)) { + def customPluginProcessName = ""; + if (pluginProcessName != null) { + customPluginProcessName = "android:process=\"" + pluginProcessName + "\"" + } + println '正在设置插件进程配置:' + customPluginProcessName + def modifyedManifestContent = originManifestContent.replaceAll("android:process=\":plugin\"", customPluginProcessName) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + + //指定当前宿主版本与哪些历史宿主版本兼容, 用于指定插件版本跨宿主版本。默认是自己与自己兼容,也即插件不能跨宿主版本 + //例如: + // 宿主版本v1,插件版本v1.1, v1.2 + // 宿主版本v2,插件版本v2.1, v2.1 + // 默认插件不能跨宿主版本,也就是说插件版本v1.1, v1.2只能在宿主版本v1上运行,而插件版本v2.1, v2.2只能在宿主版本v2上运行 + //若在发布宿主版本v2时,同时指定这个版本与宿主v1版本兼容,则插件版本v1.1, v1.2也可以在宿主版本v2上运行 + //此功能请谨慎使用,否则可能引起插件崩溃 + def compatibleWithHostVersion = fairy.compatibleWithHostVersion + if(compatibleWithHostVersion != null) { + originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replaceAll("fairy_compatibleWithHostVersion_NOT_SET", compatibleWithHostVersion.trim()) + manifestFile.write(modifyedManifestContent, 'UTF-8') + } + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doFirst { + File public_ids = file(getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/outputs/public_ids.properties") + if (!public_ids.exists()) { + def packageName = android.defaultConfig.applicationId + public_ids.write( + packageName + ":attr/public_static_final_host_attr_ = 0x7f010000\n" + + packageName + ":drawable/public_static_final_host_drawable_ = 0x7f320000\n" + + packageName + ":layout/public_static_final_host_layout_ = 0x7f330000\n" + + packageName + ":anim/public_static_final_host_anim_ = 0x7f340000\n" + + packageName + ":xml/public_static_final_host_xml_ = 0x7f350000\n" + + packageName + ":raw/public_static_final_host_raw_ = 0x7f360000\n" + + packageName + ":dimen/public_static_final_host_dimen_ = 0x7f370000\n" + + packageName + ":string/public_static_final_host_string_ = 0x7f380000\n" + + packageName + ":style/public_static_final_host_style_ = 0x7f390000\n" + + packageName + ":color/public_static_final_host_color_ = 0x7f3a0000\n" + + packageName + ":id/public_static_final_host_id_ = 0x7f3b0000\n" + + packageName + ":bool/public_static_final_host_bool_ = 0x7f3c0000\n" + + packageName + ":integer/public_static_final_host_int_ = 0x7f3d0000\n" + + packageName + ":array/public_static_final_host_array_ = 0x7f3e0000\n" + + packageName + ":menu/public_static_final_host_menu_ = 0x7f3f0000\n" + + packageName + ":mipmap/public_static_final_host_mipmap_ = 0x7f400000\n" + + packageName + ":animator/public_static_final_host_animator_ = 0x7f410000\n" + + packageName + ":fraction/public_static_final_host_fraction_ = 0x7f420000\n" + + packageName + ":font/public_static_final_host_font_ = 0x7f430000\n" + + packageName + ":plurals/public_static_final_host_plurals_ = 0x7f440000\n" + + packageName + ":interpolator/public_static_final_host_interpolator_ = 0x7f450000\n" + + packageName + ":transition/public_static_final_host_transition_ = 0x7f460000\n") + } + } + + tasks.findByName("process${varName.capitalize()}Resources").doLast { + copy { + from getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/intermediates/stable_resource_ids_file/${varName}/process${varName.capitalize()}Resources/stableIds.txt" + into getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/outputs/" + rename { "public_attrs.properties" } + filter { String line -> + (line.contains(":attr/") || line.contains("public_static_final_host")) ? line : null + } + } + + //实际上最新版sdk和gradle可以直接指定apk了, 可以不使用.ap_文件 + //def processResFullName = baseVariant.apkData.fullName + copy { + println name + ' 编译宿主资源编译完成后导出后缀为.ap_的资源包,此资源包在编译非独立插件时需要此包' + from linkedResourcesOutputDir + include("*.ap_") + into("${getLayout().getBuildDirectory().getAsFile().get()}/outputs/") + // 可能有多种favor的ap_, 所以这里设置了重复策略 + duplicatesStrategy = DuplicatesStrategy.WARN + rename(new Transformer() { + @Override + String transform(String s) { + //多abi时会相互覆盖,不过对我们而言应该没什么影响 + println "rename: " + s + return "resources.ap_" + } + }) + } + } + + tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar").doLast { + copy { + from(getLayout().getBuildDirectory().getAsFile().get().absolutePath + '/intermediates/compile_app_classes_jar/' + varName + "/" + name) { + include "classes.jar" + } + into(getLayout().getBuildDirectory().getAsFile().get().absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar") + + //将宿主的所有class,包括宿主的、和它所有依赖的类,合并起来打出一个jar来,供将来的非独立插件使用 + variant.javaCompileProvider.configure { + it.doLast { + println "Merge Jar After Task " + name + " buildType is " + buildTypeName + ", flavorName is " + flavorName + ", varName is " + varName + it.inputs.files.each { inputsFile-> + //println "inputs: " + inputsFile + } + + File jarFile = new File(getLayout().getBuildDirectory().getAsFile().get(), "outputs/host_classes.jar") + if (jarFile.exists()) { + jarFile.delete() + } + + JarMerger jarMerger = new JarMerger(jarFile) + try { + jarMerger.setFilter(new JarFilter() { + public boolean checkEntry(String archivePath) throws JarFilter.ZipAbortException { + if (archivePath.endsWith(".class")) { + return true + } + return false + } + }); + + classpath.each { jarPath -> + if (jarPath.isDirectory()) { + println "adding folder " + jarPath + jarMerger.addFolder(jarPath); + } else if (jarPath.isFile()) { + println "adding jar " + jarPath + jarMerger.addJar(jarPath); + } else { + println "not exist, ignore: " + jarPath + } + } + + File classes = new File(destinationDirectory.getAsFile().get().getParent(), "classes.jar"); + classes.delete(); + + try { + ZipUtil.zip(destinationDirectory.getAsFile().get().absolutePath, destinationDirectory.getAsFile().get().getParent(), "classes.jar", new ArrayList()) + } catch(Exception e) { + throw new Exception("fail to create jar for app classes ") + } + + if (classes.exists()) { + println "adding jar " + classes + jarMerger.addJar(classes) + } else { + throw new Exception("Not exists : classes file path is " + classes.absolutePath) + } + + } finally { + jarMerger.close() + } + + println "Merge Jar Finished, Jar is at " + jarFile.absolutePath + } + } + + if (variant.buildType.minifyEnabled) { + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + proguardTask.doLast { + proguardTask.outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + copy { + from file.absolutePath + into("${project.buildDir}/outputs/minifyWithR8") + } + } + } + } + } + } + + //将宿主编译产物作为基线包存档,这样可以使插件脱离宿主工程独立编译 + task "makeHostBaseLine${varName.capitalize()}"(type: Zip, dependsOn:"assemble${varName.capitalize()}") { + archiveExtension = "bar" //Baseline Application Resource + archiveBaseName = 'host' + archiveVersion = android.defaultConfig.versionName + archiveClassifier = "${varName.capitalize()}" + from zipTree("${project.buildDir}/outputs/resources.ap_") + from("${project.buildDir}/outputs") { + exclude '*.ap_' + } + destinationDirectory = file("${project.buildDir}/distributions/") + } + + //导出宿主最终的基线包 + tasks.findByName("assemble${varName.capitalize()}").finalizedBy "makeHostBaseLine${varName.capitalize()}" + } + + if (gradle.startParameter.taskNames.find { + println ">>>>>>执行命令: " + it + it.startsWith("assemble") || it.startsWith("build") + } != null) { + //nothing + } +} + +def add(Set set, ResolvedDependencyResult dependencyResult, boolean childKept) { + if (!(dependencyResult.getSelected().getId() instanceof DefaultProjectComponentIdentifier)) {//源码依赖的不处理 + String displayName = dependencyResult.getSelected().getId().displayName; + String moduleIdentifier = dependencyResult.getSelected().getId().moduleIdentifier; + String group = dependencyResult.getSelected().getId().group; + String module = dependencyResult.getSelected().getId().module; + String version = dependencyResult.getSelected().getId().version; + if (!childKept && !fairy.shouldBeKept(moduleIdentifier)) { + boolean succ = set.add(" all*.exclude group: '" + group + "', module: '" + module + "' //, version: '" + version + "'") + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, childKept) + } + } + } + } else { + boolean succ = set.add(" all { resolutionStrategy.force '" + displayName + "' }") + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, true) + } + } + } + } + } else { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult, childKept) + } + } + } +} + +//导出主题patch +def createThemePatch(String varName, String buildType, File patchDir) { + + def packageName = android.defaultConfig.applicationId + if (android.buildTypes[buildType].applicationIdSuffix != null) { + packageName = packageName + android.buildTypes[buildType].applicationIdSuffix; + } + + String mergedResDir = "${project.buildDir}/intermediates/incremental/" + varName + "/merge" + varName.capitalize() + "Resources/merged.dir/" + FileTree allxmlFiles = fileTree(dir: mergedResDir) + allxmlFiles.include 'values/values*.xml', + 'values-v1*/values-v1*.xml', + 'values-v2*/values-v2*.xml', + 'values-v3*/values-v3*.xml', + 'values-*-v1*/values-*-v1*.xml', + 'values-*-v2*/values-*-v2*.xml', + 'values-*-v4/values-*-v4.xml', + 'values-*-v8/values-*-v8.xml', + 'values-land/values-land.xml' + + allxmlFiles.each { File itemFile -> + def valuesDir = itemFile.getAbsolutePath().replace(itemFile.getParentFile().getParentFile().absolutePath, "").replace(itemFile.name, "") + def destFile = new File(patchDir, 'res' + valuesDir + "patch_" + itemFile.name) + destFile.getParentFile().mkdirs() + println "export from " + itemFile + " to " + destFile + ThemeProcessor.exportThemeStyle(itemFile, destFile, packageName) + } +} + +public class ThemeProcessor extends DefaultHandler { + + public static void exportThemeStyle(File srcFile, File destFile, String packageName) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + saxParser.parse(new FileInputStream(srcFile), new ThemeProcessor(destFile, packageName)); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + //////////////// + //////////////// + //////////////// + + File destFile; + String packageName; + Stack stack = new Stack(); + BufferedWriter outXmlStream = null; + BufferedWriter outPublicAttrStream = null; + + HashSet attrSets = new HashSet<>(); + + HashSet dupcate = new HashSet<>(); + + public ThemeProcessor(File destFile, String packageName) { + this.destFile = destFile; + this.packageName = packageName; + } + + public void startDocument() throws SAXException { + try { + outXmlStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8")); + outXmlStream.write(""); + outXmlStream.write("\n"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + + if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") + || qName.equals("integer") || qName.equals("string") + || qName.equals("ns2:g") || qName.equals("ns1:g") + || qName.equals("u")) { + stack.add(new Node(attributes.getValue("name"), false, true)); + return; + } + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + try { + if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || (attrSets.add(attributes.getValue("name"))?false:(dupcate.add(attributes.getValue("name"))?true:true))) + || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { + //skip + skip = true; + } else { + if (qName.equals("enum")) { + if (!stack.empty()) { + Node top = stack.peek(); + if (!dupcate.contains(top.name)) { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } else { + outXmlStream.write(tag); + if (qName.equals("attr") && outPublicAttrStream != null) { + outPublicAttrStream.write(tag.replace("") + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + if (!stack.empty()) { + Node parent = stack.peek(); + if (qName.equals("enum") && dupcate.contains(parent.name)) { + //nothing + } else { + outXmlStream.write(space + ""); + } + } else { + outXmlStream.write(space + ""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + String text = new String(ch, start, length); + text = text.replaceAll("[\n ]", ""); + if (text.length() > 0) { + try { + if (text.startsWith("@color")) { + text = text.replace("@color", "@*" + packageName +":color"); + + } else if (text.startsWith("@dimen")) { + text = text.replace("@dimen", "@*" + packageName +":dimen"); + + } else if (text.startsWith("@string")) { + text = text.replace("@string", "@*" + packageName +":string"); + + } else if (text.startsWith("@bool")) { + text = text.replace("@bool", "@*" + packageName +":bool"); + + } else if (text.startsWith("@integer")) { + text = text.replace("@integer", "@*" + packageName +":integer"); + + } else if (text.startsWith("@layout")) { + text = text.replace("@layout", "@*" + packageName +":layout"); + + } else if (text.startsWith("@anim")) { + text = text.replace("@anim", "@*" + packageName +":anim"); + + } else if (text.startsWith("@id")) { + text = text.replace("@id", "@*" + packageName +":id"); + + } else if (text.startsWith("@drawable")) { + text = text.replace("@drawable", "@*" + packageName +":drawable"); + + //} else if (text.startsWith("?attr")) { + // text = text.replace("?attr", "?*" + packageName +":attr"); + } else if (text.startsWith("@mipmap")) { + text = text.replace("@mipmap", "@*" + packageName +":mipmap"); + } else if (text.startsWith("@style")) { + if (node.name.equals("android:windowAnimationStyle")) { + text = text.replace("@style", "@*" + packageName +":style"); + } + } + + outXmlStream.write(text); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void endDocument() throws SAXException { + try { + outXmlStream.flush(); + outXmlStream.close(); + + if (outPublicAttrStream != null) { + outPublicAttrStream.write("\n"); + outPublicAttrStream.flush(); + outPublicAttrStream.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + +} + +public class JarMerger { + private final byte[] buffer = new byte[8192]; + private final File jarFile; + private FileOutputStream fos; + private JarOutputStream jarOutputStream; + + private JarFilter filter; + + public JarMerger(File jarFile) throws IOException { + this.jarFile = jarFile; + } + + private void init() throws IOException { + if(this.fos == null && this.jarOutputStream == null) { + if(!this.jarFile.getParentFile().mkdirs() && !this.jarFile.getParentFile().exists()) { + throw new RuntimeException("Cannot create directory " + this.jarFile.getParentFile()); + } + this.fos = new FileOutputStream(this.jarFile); + this.jarOutputStream = new JarOutputStream(fos); + } + } + + public void setFilter(JarFilter filter) { + this.filter = filter; + } + + public void addFolder(File folder) throws IOException { + this.init(); + + try { + this.addFolderInternal(folder, ""); + } catch (JarFilter.ZipAbortException var3) { + throw new IOException(var3); + } + } + + private void addFolderInternal(File folder, String path) throws IOException, JarFilter.ZipAbortException { + File[] files = folder.listFiles(); + if(files != null) { + File[] arr$ = files; + int len$ = files.length; + + for(int i$ = 0; i$ < len$; ++i$) { + File file = arr$[i$]; + if(!file.isFile()) { + if(file.isDirectory()) { + this.addFolderInternal(file, path + file.getName() + "/"); + } + } else { + String entryPath = path + file.getName(); + if(this.filter == null || this.filter.checkEntry(entryPath)) { + this.jarOutputStream.putNextEntry(new JarEntry(entryPath)); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + + int count; + while((count = fis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + } finally { + if (fis != null) { + fis.close(); + fis = null; + } + } + + this.jarOutputStream.closeEntry(); + } + } + } + } + + } + + public void addJar(File file) throws IOException { + this.addJar(file, false); + } + + public void addJar(File file, boolean removeEntryTimestamp) throws IOException { + this.init(); + + FileInputStream e = null; + ZipInputStream zis = null; + try { + e = new FileInputStream(file); + zis = new ZipInputStream(e); + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + if(!entry.isDirectory()) { + String name = entry.getName(); + if(this.filter == null || this.filter.checkEntry(name)) { + JarEntry newEntry; + if(entry.getMethod() == ZipEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + newEntry = new JarEntry(name); + } + + if(removeEntryTimestamp) { + newEntry.setTime(0L); + } + try { + this.jarOutputStream.putNextEntry(newEntry); + } catch(Exception duplicate){ + //类重复了,先忽略吧 + println "addJar putNextEntry fail(duplicate): " + name + continue; + } + int count; + while((count = zis.read(this.buffer)) != -1) { + this.jarOutputStream.write(this.buffer, 0, count); + } + + this.jarOutputStream.closeEntry(); + zis.closeEntry(); + } + } + } + } catch (JarFilter.ZipAbortException var13) { + throw new IOException(var13); + } finally { + if (zis != null) { + zis.close(); + } + if (e != null) { + e.close(); + } + } + + } + + public void addEntry(String path, byte[] bytes) throws IOException { + this.init(); + this.jarOutputStream.putNextEntry(new JarEntry(path)); + this.jarOutputStream.write(bytes); + this.jarOutputStream.closeEntry(); + } + + public void close() throws IOException { + if (this.jarOutputStream != null) { + jarOutputStream.close(); + jarOutputStream = null; + } + if (this.fos != null) { + fos.close(); + fos = null; + } + + } +} + +public interface JarFilter { + boolean checkEntry(String var1) throws ZipAbortException; + + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } +} + +public class ZipUtil { + + public static List unZip(String path, String savepath) throws IOException + { + def resultList = new ArrayList(); + def count = -1; + def index = -1; + def flag = false; + def file = null; + def is = null; + def fos = null; + def bos = null; + + ZipFile zipFile = new ZipFile(path); + Enumeration entries = zipFile.entries(); + + while(entries.hasMoreElements()) + { + def buf = new byte[2048]; + ZipEntry entry = (ZipEntry)entries.nextElement(); + + def filename = savepath + entry.getName(); + + File folder = new File(filename).getParentFile(); + if(!folder.exists()){ + folder.mkdirs(); + } + + if(!filename.endsWith(File.separator)){ + + System.out.println("解压:" + filename); + + file = new File(filename); + file.createNewFile(); + is = zipFile.getInputStream(entry); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos, 2048); + + while((count = is.read(buf)) > -1) + { + bos.write(buf, 0, count ); + } + + bos.flush(); + + fos.close(); + is.close(); + + resultList.add(entry.getName()); + } + } + + zipFile.close(); + + return resultList + } + + /** + * 递归压缩文件夹 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zos 压缩文件存储对象 + * @throws Exception + */ + public static zip(String srcRootDir, File file, ZipOutputStream zos, ArrayList filter) throws Exception + { + if (file == null) + { + return; + } + + //如果是文件,则直接压缩该文件 + if (file.isFile()) + { + int count, bufferLen = 1024; + byte[] data = new byte[bufferLen]; + + //获取文件相对于压缩文件夹根目录的子路径 + String subPath = file.getAbsolutePath(); + int index = subPath.indexOf(srcRootDir); + if (index != -1) + { + subPath = subPath.substring(srcRootDir.length() + File.separator.length()); + } + + if (filter.contains(subPath)) { + System.out.println("过滤:" + subPath) + return; + } else { + System.out.println("压缩:" + subPath) + } + + ZipEntry entry = new ZipEntry(subPath); + zos.putNextEntry(entry); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(data, 0, bufferLen)) != -1) + { + zos.write(data, 0, count); + } + bis.close(); + zos.closeEntry(); + } + //如果是目录,则压缩整个目录 + else + { + //压缩目录中的文件或子目录 + File[] childFileList = file.listFiles(); + for (int n=0; n filter) throws Exception + { + CheckedOutputStream cos = null; + ZipOutputStream zos = null; + try + { + File srcFile = new File(srcPath); + + //判断压缩文件保存的路径是否存在,如果不存在,则创建目录 + File zipDir = new File(zipPath); + if (!zipDir.exists() || !zipDir.isDirectory()) + { + zipDir.mkdirs(); + } + + //创建压缩文件保存的文件对象 + String zipFilePath = zipPath + File.separator + zipFileName; + File zipFile = new File(zipFilePath); + if (zipFile.exists()) + { + ///删除已存在的目标文件 + boolean success = zipFile.delete(); + System.out.println("删除已存在的目标文件:" + success) + + } + + cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zos = new ZipOutputStream(cos); + + //如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir = srcPath; + if (srcFile.isFile()) + { + int index = srcPath.lastIndexOf(File.separator); + if (index != -1) + { + srcRootDir = srcPath.substring(0, index); + } + } + //调用递归压缩方法进行目录或文件压缩 + zip(srcRootDir, srcFile, zos, filter); + zos.flush(); + } + catch (Exception e) + { + throw e; + } + finally + { + try + { + if (zos != null) + { + zos.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } +} diff --git a/FairyPlugin/agp8_13_0/plugin.gradle b/FairyPlugin/agp8_13_0/plugin.gradle new file mode 100644 index 00000000..5f2e2004 --- /dev/null +++ b/FairyPlugin/agp8_13_0/plugin.gradle @@ -0,0 +1,725 @@ +import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.helpers.DefaultHandler + +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.sax.SAXTransformerFactory +import javax.xml.transform.sax.TransformerHandler +import javax.xml.transform.stream.StreamResult +import java.security.MessageDigest +import java.util.regex.Matcher +import java.util.regex.Pattern + +configurations { + baselinePatch + debugBaselinePatch + releaseBaselinePatch +} + +class FaiyExt { + private boolean requireHostVersion = true; + + public boolean getRequireHostVersion() { + return requireHostVersion + } + + public void setRequireHostVersion(boolean required) { + this.requireHostVersion = required + } + +} +extensions.create('fairy', FaiyExt) + +android{ +} + +def hostBar = "host.bar" +def hostJar = "host_classes.jar" +def hostDependencyFile = "host_dependencies.gradle" +def hostBarRootDir = rootProject.rootDir.absolutePath + '/.gradle/fairy/baselinePatch/' +def defaultExtraDir = hostBarRootDir + "unspecified" +def defaultHostDependencies = defaultExtraDir + "/" + hostDependencyFile +def currentSelectedBar = defaultExtraDir + "/" + hostBar +gradle.startParameter.taskNames.each { startTaskName -> + if (startTaskName.contains("Debug")) { + defaultHostDependencies = hostBarRootDir + "debug" + "/" + hostDependencyFile + currentSelectedBar = hostBarRootDir + "debug" + "/" + hostBar + } else if (startTaskName.contains("Release")) { + defaultHostDependencies = hostBarRootDir + "release" + "/" + hostDependencyFile + currentSelectedBar = hostBarRootDir + "release" + "/" + hostBar + } +} + +println "执行命令决定了bar文件依赖路径!" +println ">>>" + hostBarRootDir +println ">>>" + currentSelectedBar + +if (file(defaultHostDependencies).exists()) { + println "apply from: " + defaultHostDependencies + apply from: defaultHostDependencies +} + +android.androidResources.additionalParameters( + '-I', currentSelectedBar, + '--package-id', "0x7f", //默认0x7f,可以修改为任意其他值,如0x66、0x88,但要确保不和系统已经使用的id重复。典型的如0x10、0x20,都已被系统使用 + '--allow-reserved-package-id', + '--stable-ids', project.buildDir.absolutePath + "/outputs/public_attrs.properties") + +afterEvaluate { + def isApplicationModule = getPlugins().hasPlugin("com.android.application") + if (isApplicationModule) { + if (android.defaultConfig.applicationId == null) { + throw new Exception("插件build.gradle未配置android.defaultConfig.applicationId") + } + } + def moduleVariants = isApplicationModule ? android.applicationVariants : android.libraryVariants + for (variant in moduleVariants) { + def varName = variant.name; + def buildTypeName = variant.buildType.name + def flavorName = variant.flavorName + def varDirName = variant.dirName + def rtConfig = variant.runtimeConfiguration + def hostBarExtraRootDir = hostBarRootDir + variant.buildType.name + + println '\n' + println project.name + ' Check Env : variant=' + varName + ", buildTypeName=" + buildTypeName + ", flavorName=" + flavorName + ", varDirName=" + varDirName + + //读取宿主基线包文件路径,并解压到指定目录 + def configure = configurations.maybeCreate(variant.buildType.name + 'BaselinePatch') + if (configure == null || configure.files.size() == 0) { + configure = configurations['baselinePatch'] + } + if (configure.files.size() == 0) { + println '未配置基线包依赖!' + } + configure.files.each { patch -> + println "发现宿主基线配置指向位置:" + patch.absolutePath + //从原理上讲应该每个变种都需要一个对应的基线包解压路径,这里偷懒了,只考虑了buildType,忽略favor + if ("".equals(patch.absolutePath) || !file(patch.absolutePath).exists()) { + println "宿主基线patch文件不存在:" + patch.absolutePath + println "清理对应的缓存目录:" + hostBarExtraRootDir + file(hostBarExtraRootDir).deleteDir(); + } else { + //按buildType解压到不同的文件夹里面 + println "解压宿主基线文件:" + patch.absolutePath + "\n 到 " + hostBarExtraRootDir + //这里做一个简单校验判断,提高效率 + if (file(hostBarExtraRootDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + def zipFile = file(patch.absolutePath) + def outputDir = file(hostBarExtraRootDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } else { + println "源与目标文件大小相同,省去解压步骤" + } + //这里做一个简单校验判断,提高效率 + if (file(defaultExtraDir + "/" + hostBar).size() != file(patch.absolutePath).size()) { + copy { + //留存一份到默认目录,当不是使用assembleDebug和assembleRelease编译时会使用这个文件夹 + //如果同时配置了debug和release,使用最后一个遍历到的buildType覆盖 + def zipFile = file(patch.absolutePath); + def outputDir = file(defaultExtraDir) + outputDir.deleteDir(); + from zipTree(zipFile) + from(zipFile) { + rename { + hostBar + } + } + into outputDir + } + } + //读取宿主编译信息 + File propFile = file(hostBarExtraRootDir + "/HostInfo.prop"); + if (!propFile.exists()) { + throw new Exception("HostInfo.prop丢失:" + propFile.absolutePath) + } + def Properties props = new Properties() + props.load(new FileInputStream(propFile)) + props.keySet().forEach { propKey-> + project.ext.setProperty(propKey, props.getProperty(propKey)) + } + } + } + //根据混淆开关来选择是使用compileOnly还是implementation来依赖基线包的jar + //这里偷懒了,只考虑了buildType,忽略favor + def hostClassesJar = hostBarExtraRootDir + "/" + hostJar + if (!file(hostClassesJar).exists()) { + //当不是使用assembleDebug和assembleRelease编译时 + println "hostClassesJar " + hostClassesJar + " 不存在,自动切换到default" + hostClassesJar = defaultExtraDir + "/" + hostJar; + } + println "添加宿主基线jar依赖:" + hostClassesJar + "\n混淆开关:minifyEnabled=" + variant.buildType.minifyEnabled + if (!variant.buildType.minifyEnabled) { + configurations[variant.buildType.name + 'CompileOnly'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } else { + configurations[variant.buildType.name + 'Implementation'].dependencies.add(project.dependencies.create(files(hostClassesJar))) + } + + def mergeResourceTask = tasks.findByName("merge${varName.capitalize()}Resources"); + mergeResourceTask.setOnlyIf { true } + mergeResourceTask.outputs.upToDateWhen { false } + mergeResourceTask.doLast { + //应用宿主的主题包 + applyThemePatch(varName, varDirName, hostBarExtraRootDir); + } + + if (isApplicationModule) { + variant.getPreBuildProvider().configure { + it.doLast { + if (rtConfig.isCanBeResolved() && rtConfig.getResolutionAlternatives() == null) { + TreeSet hashSet = new TreeSet() + //rtConfig.getIncoming().beforeResolve(new Action() {}) + rtConfig.getIncoming().getResolutionResult().getRoot().getDependencies().each() { dependencyResult-> + if (dependencyResult instanceof ResolvedDependencyResult) { + add(hashSet, dependencyResult) + } + } + File dependenciesTxt = new File("${project.buildDir}/outputs/plugin_dependencies.txt") + dependenciesTxt.getParentFile().mkdirs() + dependenciesTxt.write("// This file is automatically generated by Plugin Script\n") + dependenciesTxt.append("// variant is ${varName}\n") + hashSet.each { moduleFullName-> + dependenciesTxt.append(moduleFullName) + dependenciesTxt.append("\n") + } + dependenciesTxt.append("\n") + } + } + } + tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar").doLast { + project.copy { + from(buildDir.absolutePath + '/intermediates/compile_app_classes_jar/' + varName + "/" + name) { + include "classes.jar" + } + into(buildDir.absolutePath + "/outputs") + rename('classes', project.name + "-" + varName) + } + } + tasks.findByName("compile${varName.capitalize()}JavaWithJavac").finalizedBy tasks.findByName("bundle${varName.capitalize()}ClassesToCompileJar") + tasks.findByName("process${varName.capitalize()}Manifest").doLast { + def hostVersionName = project.ext.getProperty("host.versionName") + def hostVersionCode = project.ext.getProperty("host.versionCode") + def hostApplicationId = project.ext.getProperty("host.applicationId") + def hostBuildType = project.ext.getProperty("host.buildType") + def hostFlavorName = project.ext.getProperty("host.flavorName") + def hostVarName = project.ext.getProperty("host.varName") + def hostVarDirName = project.ext.getProperty("host.varDirName") + if (hostApplicationId != null) { + projects.logger.log(LogLevel.ERROR, "宿主基线包信息:hostVersionName=" + hostVersionName + " hostVersionCode=" + hostVersionCode + " hostApplicationId=" + hostApplicationId + " hostBuildType=" + hostBuildType + " hostFlavorName=" + hostFlavorName) + } else { + if ("".equals(hostBarExtraRootDir) || !file(hostBarExtraRootDir).exists()) { + throw new Exception("依赖的宿主基线文件不存在,请检查${project.name}工程的dependencies.baselinePatch依赖的文件是否存在。\n依赖的文件路径为:" + hostBarExtraRootDir) + } else { + throw new Exception("宿主基线包信息丢失") + } + } + + fileTree(multiApkManifestOutputDirectory).include("**/AndroidManifest.xml").each { manifestFile-> + if (hostVersionName != null && hostApplicationId != null && hostVersionCode != null) { + println "插件Manifest:" + manifestFile.absolutePath + println "插入宿主版本号标识 requiredHostVersionName=" + hostVersionName + println "插入宿主版本号标识 requiredHostVersionCode=" + hostVersionCode + println "插入宿主ID hostApplicationId=" + hostApplicationId + def originManifestContent = manifestFile.getText('UTF-8') + def modifyedManifestContent = originManifestContent.replace(" + (packageName + ":" + line.split([":"])[1]) + .replace("public_static_final_host", "public_static_final_plugin") + .replace("0x7f3", "0x7f0") + .replace("0x7f4", "0x7f1") + } + } + } + } + + //处理混淆,这里保存混淆以后dex之前的jar包作为基线包备用 + def proguardTask = project.tasks.findByName("minify${varName.capitalize()}WithR8") + if (proguardTask != null) { + if (project.ext.has("host.varName")) { + def baselineMappingFile = hostBarExtraRootDir + "/mapping/" + project.ext.getProperty("host.varName") + "/mapping.txt"; + println "发现基线mapping文件: " + baselineMappingFile + } + proguardTask.doLast { + println "混淆完成 " + varName.capitalize() + def host_obfuscated_dex = hostBarExtraRootDir + "/minifyWithR8" + if (!new File(host_obfuscated_dex).exists()) { + println "宿主基线混淆包不存在 " + host_obfuscated_dex + throw new Exception("宿主基线混淆包不存在 " + host_obfuscated_dex + ", 请检查宿主编译产物中是否包含此文件") + } + println "依赖的宿主基线混淆包路径为:" + host_obfuscated_dex + + outputs.files.files.each { File file-> + if (file.absolutePath.endsWith(proguardTask.name)) { + println "file outputs=>${file.absolutePath}" + diffDexes(file.absolutePath, host_obfuscated_dex) + } + } + } + } + } + } +} + +def add(Set set, ResolvedDependencyResult dependencyResult) { + if (!(dependencyResult.getSelected().getId() instanceof DefaultProjectComponentIdentifier)) {//源码依赖的不处理 + boolean succ = set.add(dependencyResult.getSelected().getId().displayName) + if (succ) { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult) + } + } + } + } else { + dependencyResult.getSelected().getDependencies().each() { childDependencyResult-> + if (childDependencyResult instanceof ResolvedDependencyResult) { + add(set, childDependencyResult) + } + } + } +} + +def String curl(String url, String md5, String dir) { + File file = new File(dir, url.substring(url.lastIndexOf("/") + 1)) + if (file.exists() && file.length() > 0) { + return file.absolutePath + } + println "downloading " + url + new File(dir).mkdirs(); + MessageDigest messageDigest = MessageDigest.getInstance("MD5") + messageDigest.reset() + try { + HttpURLConnection httpConn =(HttpURLConnection)(new URL(url).openConnection()) + InputStream inputStream = httpConn.getInputStream() + OutputStream ouput =new FileOutputStream(file) + byte[] buffer = new byte[8*1024] + int size + while((size = inputStream.read(buffer)) != -1) { + ouput.write(buffer, 0, size) + messageDigest.update(buffer, 0, size) + } + ouput.close() + httpConn.disconnect() + } catch(Exception){} + byte[] bs = messageDigest.digest() + StringBuilder stringBuilder = new StringBuilder() + for(byte b : bs) { + String hex = Integer.toHexString(b & 0xFF) + if (hex.length() < 2) { + stringBuilder.append("0") + } + stringBuilder.append(hex) + } + if (!md5.equalsIgnoreCase(stringBuilder.toString())) { + println "download fail: " + url + " " + stringBuilder.toString() + file.delete() + } else { + println "download success: " + url + " " + stringBuilder.toString() + } + return file.absolutePath +} +def ArrayList basksmali(String exe, String src, String dest) throws Exception +{ + ArrayList result = new ArrayList<>() + new File(dest).mkdirs() + File srcDir = new File(src); + for (File dex : srcDir.listFiles()) { + var cmd = ("java -jar " + exe + " " + + "d " + + "-o " + dest + "/" + dex.name + " " + + dex.absolutePath) + consumeStream(cmd.execute().getInputStream()) + fileTree(dest + "/" + dex.name).forEach { it-> + var relativeFile = it.absolutePath.replace(dest + "/" + dex.name, "") + result.add(relativeFile) + } + } + return result; +} +def void smali(String exe, String src, String dest) throws Exception +{ + for (File f : file(src).listFiles()) { + file(dest + "/" + f.name).delete() + var cmd = ("java -jar " + exe + " " + + "a " + + "-o " + dest + "/" + f.name + " " + + f.absolutePath) + println cmd + consumeStream(cmd.execute().getInputStream()) + } +} +def consumeStream(InputStream is) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + final byte[] buffer = new byte[256] + int len + try { + while ((len = is.read(buffer)) > 0) { + outputStream.write(buffer, 0, len) + } + } catch(IOException){} + finally { + String msg = outputStream.toString() + if (msg != null && msg.length() > 0) { + println "***exe msg begin***" + println msg + println "***exe msg end***" + } + try { + outputStream.close() + is.close() + } catch(Exception){} + } +} +def deleteDuplicated(String plugin, ArrayList hostClasses) { + for(File dexDir : file(plugin).listFiles()) { + fileTree(dexDir.absolutePath).forEach { + var relativeFile = it.absolutePath.replace(dexDir.absolutePath, "") + if (hostClasses.contains(relativeFile)) { + it.delete() + } + } + } +} +def diffDexes(String plugin, String host) { + println "通过基线混淆包和插件混淆包做diff清理重复的class" + println "插件:" + plugin + println "宿主:" + host + var BAKSMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar", "9ef9dcba677533541b8a1385e6af550b", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + var SMALI = curl("https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar", "0386bfa3676962fba8ca560392d7c78e", rootProject.rootDir.absolutePath + '/.gradle/fairy/tools') + basksmali(BAKSMALI, plugin, "${project.buildDir}/tmp/dexUnzip/plugin") + ArrayList hostClasses = basksmali(BAKSMALI, host, "${project.buildDir}/tmp/dexUnzip/host") + deleteDuplicated("${project.buildDir}/tmp/dexUnzip/plugin", hostClasses) + smali(SMALI, "${project.buildDir}/tmp/dexUnzip/plugin", plugin) +} + +def applyThemePatch(varName, varDir, hostPatchExtractDir) { + println "开始merge插件工程资源:" + hostPatchExtractDir + " 到" + varDir + + if (!file(hostPatchExtractDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + hostPatchExtractDir); + } + + def fromDir = hostPatchExtractDir + "/theme_patch/" + project.ext.getProperty("host.varDirName") + def resourceDir = project.buildDir.absolutePath + "/intermediates/incremental/merge" + varName.capitalize() + "Resources/merged.dir" + + if (!file(fromDir).exists()) { + throw new FileNotFoundException("Dir Not Found: " + fromDir); + } + + if (!file(resourceDir).exists()) { + file(resourceDir).mkdirs() + } + + FileTree allxmlFiles = fileTree(dir: fromDir) + allxmlFiles.include '**/*.xml' + + if (allxmlFiles.size() == 0) { + println fromDir + " 目录未生成,请先编译宿主!!" + throw new FileNotFoundException("theme_patch目录未生成,请先编译宿主!!\n请检查这个目录:\n" + fromDir); + } + + allxmlFiles.each { File itemFile -> + file(buildDir.getAbsolutePath() + "/intermediates/merged_res/" + varName).mkdirs() + + File buildToolsPath = new File(android.getAdbExecutable().getParentFile().getParentFile(), "build-tools/" + android.getBuildToolsVersion()) + String aapt2Exe = new File(buildToolsPath, "aapt2" + (System.getProperty("os.name").startsWith("Windows")?".exe":"")).absolutePath + + projects.logger.log(LogLevel.ERROR, "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName/merge${varName.capitalize()}Resources $itemFile") + "$aapt2Exe compile -o $buildDir.absolutePath/intermediates/merged_res/$varName/merge${varName.capitalize()}Resources $itemFile".execute().waitFor() + } +} + +public class StyleProcessor extends DefaultHandler { + + File destFile; + Stack stack = new Stack(); + TransformerHandler outXmlHandler; + + SortedMap> attrList = new TreeMap>(); + HashSet attrSets = new HashSet(); + + HashMap enumItemList = new HashMap(); + + HashMap> attrFlagMap = new HashMap>(); + + public StyleProcessor(File destFile) { + this.destFile = destFile; + + SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + outXmlHandler = factory.newTransformerHandler(); + + } + + public static StyleProcessor fixDeclareStyle(File srcFile, File destFile) { + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + StyleProcessor processor = new StyleProcessor(destFile); + BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), "UTF-8")); + InputSource inputSource = new InputSource(br) + saxParser.parse(inputSource, processor); + return processor; + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (SAXException e) { + System.out.println(e.getMessage()); + } catch (FileNotFoundException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + return null; + } + + //////////////// + //////////////// + //////////////// + + public void startDocument() throws SAXException { + try { + javax.xml.transform.Transformer transformer = outXmlHandler.getTransformer(); // 设置xml属性 + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + outXmlHandler.setResult(new StreamResult(new OutputStreamWriter(new FileOutputStream(destFile), "UTF-8"))); + char[] common = new String("\n AUTO-GENERATED FILE. DO NOT MODIFY \n").toCharArray(); + outXmlHandler.comment(common, 0, common.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException { + + boolean skip = false; + if (!qName.equals("declare-styleable")) { + String space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + String tag = space + "<" + qName; + for (int i = 0; i < attributes.getLength(); i++) { + tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; + } + tag = tag + ">"; + + if (qName.equals("attr") && stack.size() == 2) { + String parentDecalreStyleName = attrList.lastKey(); + attrList.get(parentDecalreStyleName).add(attributes.getValue("name")); + } + + if (qName.equals("enum") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String regx = ":" + attr + "\\s*=\\s*\"" + attributes.getValue("name") + "\""; + String regValue = ":" + attr + "=\"" + attributes.getValue("value") + "\""; + println "prepare enum att regx " + regx + "-->" + regValue + " enumItemList size = " + enumItemList.size(); + enumItemList.put(regx, regValue) + + } + + if (qName.endsWith("flag") && stack.size() == 3) { + + Node n3 = stack.get(2); + String attr = n3.name; + + String flagName = attributes.getValue("name"); + String flagValue = attributes.getValue("value"); + + HashMap item = attrFlagMap.get(attr); + if (item == null) { + item = new HashMap(); + attrFlagMap.put(attr, item) + } + + println "collect attr flag " + attr + "={" + flagName + "=" + flagValue + "}" + + item.put(flagName, flagValue); + } + + if (qName.equals("attr")) { + if (!attrSets.contains(attributes.getValue("name"))) { + attrSets.add(attributes.getValue("name")); + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } else { + skip = true; + } + } else { + try { + outXmlHandler.startElement(uri, localName, qName, attributes) + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + //declare-styleable的name属性 + attrList.put(attributes.getValue("name"), new ArrayList()); + } + + if (!stack.empty()) { + Node top = stack.peek(); + top.hasChild = true; + } + stack.add(new Node(attributes.getValue("name"), false, skip)); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + + Node node = stack.pop(); + if (node.skip) { + return; + } + + if (!qName.equals("declare-styleable")) { + String space = ""; + if (node.hasChild) { + space = "\n"; + for (int i = 0; i < stack.size(); i++) { + space = space + " "; + } + } + try { + outXmlHandler.endElement(uri, localName, qName) + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + public void characters(char[] ch, int start, int length) throws SAXException { + Node node = stack.peek(); + if (node.skip) { + return; + } + + try { + outXmlHandler.characters(ch, start, length) + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void endDocument() throws SAXException { + try { + outXmlHandler.endDocument(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static class Node { + String name = null; + boolean hasChild = false; + boolean skip = false; + + public Node(String name, boolean hasChild, boolean skip) { + this.name = name; + this.hasChild = hasChild; + this.skip = skip; + } + } + + public static String fixAttrFlag(final String xmlText, HashMap> attrFlagMap) { + + String localText = xmlText; + + Iterator>> flagItr = attrFlagMap.entrySet().iterator(); + + while (flagItr.hasNext()) { + Map.Entry> entry = flagItr.next(); + + HashMap flagValueMap = entry.getValue(); + String attrName = entry.getKey(); + + String regx = ":" + attrName + "\\s*=\\s*\".+?\""; + + Matcher matcher = Pattern.compile(regx).matcher(localText); + + HashMap flagsMap = new HashMap(); + + while(matcher.find()) { + + String text0 = matcher.group(0); + + String flagValue = text0.split("=")[1].trim().replaceAll("\"", ""); + String[] flags = flagValue.split("\\|"); + + Integer flagIntValue = null; + for(String flag: flags) { + + String intFlag = flagValueMap.get(flag); + int definedValue; + if (intFlag.startsWith("0x")) { + //16进制 + definedValue = Integer.valueOf(intFlag.substring(2), 16); + } else { + //10进制 + definedValue = Integer.valueOf(intFlag); + } + + if (flagIntValue == null) { + flagIntValue = definedValue; + } else { + flagIntValue = flagIntValue | definedValue; + } + } + + String text0ed = ":" + attrName + "=\"" + flagIntValue + "\""; + text0 = text0.replaceAll("\\|", "\\\\|");//正则转义 + + println "prepare enum att regx " + attrName + ", " + text0 + " --> " + text0ed + + flagsMap.put(text0, text0ed); + } + + Iterator> iterator = flagsMap.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry stringEntry = iterator.next(); + localText = localText.replaceAll(stringEntry.getKey(), stringEntry.getValue()); + } + } + + return localText; + } + +} diff --git a/FairyPlugin/build.gradle b/FairyPlugin/build.gradle new file mode 100644 index 00000000..3d62db3b --- /dev/null +++ b/FairyPlugin/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' + +apply from: './jitpack.gradle' + +android { + compileSdkVersion COMPILE_SDK_VERSION + buildToolsVersion BUILD_TOOLS_VERSION + + namespace = 'com.limpoxe.fairy' + + defaultConfig { + minSdkVersion MIN_SDK_VERSION + targetSdkVersion TARGET_SDK_VERSION + } + + lint { + abortOnError = false + checkReleaseBuilds = false + disable 'MissingClass', + 'MissingPermission', + 'QueryPermissionsNeeded', + 'ExportedReceiver', + 'ApplySharedPref', + 'ObsoleteSdkInt', + 'PackageManagerGetSignatures', + 'StaticFieldLeak', + 'UseValueOf', + 'NewApi', + 'PrivateApi', + 'DiscouragedPrivateApi', + 'SoonBlockedPrivateApi' + htmlReport = false + textReport = false + xmlReport = false + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +dependencies { + compileOnly 'androidx.collection:collection:1.1.0' + //这个依赖不是必须, 引入这个库是为了增强插件间通信能力 + //https://github.com/limpoxe/Android-ServiceManager + implementation 'com.limpoxe.support:android-servicemanager:1.0.5@aar' +} \ No newline at end of file diff --git a/FairyPlugin/host.gradle b/FairyPlugin/host.gradle new file mode 100644 index 00000000..32f59967 --- /dev/null +++ b/FairyPlugin/host.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/host.gradle" diff --git a/FairyPlugin/jitpack.gradle b/FairyPlugin/jitpack.gradle new file mode 100644 index 00000000..1b0a3644 --- /dev/null +++ b/FairyPlugin/jitpack.gradle @@ -0,0 +1,44 @@ +apply plugin: 'maven-publish' + + +// load properties +Properties properties = new Properties() +File localPropertiesFile = project.file("local.properties"); +if(localPropertiesFile.exists()){ + properties.load(localPropertiesFile.newDataInputStream()) +} +File projectPropertiesFile = project.file("project.properties"); +if(projectPropertiesFile.exists()){ + properties.load(projectPropertiesFile.newDataInputStream()) +} + +// read properties +def projectName = properties.getProperty("project.name") +def projectGroupId = properties.getProperty("project.groupId") +def projectArtifactId = properties.getProperty("project.artifactId") +def projectVersion = properties.getProperty("project.version") +def projectPackaging = properties.getProperty("project.packaging") +def projectSiteUrl = properties.getProperty("project.siteUrl") +def projectGitUrl = properties.getProperty("project.gitUrl") + +def developerId = properties.getProperty("developer.id") +def developerName = properties.getProperty("developer.name") +def developerEmail = properties.getProperty("developer.email") + +def javadocName = properties.getProperty("javadoc.name") + +// jitpack commad: +// ./gradlew clean -Pgroup=com.github.limpoxe -Pversion= -xtest -xlint assemble publishToMavenLocal +publishing { + publications { + release(MavenPublication) { + groupId = projectGroupId + artifactId = projectArtifactId + version = version // 'unspecified' or command args + + afterEvaluate { + from components.release + } + } + } +} \ No newline at end of file diff --git a/FairyPlugin/jitpack.yml b/FairyPlugin/jitpack.yml new file mode 100644 index 00000000..db342ca6 --- /dev/null +++ b/FairyPlugin/jitpack.yml @@ -0,0 +1,7 @@ +before_install: + - yes | sdkmanager "cmake;3.10.2.4988404" + - sdk install java 11.0.10-open + - sdk use java 11.0.10-open + +jdk: + - openjdk11 \ No newline at end of file diff --git a/FairyPlugin/plugin.gradle b/FairyPlugin/plugin.gradle new file mode 100644 index 00000000..452b73d8 --- /dev/null +++ b/FairyPlugin/plugin.gradle @@ -0,0 +1 @@ +apply from : "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp4_1_3/plugin.gradle" diff --git a/PluginTest/proguard-project.txt b/FairyPlugin/proguard-project.txt similarity index 100% rename from PluginTest/proguard-project.txt rename to FairyPlugin/proguard-project.txt diff --git a/FairyPlugin/proguard-rules.pro b/FairyPlugin/proguard-rules.pro new file mode 100644 index 00000000..c0eaab7f --- /dev/null +++ b/FairyPlugin/proguard-rules.pro @@ -0,0 +1,23 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/cailiming/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# 保护FairyPlugin不受混淆影响 Begin +-keep public class * extends android.app.Instrumentation {public *;} +-keep public class * extends android.content.ContextWrapper {public *;} +-keep public class com.limpoxe.support.servicemanager.ServiceManager {public *;} +# 保护FairyPlugin不受混淆影响 End \ No newline at end of file diff --git a/FairyPlugin/project.properties b/FairyPlugin/project.properties new file mode 100644 index 00000000..64f4fed8 --- /dev/null +++ b/FairyPlugin/project.properties @@ -0,0 +1,14 @@ +project.name=Android-Plugin-Framework +project.groupId=com.github.limpoxe +project.artifactId=Android-Plugin-Framework +project.packaging=aar +project.siteUrl=https://github.com/limpoxe/Android-Plugin-Framework +project.gitUrl=https://github.com/limpoxe/Android-Plugin-Framework.git + +#developer +developer.id=limpoxe +developer.name=limpoxe +developer.email=fixerror@163.com + +#javadoc +javadoc.name=Android-Plugin-Framework \ No newline at end of file diff --git a/FairyPlugin/src/main/AndroidManifest.xml b/FairyPlugin/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4760e8d7 --- /dev/null +++ b/FairyPlugin/src/main/AndroidManifest.xml @@ -0,0 +1,811 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/content/LoadedPlugin.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/LoadedPlugin.java new file mode 100644 index 00000000..69cad8f4 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/LoadedPlugin.java @@ -0,0 +1,53 @@ +package com.limpoxe.fairy.content; + +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; + +import com.limpoxe.fairy.util.LogUtil; + +/** + * Created by cailiming on 16/3/9. + * + */ +public class LoadedPlugin { + + public final ClassLoader pluginClassLoader; + public final Context pluginContext; + public final Resources pluginResource; + + public final String pluginPackageName; + public final String pluginSourceDir; + + public boolean applicationOnCreateCalled = false; + + public Application pluginApplication; + + public LoadedPlugin(String packageName, + String pluginSourceDir, + Context pluginContext, + ClassLoader pluginClassLoader) { + this.pluginPackageName = packageName; + this.pluginSourceDir = pluginSourceDir; + this.pluginContext = pluginContext; + this.pluginClassLoader = pluginClassLoader; + this.pluginResource = pluginContext.getResources(); + } + + public Class loadClassByName(String clazzName) { + try { + Class pluginClazz = ((ClassLoader) pluginClassLoader).loadClass(clazzName); + LogUtil.v("loadPluginClass Success for clazzName ", clazzName); + return pluginClazz; + } catch (ClassNotFoundException e) { + LogUtil.printException("ClassNotFound " + clazzName, e); + } catch (IllegalAccessError illegalAccessError) { + illegalAccessError.printStackTrace(); + throw new IllegalAccessError("出现这个异常最大的可能是插件dex和" + + "宿主dex包含了相同的class导致冲突, " + + "请检查插件的编译脚本,确保排除了所有公共依赖库的jar"); + } + return null; + } + +} diff --git a/PluginCore/src/com/plugin/content/PluginActivityInfo.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginActivityInfo.java similarity index 78% rename from PluginCore/src/com/plugin/content/PluginActivityInfo.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginActivityInfo.java index a82bd43f..763dc320 100644 --- a/PluginCore/src/com/plugin/content/PluginActivityInfo.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginActivityInfo.java @@ -1,4 +1,4 @@ -package com.plugin.content; +package com.limpoxe.fairy.content; import android.content.pm.ActivityInfo; @@ -20,6 +20,16 @@ public class PluginActivityInfo implements Serializable { private String theme;//int private String immersive;//int string private String uiOptions; + private int configChanges; + private boolean useHostPackageName = false; + + public int getConfigChanges() { + return configChanges; + } + + public void setConfigChanges(int configChanges) { + this.configChanges = configChanges; + } public String getUiOptions() { return uiOptions; @@ -84,4 +94,13 @@ public String getTheme() { public void setTheme(String theme) { this.theme = theme; } + + public boolean isUseHostPackageName() { + return useHostPackageName; + } + + public void setUseHostPackageName(boolean useHostPackageName) { + this.useHostPackageName = useHostPackageName; + } + } diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginDescriptor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginDescriptor.java new file mode 100644 index 00000000..4817f874 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginDescriptor.java @@ -0,0 +1,743 @@ +package com.limpoxe.fairy.content; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.Bundle; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + *
+ * @author cailiming
+ * 
+ */ +public class PluginDescriptor implements Serializable { + + public static final int UNKOWN = 0; + public static final int BROADCAST = 1; + public static final int ACTIVITY = 2; + public static final int SERVICE = 4; + public static final int PROVIDER = 6; + public static final int FRAGMENT = 8; + public static final int FUNCTION = 9; + + private static final long serialVersionUID = 6742761531732381741L; + + private String packageName; + + private String platformBuildVersionCode; + + private String platformBuildVersionName; + + private String minSdkVersion; + + private String targetSdkVersion; + + private String versionCode; + private String versionName; + + private String requiredHostVersionName; + + private boolean autoStart; + + private String description; + + private boolean isStandalone; + + private boolean isEnabled; + + private String applicationName; + + private int applicationIcon; + + private int applicationLogo; + + private int applicationTheme; + + private boolean useHostPackageName; + + /** + * 定义在插件Manifest中的meta-data标签 + */ + + private transient Bundle metaData; + + private HashMap metaDataObject = new HashMap(); + private HashMap metaDataResource = new HashMap(); + private HashMap metaDataTobeInflate = new HashMap(); + + private HashMap providerInfos = new HashMap(); + + /** + * key: fragment id, + * value: fragment class + */ + private HashMap fragments = new HashMap(); + + /** + * key: localservice id, + * value: localservice class + */ + private HashMap functions = new HashMap(); + + /** + * key: activity class name + * value: intentfilter list + */ + private HashMap> activitys = new HashMap>(); + + /** + * key: activity class name + * value: activity info in Manifest + */ + private HashMap activityInfos = new HashMap(); + + /** + * key: service class name + * value: intentfilter list + */ + private HashMap> services = new HashMap>(); + + private HashMap serviceInfos = new HashMap(); + + /** + * key: receiver class name + * value: intentfilter list + */ + private HashMap> receivers = new HashMap>(); + + private String rootDir; + private String versionedRootDir; + private String nativeLibDir; + private String dalvikCacheDir; + private String dataDir; + private String installedPath; + private long fileSize; + private String fileMd5; + private String fileCrc; + private long installationTime; + + private String[] dependencies; + + private ArrayList muliDexList; + + private transient HashMap packageInfoHashMap; + + //=============getter and setter====================== + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getPlatformBuildVersionCode() { + return platformBuildVersionCode; + } + + public void setPlatformBuildVersionCode(String platformBuildVersionCode) { + this.platformBuildVersionCode = platformBuildVersionCode; + } + + public String getPlatformBuildVersionName() { + return platformBuildVersionName; + } + + public void setPlatformBuildVersionName(String platformBuildVersionName) { + this.platformBuildVersionName = platformBuildVersionName; + } + + public String getMinSdkVersion() { + return minSdkVersion; + } + + public void setMinSdkVersion(String minSdkVersion) { + this.minSdkVersion = minSdkVersion; + } + + public String getTargetSdkVersion() { + return targetSdkVersion; + } + + public void setTargetSdkVersion(String targetSdkVersion) { + this.targetSdkVersion = targetSdkVersion; + } + + public String getVersion() { + return versionName + "_" + versionCode; + } + + public String getVersionCode() { + return versionCode; + } + + public void setVersionCode(String versionCode) { + this.versionCode = versionCode; + } + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + + public void setRequiredHostVersionName(String requiredHostVersionName) { + this.requiredHostVersionName = requiredHostVersionName; + } + + public String getRequiredHostVersionName() { + return requiredHostVersionName; + } + + public boolean getAutoStart() { + return autoStart; + } + + public void setAutoStart(boolean autoStart) { + this.autoStart = autoStart; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getApplicationIcon() { + return applicationIcon; + } + + public void setApplicationIcon(int icon) { + this.applicationIcon = icon; + } + + public int getApplicationLogo() { + return applicationLogo; + } + + public void setApplicationLogo(int logo) { + this.applicationLogo = logo; + } + + public int getApplicationTheme() { + return applicationTheme; + } + + public void setApplicationTheme(int theme) { + this.applicationTheme = theme; + } + + public static void inflateMetaData(PluginDescriptor descriptor, Resources pluginRes) { + Bundle metaData = descriptor.getMetaData(); + + if (metaData == null) { + LogUtil.i("开始填充插件MetaData"); + + metaData = new Bundle(); + //todo cailiming bugfix metadata需要按manifest中的节点分开存储,不能合并成一个大bundle + descriptor.setMetaData(metaData); + + Iterator strItr = descriptor.getMetaDataObject().keySet().iterator(); + while(strItr.hasNext()) { + String key = strItr.next(); + Object value = descriptor.getMetaDataObject().get(key); + LogUtil.d(key, value); + if (value instanceof Boolean) { + metaData.putBoolean(key, (Boolean) value); + } else if (value instanceof Float) { + metaData.putFloat(key, (Float) value); + } else if (value instanceof Integer) { + metaData.putInt(key, (Integer) value); + } else if (value instanceof String) { + metaData.putString(key, (String) value); + } + } + + Iterator> resItr = descriptor.getMetaDataResource().entrySet().iterator(); + while(resItr.hasNext()) { + Map.Entry entry = resItr.next(); + LogUtil.d(entry.getKey(), entry.getValue()); + metaData.putInt(entry.getKey(), entry.getValue()); + } + + HashMap resIdData = descriptor.getMetaDataTobeInflate(); + if (resIdData != null) { + Iterator> itr = resIdData.entrySet().iterator(); + while(itr.hasNext()){ + Map.Entry entry = itr.next(); + String resId = entry.getValue(); + String key = entry.getKey(); + + String packageName = null; + int id = 0; + if (resId.contains(":")) { + String[] names = resId.split(":"); + packageName = names[0].replace("@", ""); + id = (int)Long.parseLong(names[1], 16); + } else { + packageName = descriptor.getPackageName(); + id = (int)Long.parseLong(resId.replace("@", ""), 16); + } + + Resources resources = null; + if (packageName.equals(descriptor.getPackageName())) { + resources = pluginRes; + } else if (packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + resources = FairyGlobal.getHostApplication().getResources(); + } else if (packageName.equals("android")) { + resources = Resources.getSystem(); + } else { + //?? + } + + if (resources != null && id != 0) { + String type = resources.getResourceTypeName(id); + LogUtil.d("inflateMetaData", "type", type, id, key); + if ("string".equals(type)) { + metaData.putString(key, resources.getString(id)); + } else if ("integer".equals(type)) { + metaData.getInt(key, resources.getInteger(id)); + } else if ("boolean".equals(type)) { + metaData.putBoolean(key, resources.getBoolean(id)); + } else { + //int array?? + } + } + } + } + LogUtil.i("填充插件MetaData 完成"); + } + } + + public Bundle getMetaData() { + return metaData; + } + + public void setMetaData(Bundle metaData) { + this.metaData = metaData; + } + + public HashMap getMetaDataObject() { + return metaDataObject; + } + + public void setMetaDataObject(HashMap metaDataObj) { + this.metaDataObject = metaDataObj; + } + + public HashMap getMetaDataResource() { + return metaDataResource; + } + + public void setMetaDataResource(HashMap metaDataResource) { + this.metaDataResource = metaDataResource; + } + + public HashMap getMetaDataTobeInflate() { + return metaDataTobeInflate; + } + + public void setMetaDataTobeInflate(HashMap metaDataTobeInflate) { + this.metaDataTobeInflate = metaDataTobeInflate; + } + + public HashMap getFragments() { + return fragments; + } + + public void setfragments(HashMap fragments) { + this.fragments = fragments; + } + + public HashMap getFunctions() { + return functions; + } + + public void setFunctions(HashMap functions) { + this.functions = functions; + } + + public HashMap> getReceivers() { + return receivers; + } + + public void setReceivers(HashMap> receivers) { + this.receivers = receivers; + } + + public HashMap> getActivitys() { + return activitys; + } + + public void setActivitys(HashMap> activitys) { + this.activitys = activitys; + } + + public HashMap getActivityInfos() { + return activityInfos; + } + + public void setActivityInfos(HashMap activityInfos) { + this.activityInfos = activityInfos; + } + + public HashMap getServiceInfos() { + return serviceInfos; + } + + public void setServiceInfos(HashMap serviceInfos) { + this.serviceInfos = serviceInfos; + } + + public HashMap> getServices() { + return services; + } + + public void setServices(HashMap> services) { + this.services = services; + } + + public static String getFairyDir() { + return FairyGlobal.getHostApplication().getDir("plugin_dir", Context.MODE_PRIVATE).getAbsolutePath(); + } + + public String getRootDir() { + if (rootDir == null) { + rootDir = getFairyDir() + File.separator + getPackageName(); + } + return rootDir; + } + + public void setRootDir(String rootDir) { + this.rootDir = rootDir; + } + + public String getVersionedRootDir() { + if (versionedRootDir == null) { + versionedRootDir = getRootDir() + File.separator + getVersion(); + } + return versionedRootDir; + } + + public void setVersionedRootDir(String versionedRootDir) { + this.versionedRootDir = versionedRootDir; + } + + public String getNativeLibDir() { + if (nativeLibDir == null) { + nativeLibDir = getVersionedRootDir() + File.separator + "lib"; + } + return nativeLibDir; + } + + public void setNativeLibDir(String nativeLibDir) { + this.nativeLibDir = nativeLibDir; + } + + public String getDalvikCacheDir() { + if (dalvikCacheDir == null) { + dalvikCacheDir = getVersionedRootDir() + File.separator + "dalvik-cache"; + } + return dalvikCacheDir; + } + + public void setDalvikCacheDir(String dalvikCacheDir) { + this.dalvikCacheDir = dalvikCacheDir; + } + + public String getDataDir() { + if (dataDir == null) { + dataDir = getRootDir() + File.separator + "data"; + } + return dataDir; + } + + public void setDataDir(String dataDir) { + this.dataDir = dataDir; + } + + public String getInstalledPath() { + if (installedPath == null) { + installedPath = getVersionedRootDir() + File.separator + "base-1.apk"; + } + return installedPath; + } + + public void setInstalledPath(String installedPath) { + this.installedPath = installedPath; + } + + public long getFileSize() { + return fileSize; + } + + public void setFileSize(long fileSize) { + this.fileSize = fileSize; + } + + public String getFileMd5() { + return fileMd5; + } + + public void setFileMd5(String fileMd5) { + this.fileMd5 = fileMd5; + } + + public String getFileCrc() { + return fileCrc; + } + + public void setFileCrc(String fileCrc) { + this.fileCrc = fileCrc; + } + + public String[] getDependencies() { + return dependencies; + } + + public void setDependencies(String[] dependencies) { + this.dependencies = dependencies; + } + + public List getMuliDexList() { + return muliDexList; + } + + public void setMuliDexList(ArrayList muliDexList) { + this.muliDexList = muliDexList; + } + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public boolean isEnabled() { + return isEnabled; + } + + public void setEnabled(boolean isEnabled) { + this.isEnabled = isEnabled; + } + + public boolean isStandalone() { + return isStandalone; + } + + public void setStandalone(boolean isStandalone) { + this.isStandalone = isStandalone; + } + + public HashMap getProviderInfos() { + return providerInfos; + } + + public void setProviderInfos(HashMap providerInfos) { + this.providerInfos = providerInfos; + } + + public boolean isUseHostPackageName() { + return useHostPackageName; + } + + public void setUseHostPackageName(boolean useHostPackageName) { + this.useHostPackageName = useHostPackageName; + } + + public long getInstallationTime() { + return installationTime; + } + + public void setInstallationTime(long installationTime) { + this.installationTime = installationTime; + } + + /** + * 需要根据id查询的只有fragment + * @param clazzId + * @return + */ + public String getPluginClassNameById(String clazzId) { + String clazzName = getFragments().get(clazzId); + + if (clazzName == null) { + LogUtil.w("PluginDescriptor", "clazzName not found for classId ", clazzId); + } else { + LogUtil.v("PluginDescriptor", "clazzName found ", clazzName); + } + return clazzName; + } + + /** + * 需要根据Id查询的只有fragment + * @param clazzId + * @return + */ + public boolean containsFragment(String clazzId) { + if (getFragments().containsKey(clazzId) && isEnabled()) { + return true; + } + return false; + } + + /** + * 根据className查询 + * @param clazzName + * @return + */ + public boolean containsName(String clazzName) { + if (getFragments().containsValue(clazzName) && isEnabled()) { + return true; + } else if (getActivitys().containsKey(clazzName) && isEnabled()) { + return true; + } else if (getReceivers().containsKey(clazzName) && isEnabled()) { + return true; + } else if (getServices().containsKey(clazzName) && isEnabled()) { + return true; + } else if (getProviderInfos().containsKey(clazzName) && isEnabled()) { + return true; + } else if (getApplicationName().equals(clazzName) && !clazzName.equals(Application.class.getName()) && isEnabled()) { + return true; + } + return false; + } + + /** + * 获取class的类型: activity + * @return + */ + public int getType(String clazzName) { + if (getFragments().containsValue(clazzName) && isEnabled()) { + return FRAGMENT; + } else if (getActivitys().containsKey(clazzName) && isEnabled()) { + return ACTIVITY; + } else if (getReceivers().containsKey(clazzName) && isEnabled()) { + return BROADCAST; + } else if (getServices().containsKey(clazzName) && isEnabled()) { + return SERVICE; + } else if (getProviderInfos().containsKey(clazzName) && isEnabled()) { + return PROVIDER; + } + return UNKOWN; + } + + public boolean isBroken() { + //简单检查一下文件是否已损坏 + File file = new File(getInstalledPath()); + if (!file.exists()) { + return true; + } + if (!file.canRead()) { + return true; + } + if (file.length() == 0) { + return true; + } + if (getFileSize() > 0 && file.length() != getFileSize()) { + return true; + } + //md5 or crc check + return false; + } + + public ArrayList matchPlugin(Intent intent, int type) { + PluginDescriptor plugin = this; + ArrayList result = null; + String clazzName = null; + // 如果是通过组件进行匹配的, 这里忽略了packageName + if (intent.getComponent() != null) { + if (plugin.containsName(intent.getComponent().getClassName())) { + clazzName = intent.getComponent().getClassName(); + result = new ArrayList(1); + result.add(clazzName); + LogUtil.e("暂时不考虑不同的插件中配置了相同类全名的组件的问题, 先到先得", getPackageName(), clazzName); + return result; + } + } else { + // 如果是通过IntentFilter进行匹配的 + ArrayList list = null; + if (type == PluginDescriptor.ACTIVITY) { + list = findClassNameByIntent(intent, plugin.getActivitys()); + } else if (type == PluginDescriptor.SERVICE) { + list = findClassNameByIntent(intent, plugin.getServices()); + } else if (type == PluginDescriptor.BROADCAST) { + list = findClassNameByIntent(intent, plugin.getReceivers()); + } + return list; + } + return null; + } + + private static ArrayList findClassNameByIntent(Intent intent, HashMap> intentFilter) { + if (intentFilter != null) { + ArrayList targetClassNameList = null; + + Iterator>> entry = intentFilter.entrySet().iterator(); + while (entry.hasNext()) { + Map.Entry> item = entry.next(); + Iterator values = item.getValue().iterator(); + while (values.hasNext()) { + PluginIntentFilter filter = values.next(); + int result = filter.match(intent.getAction(), intent.getType(), intent.getScheme(), + intent.getData(), intent.getCategories()); + + if (result != PluginIntentFilter.NO_MATCH_ACTION + && result != PluginIntentFilter.NO_MATCH_CATEGORY + && result != PluginIntentFilter.NO_MATCH_DATA + && result != PluginIntentFilter.NO_MATCH_TYPE) { + if (targetClassNameList == null) { + targetClassNameList = new ArrayList(); + } + targetClassNameList.add(item.getKey()); + } + } + } + return targetClassNameList; + } + return null; + } + + public PackageInfo getPackageInfo(Object flags) { + if (packageInfoHashMap == null) { + packageInfoHashMap = new HashMap<>(); + } + PackageInfo packageInfo = packageInfoHashMap.get(flags); + if (packageInfo == null) { + if (flags instanceof Integer) { + packageInfo = FairyGlobal.getHostApplication().getPackageManager().getPackageArchiveInfo(getInstalledPath(), (int)flags); + } else { + packageInfo = FairyGlobal.getHostApplication().getPackageManager().getPackageArchiveInfo(getInstalledPath(), PackageManager.PackageInfoFlags.of((long)flags)); + } + if (packageInfo != null && packageInfo.applicationInfo != null) { + packageInfo.applicationInfo.sourceDir = getInstalledPath(); + packageInfo.applicationInfo.publicSourceDir = getInstalledPath(); + } + packageInfoHashMap.put(flags, packageInfo); + } + + return packageInfo; + } +} diff --git a/PluginCore/src/com/plugin/content/PluginIntentFilter.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginIntentFilter.java similarity index 98% rename from PluginCore/src/com/plugin/content/PluginIntentFilter.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginIntentFilter.java index e892f9c2..fcacae49 100644 --- a/PluginCore/src/com/plugin/content/PluginIntentFilter.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginIntentFilter.java @@ -1,21 +1,19 @@ -package com.plugin.content; +package com.limpoxe.fairy.content; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; import android.util.AndroidException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.Set; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import com.plugin.util.LogUtil; - /** * Copy from Android SDK */ @@ -1070,22 +1068,22 @@ public final int match(String action, String type, String scheme, return dataMatch; } - public void readFromXml(String tagName, XmlPullParser parser) throws XmlPullParserException, + public void readFromXml(String tagName, String nameSpace, XmlPullParser parser) throws XmlPullParserException, IOException { if (tagName.equals(ACTION_STR)) { - String name = parser.getAttributeValue(null, NAME_STR); + String name = parser.getAttributeValue(nameSpace, NAME_STR); if (name != null) { addAction(name); } } else if (tagName.equals(CAT_STR)) { - String name = parser.getAttributeValue(null, NAME_STR); + String name = parser.getAttributeValue(nameSpace, NAME_STR); if (name != null) { addCategory(name); } } else if (tagName.equals("data")) { - String name = parser.getAttributeValue(null, TYPE_STR); + String name = parser.getAttributeValue(nameSpace, TYPE_STR); if (name != null) { try { addDataType(name); @@ -1093,7 +1091,7 @@ public void readFromXml(String tagName, XmlPullParser parser) throws XmlPullPars } } - name = parser.getAttributeValue(null, SCHEME_STR); + name = parser.getAttributeValue(nameSpace, SCHEME_STR); if (name != null) { addDataScheme(name); } @@ -1108,19 +1106,19 @@ public void readFromXml(String tagName, XmlPullParser parser) throws XmlPullPars // } //AUTH_STR - String host = parser.getAttributeValue(null, HOST_STR); - String port = parser.getAttributeValue(null, PORT_STR); + String host = parser.getAttributeValue(nameSpace, HOST_STR); + String port = parser.getAttributeValue(nameSpace, PORT_STR); if (host != null) { addDataAuthority(host, port); } //PATH_STR - String path = parser.getAttributeValue(null, LITERAL_STR); + String path = parser.getAttributeValue(nameSpace, LITERAL_STR); if (path != null) { addDataPath(path, PluginPatternMatcher.PATTERN_LITERAL); - } else if ((path=parser.getAttributeValue(null, PREFIX_STR)) != null) { + } else if ((path=parser.getAttributeValue(nameSpace, PREFIX_STR)) != null) { addDataPath(path, PluginPatternMatcher.PATTERN_PREFIX); - } else if ((path=parser.getAttributeValue(null, SGLOB_STR)) != null) { + } else if ((path=parser.getAttributeValue(nameSpace, SGLOB_STR)) != null) { addDataPath(path, PluginPatternMatcher.PATTERN_SIMPLE_GLOB); } } else { diff --git a/PluginCore/src/com/plugin/content/PluginPatternMatcher.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginPatternMatcher.java similarity index 99% rename from PluginCore/src/com/plugin/content/PluginPatternMatcher.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginPatternMatcher.java index 5860ae87..d046acff 100644 --- a/PluginCore/src/com/plugin/content/PluginPatternMatcher.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginPatternMatcher.java @@ -1,4 +1,4 @@ -package com.plugin.content; +package com.limpoxe.fairy.content; import java.io.Serializable; diff --git a/PluginCore/src/com/plugin/content/PluginProviderInfo.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginProviderInfo.java similarity index 95% rename from PluginCore/src/com/plugin/content/PluginProviderInfo.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginProviderInfo.java index 621dd1d6..d0227d3d 100644 --- a/PluginCore/src/com/plugin/content/PluginProviderInfo.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginProviderInfo.java @@ -1,4 +1,4 @@ -package com.plugin.content; +package com.limpoxe.fairy.content; import java.io.Serializable; /** @@ -11,8 +11,6 @@ */ public class PluginProviderInfo implements Serializable { - public static final String CLASS_PREFIX = "_CONTENT_PROVIDER_"; - private String name; private String packageName; diff --git a/PluginCore/src/com/plugin/content/PluginReceiverIntent.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginReceiverIntent.java similarity index 57% rename from PluginCore/src/com/plugin/content/PluginReceiverIntent.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginReceiverIntent.java index 47a8d941..e6373618 100644 --- a/PluginCore/src/com/plugin/content/PluginReceiverIntent.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/content/PluginReceiverIntent.java @@ -1,6 +1,6 @@ -package com.plugin.content; +package com.limpoxe.fairy.content; -import android.annotation.TargetApi; +import android.annotation.SuppressLint; import android.content.Intent; import android.os.Build; import android.os.Bundle; @@ -8,18 +8,20 @@ /** * Created by cailiming on 15/9/29. */ +@SuppressLint("ParcelCreator") public class PluginReceiverIntent extends Intent { public PluginReceiverIntent(Intent o) { super(o); } - @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void setExtrasClassLoader(ClassLoader loader) { - Bundle extra = getExtras(); - if (extra != null) { - loader = extra.getClassLoader(); + if (Build.VERSION.SDK_INT > 11) { + Bundle extra = getExtras(); + if (extra != null) { + loader = extra.getClassLoader(); + } } super.setExtrasClassLoader(loader); } diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/DemoApplication.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/DemoApplication.java new file mode 100644 index 00000000..617fdae7 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/DemoApplication.java @@ -0,0 +1,40 @@ +package com.limpoxe.fairy.core; + +import android.app.Application; +import android.content.Context; + +import com.limpoxe.fairy.util.ProcessUtil; + +public class DemoApplication extends Application { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + + //这个地方之所以这样写,是因为如果是插件进程,initloader必须在applicaiotn启动时执行 + //而如果是宿主进程,initloader可以在这里执行,也可以在需要时再在宿主的其他组件中执行, + // 例如点击宿主的某个Activity中的button后再执行这个方法来启动插件框架。 + + //总体原则有3点: + //1、插件进程和宿主进程都必须有机会执行initloader + //2、在插件进程和宿主进程的initloader方法都执行完毕之前,不可和插件交互 + //3、在插件进程和宿主进程的initlaoder方法都执行完毕之前启动的组件,即使在initloader都执行完毕之后,也不可和插件交互 + + //如果initloader都在进程启动时就执行,自然很轻松满足上述条件。 + if (ProcessUtil.isPluginProcess(this)) { + //插件进程,必须在这里执行initLoader + PluginLoader.initLoader(this); + } else { + //宿主进程,可以在这里执行,也可以选择在宿主的其他地方在需要时再启动插件框架 + PluginLoader.initLoader(this); + } + } + + /** + * 重写这个方法是为了支持Receiver,否则会出现ClassCast错误 + */ + @Override + public Context getBaseContext() { + return PluginLoader.fixBaseContextForReceiver(super.getBaseContext()); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/FairyGlobal.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/FairyGlobal.java new file mode 100644 index 00000000..34b5fe4d --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/FairyGlobal.java @@ -0,0 +1,183 @@ +package com.limpoxe.fairy.core; + +import android.app.Application; + +import com.limpoxe.fairy.core.exception.PluginResInitError; +import com.limpoxe.fairy.manager.mapping.StubMappingProcessor; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.util.ArrayList; + +public class FairyGlobal { + private static boolean sIsInited; + private static Application sApplication; + private static boolean sIsLocalHtmlEnable = false; + private static int sLoadingResId; + private static long sMinLoadingTime = 400; + private static boolean sIsNeedVerifyPluginSign = true; + private static boolean sSupportRemoteViews = true; + private static ArrayList mappingProcessors = new ArrayList(); + private static boolean sFakePluginProcessName = true; + private static boolean sNeedVerifyHostVersionName = true; + private static String sNotificationResPath; + private static boolean sInstallationWithSameVersion = false; + + public static Application getHostApplication() { + if (sApplication == null) { + throw new IllegalStateException("not inited yet"); + } + return sApplication; + } + + /*package*/ static void setApplication(Application application) { + sApplication = application; + //public_static_final_host_string_这个资源是由框架脚本植入宿主的, 由此判断对宿主的id分组是否成功 + int id = sApplication.getResources().getIdentifier("public_static_final_host_string_", "string", sApplication.getPackageName()); + if (id != 0 && !ResourceUtil.isMainResId(id)) { + throw new PluginResInitError("对宿主id分组失败,说明宿主编译有错,请clean后重新编译: " + ResourceUtil.covent2Hex(String.valueOf(id))); + } + } + + /*package*/ static void setIsInited(boolean isInited) { + sIsInited = isInited; + } + + public static boolean isInited() { + return sIsInited; + } + + /** + * 插件中是否支持使用本地html文件 + * @param isLocalHtmlEnable + */ + public static void setLocalHtmlenable(boolean isLocalHtmlEnable) { + sIsLocalHtmlEnable = isLocalHtmlEnable; + } + + public static boolean isLocalHtmlEnable() { + return sIsLocalHtmlEnable; + } + + /** + * 控制框架日志是否打印 + * @param isLogEnable + */ + public static void setLogEnable(boolean isLogEnable) { + LogUtil.setEnable(isLogEnable); + } + + /** + * 首次打开插件时,如果是通过Activity打开,会显示一个空白loading页, + * 通过resId设置loading页ui + * @param resId + */ + public static void setLoadingResId(int resId) { + sLoadingResId = resId; + } + + public static int getLoadingResId() { + return sLoadingResId; + } + + /** + * 设置loading页最小等待时间,用于在插件较简单,初始化较快时,避免loading页一闪而过 + * 时间设置为0表示无loading页, 默认400ms + * @param minLoadingTime + */ + public static void setMinLoadingTime(long minLoadingTime) { + sMinLoadingTime = minLoadingTime; + } + + public static long getMinLoadingTime() { + return sMinLoadingTime; + } + + /** + * 是否需要验证"插件和宿主的签名相同" + * @param needVerify + */ + public static void setNeedVerifyPlugin(boolean needVerify) { + sIsNeedVerifyPluginSign = needVerify; + } + + public static boolean isNeedVerifyPlugin() { + return sIsNeedVerifyPluginSign; + } + + public static boolean isNeedVerifyHostVersionName() { + return sNeedVerifyHostVersionName; + } + + public static void setNeedVerifyHostVersionName(boolean needVerify) { + sNeedVerifyHostVersionName = needVerify; + } + + public static String getNotificationResPath() { + if (sNotificationResPath == null) { + return getHostApplication().getExternalCacheDir().getAbsolutePath() + "/notification_res.apk"; + } + return sNotificationResPath; + } + + public static void setNotificationResPath(String notificationResPath) { + sNotificationResPath = notificationResPath; + } + + /** + * 如果两个processor可以处理同一个映射关系,则后添加processor生效,先添加的processor会被忽略 + * @param processor + */ + public static void registStubMappingProcessor(StubMappingProcessor processor) { + if (processor == null) { + return; + } + if (mappingProcessors == null) { + mappingProcessors = new ArrayList(); + } + if (!mappingProcessors.contains(processor)) { + mappingProcessors.add(processor); + } + } + + public static ArrayList getStubMappingProcessors() { + return mappingProcessors; + } + + /** + * 是否需要支持插件中发送notification是使用remoteviews并携带插件资源 + * @return + */ + public static boolean isSupportRemoteViews() { + return sSupportRemoteViews; + } + + public static void setSupportRemoteViews(boolean support){ + sSupportRemoteViews = support; + } + + public static boolean isFakePluginProcessName() { + return sFakePluginProcessName; + } + + /** + * 是否需要伪造插件进程名称,使得在插件中通过getRunningProcesses来判断当前进程时,返回的是宿主主进程而不是插件进程 + * @param fake + */ + public static void setFakePluginProcessName(boolean fake) { + sFakePluginProcessName = fake; + } + + /** + * 安装插件的时候,如果已经安装的版本,和将要安装的版本相同时,是否允许安装 + * 默认不允许 + * @return + */ + public static boolean isInstallationWithSameVersion() { + return sInstallationWithSameVersion; + } + + public static void setInstallationWithSameVersion(boolean withSameVersion) { + sInstallationWithSameVersion = withSameVersion; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/HostClassLoader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/HostClassLoader.java new file mode 100644 index 00000000..c759557f --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/HostClassLoader.java @@ -0,0 +1,17 @@ +package com.limpoxe.fairy.core; + +import dalvik.system.PathClassLoader; + +/** + * 为了支持Receiver和Service,增加此类。 + * + * @author Administrator + * + */ +public class HostClassLoader extends PathClassLoader { + + public HostClassLoader(String dexPath, ClassLoader parent) { + super(dexPath, parent); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginAppTrace.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginAppTrace.java new file mode 100644 index 00000000..172e6149 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginAppTrace.java @@ -0,0 +1,312 @@ +package com.limpoxe.fairy.core; + +import android.app.Service; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.android.HackActivityThread; +import com.limpoxe.fairy.core.android.HackContextImpl; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProviderClient; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.util.ArrayList; +import java.util.Map; + +/** + * 用于拦截Activity Service Receiver的生命周期函数 + * 同时可以跟踪一些其他消息 + * @author cailiming + * + */ +public class PluginAppTrace implements Handler.Callback { + + private final Handler mHandler; + + public PluginAppTrace(Handler handler) { + mHandler = handler; + } + + @Override + public boolean handleMessage(Message msg) { + + LogUtil.v(">>> handling: ", CodeConst.codeToString(msg.what)); + + Result result = beforeHandle(msg); + + try { + + mHandler.handleMessage(msg); + + LogUtil.v(">>> done: " + CodeConst.codeToString(msg.what)); + + } finally { + + afterHandle(msg, result); + + } + + return true; + } + + private Result beforeHandle(Message msg) { + + switch (msg.what) { + + case CodeConst.LAUNCH_ACTIVITY: + case CodeConst.RELAUNCH_ACTIVITY: + + beforeLaunchActivityFor360Safe(); + + return null; + + case CodeConst.RECEIVER: + + return beforeReceiver(msg); + + case CodeConst.CREATE_SERVICE: + + return beforeCreateService(msg); + + case CodeConst.STOP_SERVICE: + + return beforeStopService(msg); + } + return null; + } + + private static void beforeLaunchActivityFor360Safe() { + // 检查mInstrumention是否已经替换成功。 + // 之所以要检查,是因为如果手机上安装了360手机卫士等app,它们可能会劫持用户app的ActivityThread对象, + // 导致在PluginApplication的onCreate方法里面替换mInstrumention可能会失败 + // 所以这里再做一次检查 + PluginInjector.injectInstrumentation(); + } + + private static Result beforeReceiver(Message msg) { + if (ProcessUtil.isPluginProcess()) {//判断进程是为了提高效率, 因为插件组件都是在插件进程中运行的. + + Context newBase = PluginIntentResolver.resolveReceiverForClassLoader(msg.obj); + //找到class说明是插件中定义的receiver + if (newBase != null) { + + Context baseContext = FairyGlobal.getHostApplication().getBaseContext(); + + PluginInjector.replaceReceiverContext(baseContext, newBase); + + Result result = new Result(); + result.baseContext = baseContext; + + return result; + } else { + //宿主的Receiver的context不需要做特别处理,因为在framework中Receiver的context本身是对appliction的包装。 + //而宿主的application的baseContext已经在插件框架init的时候替换过了 + } + } + + return null; + } + + private static Result beforeCreateService(Message msg) { + Result result = new Result(); + result.serviceName = PluginIntentResolver.resolveServiceForClassLoader(msg.obj); + return result; + } + + private static Result beforeStopService(Message msg) { + if (ProcessUtil.isPluginProcess()) { + //销毁service时回收映射关系, 之所以要回收映射关系是为了能在宿主中尽量少的注册占位组件. + //即回收映射关系并不是必须的, 只要预注册的占位组件数据足够即可. + if (HackActivityThread.get() != null) { + Map services = HackActivityThread.get().getServices(); + if (services != null) { + Service service = services.get(msg.obj); + if (service != null) { + String pluginServiceClassName = service.getClass().getName(); + LogUtil.v("unBindStubService", pluginServiceClassName); + PluginManagerProviderClient.unBindStubService(pluginServiceClassName); + } + } + } + } + + return null; + } + + private static void afterHandle(Message msg, Result result) { + switch (msg.what) { + case CodeConst.RECEIVER: + + afterReceiver(result); + + break; + + case CodeConst.CREATE_SERVICE: + + afterCreateService(result); + + break; + + case CodeConst.CONFIGURATION_CHANGED: + + afterConfigurationChanged(msg); + + break; + } + } + + private static void afterReceiver(Result result) { + if (ProcessUtil.isPluginProcess()) { + if (result != null && result.baseContext != null) { + new HackContextImpl(result.baseContext).setReceiverRestrictedContext(null); + } + } + } + + private static void afterCreateService(Result result) { + //这里不做进程判断,是因为如果是宿主进程, 也需要为宿主service换context + if (result.serviceName != null && result.serviceName.startsWith(PluginIntentResolver.CLASS_PREFIX_SERVICE)) { + //替换service的context + //在引入了PluginShadowService以后,这个已经是多余的了, 注释掉先. + //PluginInjector.replacePluginServiceContext(result.serviceName.replace(PluginIntentResolver.CLASS_PREFIX_SERVICE, "")); + } else { + //给宿主service注入一个无害的BaseContext, 主要是为了重写宿主Service的sentBroadCast和startService方法 + //使得在宿主的service中通过intent可以打开插件的组件 + PluginInjector.replaceHostServiceContext(result.serviceName); + } + } + + private static void afterConfigurationChanged(Message msg) { + if (ProcessUtil.isPluginProcess()) { + ArrayList pluginDescriptors = PluginManagerHelper.getPlugins(); + for(PluginDescriptor pluginDescriptor : pluginDescriptors) { + LoadedPlugin loadedPlugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + if (loadedPlugin != null) { + //更新环境配置,如屏幕密度,系统语言,横竖屏等 + //TODO updateConfiguration这个方法已经过期,后续需要更改为通过反射调用它的隐藏方法 + LogUtil.v("updateConfiguration for ", pluginDescriptor.getPackageName()); + loadedPlugin.pluginResource.updateConfiguration((Configuration)msg.obj, null); + } + } + } + } + + static class Result { + String serviceName; + Context baseContext; + } + + private static class CodeConst { + public static final int LAUNCH_ACTIVITY = 100; + public static final int PAUSE_ACTIVITY = 101; + public static final int PAUSE_ACTIVITY_FINISHING= 102; + public static final int STOP_ACTIVITY_SHOW = 103; + public static final int STOP_ACTIVITY_HIDE = 104; + public static final int SHOW_WINDOW = 105; + public static final int HIDE_WINDOW = 106; + public static final int RESUME_ACTIVITY = 107; + public static final int SEND_RESULT = 108; + public static final int DESTROY_ACTIVITY = 109; + public static final int BIND_APPLICATION = 110; + public static final int EXIT_APPLICATION = 111; + public static final int NEW_INTENT = 112; + public static final int RECEIVER = 113; + public static final int CREATE_SERVICE = 114; + public static final int SERVICE_ARGS = 115; + public static final int STOP_SERVICE = 116; + public static final int CONFIGURATION_CHANGED = 118; + public static final int CLEAN_UP_CONTEXT = 119; + public static final int GC_WHEN_IDLE = 120; + public static final int BIND_SERVICE = 121; + public static final int UNBIND_SERVICE = 122; + public static final int DUMP_SERVICE = 123; + public static final int LOW_MEMORY = 124; + public static final int ACTIVITY_CONFIGURATION_CHANGED = 125; + + public static final int PROFILER_CONTROL = 127; + public static final int CREATE_BACKUP_AGENT = 128; + public static final int DESTROY_BACKUP_AGENT = 129; + public static final int SUICIDE = 130; + public static final int REMOVE_PROVIDER = 131; + public static final int ENABLE_JIT = 132; + public static final int DISPATCH_PACKAGE_BROADCAST = 133; + public static final int SCHEDULE_CRASH = 134; + public static final int DUMP_HEAP = 135; + public static final int DUMP_ACTIVITY = 136; + public static final int SLEEPING = 137; + public static final int SET_CORE_SETTINGS = 138; + public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139; + public static final int TRIM_MEMORY = 140; + public static final int DUMP_PROVIDER = 141; + public static final int UNSTABLE_PROVIDER_DIED = 142; + public static final int REQUEST_ASSIST_CONTEXT_EXTRAS = 143; + public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144; + public static final int INSTALL_PROVIDER = 145; + public static final int ON_NEW_ACTIVITY_OPTIONS = 146; + public static final int ENTER_ANIMATION_COMPLETE = 149; + public static final int START_BINDER_TRACKING = 150; + public static final int STOP_BINDER_TRACKING_AND_DUMP = 151; + public static final int MULTI_WINDOW_MODE_CHANGED = 152; + public static final int PICTURE_IN_PICTURE_MODE_CHANGED = 153; + public static final int LOCAL_VOICE_INTERACTION_STARTED = 154; + public static final int ATTACH_AGENT = 155; + public static final int APPLICATION_INFO_CHANGED = 156; + public static final int ACTIVITY_MOVED_TO_DISPLAY = 157; + public static final int RUN_ISOLATED_ENTRY_POINT = 158; + public static final int EXECUTE_TRANSACTION = 159; + public static final int RELAUNCH_ACTIVITY = 160; + + + public static String codeToString(int code) { + switch (code) { + case BIND_APPLICATION: return "BIND_APPLICATION"; + case EXIT_APPLICATION: return "EXIT_APPLICATION"; + case RECEIVER: return "RECEIVER"; + case CREATE_SERVICE: return "CREATE_SERVICE"; + case SERVICE_ARGS: return "SERVICE_ARGS"; + case STOP_SERVICE: return "STOP_SERVICE"; + case CONFIGURATION_CHANGED: return "CONFIGURATION_CHANGED"; + case CLEAN_UP_CONTEXT: return "CLEAN_UP_CONTEXT"; + case GC_WHEN_IDLE: return "GC_WHEN_IDLE"; + case BIND_SERVICE: return "BIND_SERVICE"; + case UNBIND_SERVICE: return "UNBIND_SERVICE"; + case DUMP_SERVICE: return "DUMP_SERVICE"; + case LOW_MEMORY: return "LOW_MEMORY"; + case PROFILER_CONTROL: return "PROFILER_CONTROL"; + case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT"; + case DESTROY_BACKUP_AGENT: return "DESTROY_BACKUP_AGENT"; + case SUICIDE: return "SUICIDE"; + case REMOVE_PROVIDER: return "REMOVE_PROVIDER"; + case ENABLE_JIT: return "ENABLE_JIT"; + case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST"; + case SCHEDULE_CRASH: return "SCHEDULE_CRASH"; + case DUMP_HEAP: return "DUMP_HEAP"; + case DUMP_ACTIVITY: return "DUMP_ACTIVITY"; + case SLEEPING: return "SLEEPING"; + case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS"; + case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO"; + case DUMP_PROVIDER: return "DUMP_PROVIDER"; + case UNSTABLE_PROVIDER_DIED: return "UNSTABLE_PROVIDER_DIED"; + case REQUEST_ASSIST_CONTEXT_EXTRAS: return "REQUEST_ASSIST_CONTEXT_EXTRAS"; + case TRANSLUCENT_CONVERSION_COMPLETE: return "TRANSLUCENT_CONVERSION_COMPLETE"; + case INSTALL_PROVIDER: return "INSTALL_PROVIDER"; + case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS"; + case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE"; + case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED"; + case ATTACH_AGENT: return "ATTACH_AGENT"; + case APPLICATION_INFO_CHANGED: return "APPLICATION_INFO_CHANGED"; + case RUN_ISOLATED_ENTRY_POINT: return "RUN_ISOLATED_ENTRY_POINT"; + case EXECUTE_TRANSACTION: return "EXECUTE_TRANSACTION"; + case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY"; + } + return "(unknown: " + code +")"; + } + } + +} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/PluginBaseContextWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginBaseContextWrapper.java similarity index 72% rename from PluginCore/src/com/plugin/core/PluginBaseContextWrapper.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginBaseContextWrapper.java index f0d361ff..f1c99a8e 100644 --- a/PluginCore/src/com/plugin/core/PluginBaseContextWrapper.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginBaseContextWrapper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.plugin.core; +package com.limpoxe.fairy.core; import android.annotation.TargetApi; import android.content.BroadcastReceiver; @@ -24,16 +24,24 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; +import android.view.Display; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; import java.util.ArrayList; +/** + * 这里不必重写StartActivity的相关方法,是因为所有从Context.startActivity发起的调用, + * 最后都会调用mMainThread.getInstrumentation().execStartActivity来执行 + * 而框架替换了Instrumentation,因此所有的StartActivity相关方法都会在PluginInstrumentation中做处理 + */ public class PluginBaseContextWrapper extends ContextWrapper { public PluginBaseContextWrapper(Context base) { @@ -42,7 +50,7 @@ public PluginBaseContextWrapper(Context base) { @Override public void sendBroadcast(Intent intent) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendBroadcast(item); @@ -51,7 +59,7 @@ public void sendBroadcast(Intent intent) { @Override public void sendBroadcast(Intent intent, String receiverPermission) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendBroadcast(item, receiverPermission); @@ -60,7 +68,7 @@ public void sendBroadcast(Intent intent, String receiverPermission) { @Override public void sendOrderedBroadcast(Intent intent, String receiverPermission) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendOrderedBroadcast(item, receiverPermission); @@ -70,7 +78,7 @@ public void sendOrderedBroadcast(Intent intent, String receiverPermission) { @Override public void sendOrderedBroadcast(Intent intent, String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendOrderedBroadcast(item, receiverPermission, resultReceiver, @@ -81,7 +89,7 @@ public void sendOrderedBroadcast(Intent intent, String receiverPermission, Broad @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public void sendBroadcastAsUser(Intent intent, UserHandle user) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendBroadcastAsUser(item, user); @@ -91,7 +99,7 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user) { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendBroadcastAsUser(item, user, receiverPermission); @@ -103,7 +111,7 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverP public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendOrderedBroadcastAsUser(item, user, receiverPermission, resultReceiver, scheduler, initialCode, @@ -113,7 +121,7 @@ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String re @Override public void sendStickyBroadcast(Intent intent) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendStickyBroadcast(item); @@ -123,7 +131,7 @@ public void sendStickyBroadcast(Intent intent) { @Override public void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendStickyOrderedBroadcast(item, resultReceiver, scheduler, initialCode, initialData, initialExtras); @@ -133,7 +141,7 @@ public void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultRe @Override public void removeStickyBroadcast(Intent intent) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.removeStickyBroadcast(item); @@ -143,7 +151,7 @@ public void removeStickyBroadcast(Intent intent) { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendStickyBroadcastAsUser(item, user); @@ -154,7 +162,7 @@ public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) { @Override public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { - LogUtil.d(intent); + LogUtil.v(intent); ArrayList list = PluginIntentResolver.resolveReceiver(intent); for (Intent item:list) { super.sendStickyOrderedBroadcastAsUser(item, user, resultReceiver, scheduler, initialCode, initialData, @@ -164,30 +172,59 @@ public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, Bro @Override public ComponentName startService(Intent service) { - LogUtil.d(service); + LogUtil.v(service); PluginIntentResolver.resolveService(service); return super.startService(service); } @Override public boolean stopService(Intent name) { - LogUtil.d(name); + LogUtil.v(name); PluginIntentResolver.resolveService(name); return super.stopService(name); } @Override public boolean bindService(Intent service, ServiceConnection conn, int flags) { - LogUtil.d(service); + LogUtil.v(service); PluginIntentResolver.resolveService(service); return super.bindService(service, conn, flags); } @Override public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { - if (PluginManagerHelper.getPluginDescriptorByPluginId(packageName) != null) { - return PluginLoader.getNewPluginApplicationContext(packageName); - } + //这个方法有2个作用 + // 1、context返回插件宿主packageName时,安装插件中的contentprovider时会用到它, + // 被android.app.ActiviThread这个类调用。 + // 2、可以方便的创建一个插件ApplicationContext副本。用于满足一些特定的业务需要 + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + if (pluginDescriptor != null) { + if (getPackageName().equals(packageName)) { + LogUtil.v(packageName, "创建插件Context"); + Context context = PluginCreator.getNewPluginApplicationContext(pluginDescriptor, false); + if (context != null) { + return context; + } else { + return this; + } + } else { + LogUtil.v(packageName, "创建插件Context"); + return PluginCreator.getNewPluginApplicationContext(pluginDescriptor, true); + } + } + LogUtil.v(packageName, "创建正常Context"); return super.createPackageContext(packageName, flags); } + + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + LogUtil.v("BaseContext", "可能需要重写此方法,用于适配AndroidX1.1.+"); + return super.createConfigurationContext(overrideConfiguration); + } + + @Override + public Context createDisplayContext(Display display) { + LogUtil.v("BaseContext", "可能需要重写此方法"); + return super.createDisplayContext(display); + } } diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginClassLoader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginClassLoader.java new file mode 100644 index 00000000..107ce716 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginClassLoader.java @@ -0,0 +1,29 @@ +package com.limpoxe.fairy.core; + +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import dalvik.system.PathClassLoader; + +/** + * 插件间依赖以及so管理 + * + * @author Administrator + * + */ +public class PluginClassLoader extends PathClassLoader { + + public final String pluginPackageName; + + public PluginClassLoader(String pluginPackageName, String dexPath, ClassLoader parent) { + super(dexPath, parent); + this.pluginPackageName = pluginPackageName; + } + + @Override + public String findLibrary(String name) { + String libPath = (String) RefInvoker.invokeMethod(getParent(), getParent().getClass(), "findLibrary", new Class[]{String.class}, new Object[]{name}); + LogUtil.d("findLibrary", name, libPath); + return libPath; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginContextTheme.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginContextTheme.java new file mode 100644 index 00000000..3d134228 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginContextTheme.java @@ -0,0 +1,549 @@ +package com.limpoxe.fairy.core; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.DatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; +import android.preference.PreferenceManager; +import android.util.ArrayMap; +import android.view.Display; +import android.view.LayoutInflater; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.android.HackContextImpl; +import com.limpoxe.fairy.core.android.HackResources; +import com.limpoxe.fairy.core.compat.CompatForSharedPreferencesImpl; +import com.limpoxe.fairy.core.localservice.LocalServiceManager; +import com.limpoxe.fairy.core.multidex.PluginMultiDexHelper; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * 注意:意外覆写父类方法可能会抛出LingageError + * 也就是说如果要在这个类里添加非override的public方法的话要小心了。 + */ +public class PluginContextTheme extends PluginBaseContextWrapper { + private int mThemeResource; + Resources.Theme mTheme; + private LayoutInflater mInflater; + private ApplicationInfo mApplicationInfo; + final Resources mResources; + private final ClassLoader mClassLoader; + private Application mPluginApplication; + protected final PluginDescriptor mPluginDescriptor; + + private ArrayList receivers = new ArrayList(); + + //用于插件安装multidex + private boolean crackPackageManager = false; + //用于不能修改包名的特殊插件Activity,如一些三方sdk + private boolean useHostPackageName = false; + //当前插件Context依附的外部Context,在插件Fragment或者View被潜入到宿主的Activity中时比较有用 + private Context outerContext = null; + + public PluginContextTheme(PluginDescriptor pluginDescriptor, + Context base, Resources resources, + ClassLoader classLoader) { + super(base); + mPluginDescriptor = pluginDescriptor; + mResources = resources; + mClassLoader = classLoader; + + if (!ProcessUtil.isPluginProcess()) { + throw new IllegalAccessError("本类仅在插件进程使用"); + } + } + + public void setPluginApplication(Application pluginApplication) { + this.mPluginApplication = pluginApplication; + } + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + } + + @Override + public ClassLoader getClassLoader() { + return mClassLoader; + } + + @Override + public AssetManager getAssets() { + return mResources.getAssets(); + } + + @Override + public Resources getResources() { + return mResources; + } + + /** + * 传0表示使用系统默认主题,最终的现实样式和客户端程序的minSdk应该有关系。 + * 即系统针对不同的minSdk设置了不同的默认主题样式 + * 传非0的话表示传过来什么主题就显示什么主题 + */ + @Override + public void setTheme(int resid) { + mThemeResource = resid; + initializeTheme(); + } + + @Override + public Resources.Theme getTheme() { + if (mTheme != null) { + return mTheme; + } + + Integer result = HackResources.selectDefaultTheme(mThemeResource, getBaseContext().getApplicationInfo().targetSdkVersion); + if (result != null) { + mThemeResource = result; + } + + initializeTheme(); + + return mTheme; + } + + @Override + public Object getSystemService(String name) { + if (LAYOUT_INFLATER_SERVICE.equals(name)) { + if (mInflater == null) { + mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); + } + return mInflater; + } + + Object service = getBaseContext().getSystemService(name); + + if (service == null) { + service = LocalServiceManager.getService(name); + } + + return service; + } + + + private void initializeTheme() { + final boolean first = mTheme == null; + if (first) { + mTheme = getResources().newTheme(); + Resources.Theme theme = getBaseContext().getTheme(); + if (theme != null) { + mTheme.setTo(theme); + } + } + mTheme.applyStyle(mThemeResource, true); + } + + @Override + public String getPackageName() { + + if (useHostPackageName) { + return FairyGlobal.getHostApplication().getPackageName(); + } + + if (mPluginDescriptor.isUseHostPackageName()) { + return FairyGlobal.getHostApplication().getPackageName(); + } + + //packagemanager、activitymanager、wifi、window、inputservice + //等等系统服务会获取packageName去查询信息,如果获取到插件的packageName则会crash + //而这里返回的正是插件本身的packageName, 因此需要通过安装AndroidOsServiceManager这个hook去修正, + //如果不安装AndroidOsServiceManager或者安装失败,这里应当返回宿主的packageName + return mPluginDescriptor.getPackageName(); + + } + + //@hide tabactivity会用到 + public String getBasePackageName() { + //ViewRootImpl中会调用这个方法, 这是个hide方法. + return FairyGlobal.getHostApplication().getPackageName(); + } + + ////@hide toast,ITelephony等服务会用到 + public String getOpPackageName() { + return FairyGlobal.getHostApplication().getPackageName(); + } + + @Override + public Context getApplicationContext() { + return mPluginApplication; + } + + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + //todo 这里将参数overrideConfiguration扔掉了,可能会导致夜间模式失效。要支持夜间模式需要把这个参数用起来 + return PluginCreator.createNewPluginComponentContext(this, getBaseContext(), 0); + } + + @Override + public Context createDisplayContext(Display display) { + //todo 这个函数不重写可能无法支持副屏 + return super.createDisplayContext(display); + } + + @Override + public ApplicationInfo getApplicationInfo() { + //这里的ApplicationInfo是从LoadedApk中取出来的 + //由于目前插件之间是共用1个插件进程。LoadedApk只有1个,而ApplicationInfo每个插件都有一个, + // 所以不能通过直接修改loadedApk中的内容来修正这个方法的返回值,而是将修正的过程放在Context中去做, + //避免多个插件之间造成干扰 + if (mApplicationInfo == null) { + try { + mApplicationInfo = getPackageManager().getApplicationInfo(mPluginDescriptor.getPackageName(), 0); + //这里修正packageManager中hook时设置的插件packageName + mApplicationInfo.packageName = getPackageName(); + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("PluginContextTheme.getApplicationInfo", e); + } + } + return mApplicationInfo; + } + + @Override + public String getPackageCodePath() { + return mPluginDescriptor.getInstalledPath(); + } + + @Override + public String getPackageResourcePath() { + return mPluginDescriptor.getInstalledPath(); + } + + public PluginDescriptor getPluginDescriptor() { + return mPluginDescriptor; + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + //某些情况下switchWebViewContext会触发chrome调用NetworkChangeNotifierAutoDetect$WifiManagerDelegate.getWifiSSID + //第一个参数传了null + if (receiver != null) { + receivers.add(receiver); + } + return super.registerReceiver(receiver, filter); + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + if (receivers.contains(receiver)) { + super.unregisterReceiver(receiver); + receivers.remove(receiver); + } + } + + public void unregisterAllReceiver() { + for (BroadcastReceiver br: + receivers) { + if (br != null) { + LogUtil.e("unregisterReceiver", br.getClass().getName()); + super.unregisterReceiver(br); + } + } + receivers.clear(); + } + + @Override + public PackageManager getPackageManager() { + if (crackPackageManager) { + //欺骗MultDexInstaller, 使MultDexInstaller能得到正确的插件信息 + return PluginMultiDexHelper.fixPackageManagerForMultDexInstaller(mPluginDescriptor.getPackageName(), super.getPackageManager()); + } + return super.getPackageManager(); + } + + public void setCrackPackageManager(boolean crackPackageManager) { + this.crackPackageManager = crackPackageManager; + } + + public void setUseHostPackageName(boolean useHostPackageName) { + this.useHostPackageName = useHostPackageName; + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * 隔离插件间的SharedPreferences + * @param name + * @param mode + * @return + */ + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + + if (Build.VERSION.SDK_INT > 23) { + synchronized (PluginContextTheme.class) { + HackContextImpl impl = new HackContextImpl(getContextImpl()); + + ArrayMap mSharedPrefsPaths = impl.getSharedPrefsPaths(); + String parent = new File(getDataDir(), "shared_prefs").getAbsolutePath(); + if (mSharedPrefsPaths != null) { + File file = mSharedPrefsPaths.get(name); + if (file != null && !file.getParent().equals(parent)) { + mSharedPrefsPaths.remove(name);//置空之后再get会触发重建,则getDataDir有机会生效 + } + } + + File mPreferencesDir = impl.getPreferencesDir(); + if (mPreferencesDir == null || !mPreferencesDir.getAbsolutePath().equals(parent)) { + impl.setPreferencesDir(new File(getDataDir(), "shared_prefs")); + } + } + + return super.getSharedPreferences(name, mode); + } + + //这里之所以需要追加前缀是因为ContextImpl类中的全局静态缓存sSharedPrefs + if (!name.startsWith(mPluginDescriptor.getPackageName() + "_")) { + name = mPluginDescriptor.getPackageName() + "_" + name; + } + + //4.4以上版本缓存是延迟初始化的,这里增加这句调用是为了确保已经初始化,防止反射为空 + PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + + Object cache = HackContextImpl.getSharedPrefs(); + if (Build.VERSION.SDK_INT >= 19 && cache instanceof ArrayMap) { + synchronized (PluginContextTheme.class) { + ArrayMap> sSharedPrefs = (ArrayMap>)cache; + final String packageName = getPackageName(); + ArrayMap packagePrefs = sSharedPrefs.get(packageName); + if (packagePrefs == null) { + packagePrefs = new ArrayMap(); + sSharedPrefs.put(packageName, packagePrefs); + } + + Object sp = packagePrefs.get(name); + if (sp == null) { + packagePrefs.put(name, CompatForSharedPreferencesImpl.newSharedPreferencesImpl(getSharedPrefsFile(name), mode, getPackageName())); + } + } + } else if (cache instanceof HashMap) { + HashMap sSharedPrefs = (HashMap)cache; + Object sp = sSharedPrefs.get(name); + if (sp == null) { + sSharedPrefs.put(name, CompatForSharedPreferencesImpl.newSharedPreferencesImpl(getSharedPrefsFile(name), mode, getPackageName())); + } + } + + return super.getSharedPreferences(name, mode); + } + + //android-M + //removed + public File getSharedPreferencesPath(String name) { + return getSharedPrefsFile(name); + } + + private File getSharedPrefsFile(String name) { + if (!name.startsWith(mPluginDescriptor.getPackageName() + "_")) { + name = mPluginDescriptor.getPackageName() + "_" + name; + } + return makeFilename(new File(getDataDir(), "shared_prefs"), name + ".xml"); + } + + @Override + public File getDir(String name, int mode) { + File dir = makeFilename(getDataDir(), "app_" + name); + if (!dir.exists()) { + dir.mkdirs(); + //setpermisssion + } + return dir; + } + + @Override + public File getFilesDir() { + File dir = new File(getDataDir(), "files"); + if (!dir.exists()) { + dir.mkdirs(); + //setpermisssion + } + return dir; + } + + @Override + public File getFileStreamPath(String name) { + return makeFilename(getFilesDir(), name); + } + + @Override + public FileOutputStream openFileOutput(String name, int mode) throws FileNotFoundException { + final boolean append = (mode&MODE_APPEND) != 0; + File f = makeFilename(getFilesDir(), name); + try { + FileOutputStream fos = new FileOutputStream(f, append); + //setFilePermissionsFromMode(f.getPath(), mode, 0); + return fos; + } catch (FileNotFoundException e) { + } + return super.openFileOutput(name, mode); + } + + @Override + public FileInputStream openFileInput(String name) throws FileNotFoundException { + File f = makeFilename(getFilesDir(), name); + return new FileInputStream(f); + } + + @Override + public File getNoBackupFilesDir() { + File dir = new File(getDataDir(), "no_backup"); + if (!dir.exists()) { + dir.mkdirs(); + //setpermisssion + } + return dir; + } + + @Override + public File getCacheDir() { + File dir = new File(getDataDir(), "cache"); + if (!dir.exists()) { + dir.mkdirs(); + //setpermisssion + } + return dir; + } + + @Override + public File getCodeCacheDir() { + File dir = new File(getDataDir(), "code_cache"); + if (!dir.exists()) { + dir.mkdirs(); + //setpermisssion + } + return dir; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) { + name = getAbsuloteDatabasePath(name); + return super.openOrCreateDatabase(name, mode, factory); + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, + DatabaseErrorHandler errorHandler) { + name = getAbsuloteDatabasePath(name); + return super.openOrCreateDatabase(name, mode, factory, errorHandler); + } + + @Override + public boolean deleteDatabase(String name) { + name = getAbsuloteDatabasePath(name); + return super.deleteDatabase(name); + } + + @Override + public File getDatabasePath(String name) { + name = getAbsuloteDatabasePath(name); + return super.getDatabasePath(name); + } + + @Override + public String[] databaseList() { + File f = new File(getDataDir(), "databases"); + final String[] list = f.list(); + return (list != null) ? list : EMPTY_STRING_ARRAY; + } + + @Override + public String[] fileList() { + final String[] list = getFilesDir().list(); + return (list != null) ? list : EMPTY_STRING_ARRAY; + } + + @Override + public boolean deleteFile(String name) { + File f = makeFilename(getFilesDir(), name); + return f.delete(); + } + + private String getAbsuloteDatabasePath(String name) { + if (name.charAt(0) != File.separatorChar) { + File f = makeFilename(new File(getDataDir(), "databases"), name); + name = f.getAbsolutePath(); + } + return name; + } + + private File makeFilename(File base, String name) { + if (name.indexOf(File.separatorChar) < 0) { + if (!base.exists()) { + base.mkdirs(); + //setpermisssion + } + return new File(base, name); + } + throw new IllegalArgumentException( + "File " + name + " contains a path separator"); + } + + private static final String[] EMPTY_STRING_ARRAY = {}; + + private File dataDir; + + //android-N + public File getDataDir() { + if (dataDir == null) { + dataDir = new File(mPluginDescriptor.getDataDir()); + if (!dataDir.exists()) { + dataDir.mkdirs(); + //setpermisssion + } + } + return dataDir; + } + + public Context getOuter() { + if (outerContext != null) { + return outerContext; + } + Context base = getBaseContext(); + while(base instanceof ContextWrapper) { + base = ((ContextWrapper)base).getBaseContext(); + } + if (HackContextImpl.instanceOf(base)) { + base = new HackContextImpl(base).getOuterContext(); + } + return base; + } + + public void setOuter(Context outerContext) { + this.outerContext = outerContext; + } + + private Object getContextImpl() { + int dep = 0;//这个dep限制是以防万一陷入死循环 + Context base = getBaseContext(); + while(base instanceof ContextWrapper && dep < 10) { + base = ((ContextWrapper)base).getBaseContext(); + dep++; + } + if (HackContextImpl.instanceOf(base)) { + return base; + } + return null; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginCreator.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginCreator.java new file mode 100644 index 00000000..3cfa03dc --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginCreator.java @@ -0,0 +1,253 @@ +package com.limpoxe.fairy.core; + +import android.app.Application; +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.android.HackAssetManager; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.File; +import java.util.List; + +public class PluginCreator { + + private PluginCreator() { + } + + /** + * 根据插件apk文件,创建插件dex的classloader + * + * @param absolutePluginApkPath + * 插件apk文件路径 + * @return + */ + public static ClassLoader createPluginClassLoader(String pluginPackageName, + String absolutePluginApkPath, + String absolutePluginDalvikCachePath, + String absolutePluginNativeLibPath, + boolean isStandalone, + String[] dependences, + List pluginApkMultDexPath) { + + File optDir = new File(absolutePluginDalvikCachePath); + optDir.mkdirs(); + + File libDir = new File(absolutePluginNativeLibPath); + libDir.mkdirs(); + + LogUtil.v(absolutePluginApkPath, optDir.getAbsolutePath(), libDir.getAbsolutePath()); + + return new PluginClassLoader(pluginPackageName, "", new RealPluginClassLoader( + pluginPackageName, + absolutePluginApkPath, + dependences,// 插件依赖的插件, 通常情况独立插件无子依赖, 此处参数size一般是0,但实际也可以依赖其他基础独立插件包, + // 也即独立插件之间也可以建立依赖关系,前提和非独立插件一样,被依赖的插件不可以包含资源 + optDir.getAbsolutePath(), + libDir.getAbsolutePath(), + isStandalone ? pluginApkMultDexPath : null, + isStandalone)); + } + + /** + * 根据插件apk文件,创建插件资源文件,同时绑定宿主程序的资源,这样就可以在插件中使用宿主程序的资源。 + * + * @return + */ + public static Resources createPluginResource(String mainApkPath, Resources mainRes, PluginDescriptor pluginDescriptor) { + if (!pluginDescriptor.isBroken()) { + String absolutePluginApkPath = pluginDescriptor.getInstalledPath(); + boolean isStandalone = pluginDescriptor.isStandalone(); + String[] dependencies = pluginDescriptor.getDependencies(); + + try { + String[] assetPaths = buildAssetPath(isStandalone, mainApkPath, + absolutePluginApkPath, dependencies); + AssetManager assetMgr = AssetManager.class.newInstance(); + HackAssetManager hackAssetManager = new HackAssetManager(assetMgr); + for(String path : assetPaths) { + hackAssetManager.addAssetPath(path); + } + // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm + // in L, so we do it unconditionally. + hackAssetManager.ensureStringBlocks(); + + Resources pluginRes = new PluginResourceWrapper(assetMgr, mainRes.getDisplayMetrics(), + mainRes.getConfiguration(), pluginDescriptor); + + return pluginRes; + } catch (Exception e) { + LogUtil.printException("创建插件res失败:" + absolutePluginApkPath, e); + } + } + return null; + } + + private static String[] buildAssetPath(boolean isStandalone, String app, String plugin, String[] dependencies) { + dependencies = null;//暂不支持资源多级依赖, 会导致插件难以维护 + String[] assetPaths = new String[isStandalone ? 1 : (2 + (dependencies==null?0:dependencies.length))]; + +// if (!isStandalone) { +// // 不可更改顺序否则不能兼容4.x +// assetPaths[0] = app; +// assetPaths[1] = plugin; +// if ("vivo".equalsIgnoreCase(Build.BRAND) || "oppo".equalsIgnoreCase(Build.BRAND) +// || "Coolpad".equalsIgnoreCase(Build.BRAND)) { +// // 但是!!!如是OPPO或者vivo4.x系统的话 ,要吧这个顺序反过来,否则在混合模式下会找不到资源 +// assetPaths[0] = plugin; +// assetPaths[1] = app; +// } +// LogUtil.d("create Plugin Resource from: ", assetPaths[0], assetPaths[1]); +// } else { +// assetPaths[0] = plugin; +// LogUtil.d("create Plugin Resource from: ", assetPaths[0]); +// } + + // 不可更改顺序否则不能兼容4.x,如华为P7-Android4.4.2 + assetPaths[0] = plugin; + LogUtil.v("create Plugin Resource from: ", plugin); + + if (!isStandalone) { + if (dependencies != null) { + //插件间资源依赖,这里需要遍历添加dependencies + //这里只处理1级依赖,若被依赖的插件又依赖其他插件,这里不做支持 + //插件依赖插件,如果被依赖的插件中包含资源文件,则需要在所有的插件中提供public.xml文件来分组资源id + for(int i = 0; i < dependencies.length; i++) { + PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(dependencies[i]); + if (pd != null) { + assetPaths[1+ i] = pd.getInstalledPath(); + LogUtil.v("create Plugin Resource from: ", assetPaths[1+ i]); + } else { + assetPaths[1+ i] = ""; + } + } + } + assetPaths[assetPaths.length -1] = app; + LogUtil.v("create Plugin Resource from: ", app); + } + + return assetPaths; + + } + + + /** + private static String[] buildAssetPath(boolean isStandalone, String host, String plugin, String[] dependencies) { + + + //暂不支持资源多级依赖, 因为目前资源id采用type分组, + //多级依赖编译时在type不变的情况下需要同时配合分段来完成,过于复杂。 + //除非采用aapt-packageId分组方式 + // + dependencies = null; + + ArrayList paths = new ArrayList(); + AssetManager hostAssetsManager = PluginLoader.getHostApplication().getAssets(); + Integer pathCount = (Integer)RefInvoker.invokeMethod(hostAssetsManager, + AssetManager.class, "getStringBlockCount", null, null); + if (pathCount != null) { + for(int cookie = 0; cookie < pathCount; cookie++) { + String assetsPath = (String)RefInvoker.invokeMethod(hostAssetsManager, + AssetManager.class, "getCookieName", + new Class[]{int.class}, new Object[]{cookie + 1}); + if(!TextUtils.isEmpty(assetsPath)) { + //不可更改顺序否则不能兼容4.x,如华为P7-Android4.4.2 + if (!assetsPath.equals(host)) { + paths.add(assetsPath); + LogUtil.d("create Plugin Resource from: ", assetsPath); + } else { + paths.add(plugin); + LogUtil.d("create Plugin Resource from: ", plugin); + if (!isStandalone) { + if (dependencies != null) { + //插件间资源依赖,需要遍历添加dependencies + //这里只处理1级依赖,若被依赖的插件又依赖其他插件,这里不做支持 + //插件依赖插件,如果被依赖的插件中包含资源文件,则需要在所有的插件中提供public.xml文件来分组和分段资源id + for(int j = 0; j < dependencies.length; j++) { + PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(dependencies[j]); + if (pd != null) { + paths.add(pd.getInstalledPath()); + LogUtil.d("create Plugin Resource from: ", pd.getInstalledPath()); + } + } + } + paths.add(assetsPath); + LogUtil.d("create Plugin Resource from: ", assetsPath); + + } + } + } + } + } + return paths.toArray(new String[paths.size()]); + }*/ + + /** + * 创建插件的Context + * @return + */ + public static Context createPluginContext(PluginDescriptor pluginDescriptor, Context base, Resources pluginRes, + ClassLoader pluginClassLoader) { + return new PluginContextTheme(pluginDescriptor, base, pluginRes, pluginClassLoader); + } + + /*package*/ static Context getNewPluginApplicationContext(PluginDescriptor pluginDescriptor, boolean shouldCreate) { + if (pluginDescriptor == null) { + return null; + } + //插件可能尚未初始化,确保使用前已经初始化 + final LoadedPlugin plugin; + if (shouldCreate) { + plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + } else { + plugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + } + if (plugin != null) { + PluginContextTheme newContext = (PluginContextTheme)PluginCreator.createPluginContext( + ((PluginContextTheme) plugin.pluginContext).getPluginDescriptor(), + FairyGlobal.getHostApplication().getBaseContext(), plugin.pluginResource, plugin.pluginClassLoader); + + newContext.setPluginApplication(plugin.pluginApplication); + + newContext.setTheme(pluginDescriptor.getApplicationTheme()); + + return newContext; + } + + return null; + } + + /*package*/ static Context getNewPluginApplicationContext(String pluginId) { + PluginDescriptor descriptor = PluginManagerHelper.getPluginDescriptorByFragmentId(pluginId); + return getNewPluginApplicationContext(descriptor, true); + } + + /** + * 根据当前插件的默认Context, 为当前插件的组件创建一个单独的context + * + * @param pluginContext + * @param base 由系统创建的Context。 其实际类型应该是ContextImpl + * @return + */ + public static Context createNewPluginComponentContext(Context pluginContext, Context base, int theme) { + PluginContextTheme newContext = null; + if (pluginContext != null) { + newContext = (PluginContextTheme)PluginCreator.createPluginContext(((PluginContextTheme) pluginContext).getPluginDescriptor(), + base, pluginContext.getResources(), + (ClassLoader) pluginContext.getClassLoader()); + + newContext.setPluginApplication((Application) ((PluginContextTheme) pluginContext).getApplicationContext()); + + if (theme == 0) { + newContext.setTheme(FairyGlobal.getHostApplication().getApplicationContext().getApplicationInfo().theme); + } else { + newContext.setTheme(theme); + } + } + return newContext; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginFilter.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginFilter.java new file mode 100644 index 00000000..2988b216 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginFilter.java @@ -0,0 +1,13 @@ +package com.limpoxe.fairy.core; + +public class PluginFilter { + + public static boolean maybePlugin(String pluginId) { + if (pluginId.startsWith("com.android.") + || pluginId.startsWith("com.google.")) { + return false; + } + return true; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInjector.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInjector.java new file mode 100644 index 00000000..fd1d2e28 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInjector.java @@ -0,0 +1,804 @@ +package com.limpoxe.fairy.core; + +import android.app.Activity; +import android.app.Application; +import android.app.Instrumentation; +import android.app.Service; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.Build; +import android.os.IBinder; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Window; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginActivityInfo; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.android.HackActivity; +import com.limpoxe.fairy.core.android.HackActivityThread; +import com.limpoxe.fairy.core.android.HackApplication; +import com.limpoxe.fairy.core.android.HackContextImpl; +import com.limpoxe.fairy.core.android.HackContextThemeWrapper; +import com.limpoxe.fairy.core.android.HackContextWrapper; +import com.limpoxe.fairy.core.android.HackLayoutInflater; +import com.limpoxe.fairy.core.android.HackLoadedApk; +import com.limpoxe.fairy.core.android.HackService; +import com.limpoxe.fairy.core.android.HackWindow; +import com.limpoxe.fairy.core.annotation.AnnotationProcessor; +import com.limpoxe.fairy.core.annotation.PluginContainer; +import com.limpoxe.fairy.core.compat.CompatForSupportv7_23_2; +import com.limpoxe.fairy.core.exception.PluginNotFoundError; +import com.limpoxe.fairy.core.exception.PluginNotInitError; +import com.limpoxe.fairy.core.loading.WaitForLoadingPluginActivity; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProviderClient; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +//import com.limpoxe.fairy.core.compat.CompatForAppComponentFactoryApi28; + +public class PluginInjector { + + /** + * 替换宿主程序Application对象的mBase是为了修改它的几个StartActivity、 + * StartService和SendBroadcast方法 + */ + static void injectBaseContext(Context context) { + LogUtil.v("替换宿主程序Application对象的mBase"); + HackContextWrapper wrapper = new HackContextWrapper(context); + wrapper.setBase(new PluginBaseContextWrapper(wrapper.getBase())); + } + + /** + * 注入Instrumentation主要是为了支持Activity + */ + static void injectInstrumentation() { + // 给Instrumentation添加一层代理,用来实现隐藏api的调用 + LogUtil.d("替换宿主程序Intstrumentation"); + HackActivityThread.wrapInstrumentation(); + } + + static void injectHandlerCallback() { + LogUtil.v("向宿主程序消息循环插入回调器"); + HackActivityThread.wrapHandler(); + } + + public static void installContentProviders(Context context, Context pluginContext, Collection pluginProviderInfos) { + ProviderInfo[] hostProviders = new ProviderInfo[0]; + try { + hostProviders = FairyGlobal.getHostApplication().getPackageManager() + .getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), + PackageManager.GET_PROVIDERS).providers; + } catch (Exception e) { + e.printStackTrace(); + } + boolean isAlreadyAddByHost = false; + List providers = new ArrayList(); + for (PluginProviderInfo pluginProviderInfo : pluginProviderInfos) { + + isAlreadyAddByHost = false; + + if (hostProviders != null) { + for(ProviderInfo hostProvider : hostProviders) { + if (hostProvider.authority.equals(pluginProviderInfo.getAuthority())) { + LogUtil.e("此contentProvider已经在宿主中定义,不再安装插件中定义的contentprovider", hostProvider.authority, pluginProviderInfo.getName(), pluginProviderInfo.getName()); + isAlreadyAddByHost = true; + break; + } + } + } + if (isAlreadyAddByHost) { + continue; + } + + ProviderInfo p = new ProviderInfo(); + p.name = pluginProviderInfo.getName(); + p.authority = pluginProviderInfo.getAuthority(); + p.applicationInfo = new ApplicationInfo(context.getApplicationInfo()); + p.applicationInfo.packageName = pluginContext.getPackageName(); + p.exported = pluginProviderInfo.isExported(); + p.packageName = context.getApplicationInfo().packageName; + p.grantUriPermissions = pluginProviderInfo.isGrantUriPermissions(); + providers.add(p); + } + + if(providers.size() > 0) { + LogUtil.e("为插件安装ContentProvider", pluginContext.getPackageName(), pluginProviderInfos.size()); + //安装的时候使用的是插件的Context, 所有无需对Classloader进行映射处理 + //todo + HackActivityThread.get().installContentProviders(pluginContext, providers); + } + } + + static void injectInstrumetionFor360Safe(Activity activity, Instrumentation pluginInstrumentation) { + // 检查mInstrumention是否已经替换成功。 + // 之所以要检查,是因为如果手机上安装了360手机卫士等app,它们可能会劫持用户app的ActivityThread对象, + // 导致在PluginApplication的onCreate方法里面替换mInstrumention可能会失败 + // 所以这里再做一次检查 + HackActivity hackActivity = new HackActivity(activity); + Instrumentation instrumention = hackActivity.getInstrumentation(); + if (!(instrumention instanceof PluginInstrumentionWrapper)) { + // 说明被360还原了,这里再次尝试替换 + hackActivity.setInstrumentation(pluginInstrumentation); + } + } + + static void injectActivityContext(final Activity activity) { + if (activity instanceof WaitForLoadingPluginActivity) { + return; + } + if (activity instanceof RealHostClassLoader.TolerantActivity) { + return; + } + + LogUtil.v("injectActivityContext"); + + PluginContainer container = null; + boolean isStubActivity = false; + + if (ProcessUtil.isPluginProcess()) { + // 如果是打开插件中的activity, + Intent intent = activity.getIntent(); + isStubActivity = PluginManagerProviderClient.isStub(intent.getComponent().getClassName()); + + // 或者是打开的用来显示插件组件的宿主activity + container = AnnotationProcessor.getPluginContainer(activity.getClass()); + } + + HackActivity hackActivity = new HackActivity(activity); + + if (isStubActivity || container != null) { + + // 在activityoncreate之前去完成attachBaseContext的事情 + + Context pluginContext = null; + PluginDescriptor pluginDescriptor = null; + + if (isStubActivity) { + //是打开插件中的activity + + pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(activity.getClass().getName()); + if(pluginDescriptor == null) { + throw new PluginNotFoundError("未找到插件:" + activity.getClass().getName() + ", 插件未安装、或正在安装、或已损坏"); + } + + LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + if (plugin == null) { + throw new PluginNotInitError("插件尚未初始化 " + pluginDescriptor.getPackageName() + " " + plugin); + } + + pluginContext = PluginCreator.createNewPluginComponentContext(plugin.pluginContext, activity.getBaseContext(), 0); + + //获取插件Application对象 + Application pluginApp = plugin.pluginApplication; + + //重设mApplication + hackActivity.setApplication(pluginApp); + } else { + + //是打开的用来显示插件组件的宿主activity, 比如在宿主Activity中显示插件Fragment或者插件View + + String pluginId = container.pluginId(); + if (!TextUtils.isEmpty(pluginId)) { + //进入这里表示指定了这个宿主Activity "只显示" 某个插件的组件 + // 因此直接将这个Activity的Context也替换成插件的Context + pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if(pluginDescriptor == null) { + throw new PluginNotFoundError("未找到插件:" + pluginId + ", 插件未安装、或正在安装、或已损坏"); + } + + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin == null) { + throw new PluginNotInitError("启动插件失败 " + pluginDescriptor.getPackageName()); + } + pluginContext = PluginCreator.createNewPluginComponentContext(plugin.pluginContext, activity.getBaseContext(), 0); + + } else { + //进入这里表示这个宿主可能要同时显示来自多个不同插件的组件, 也就没办法将Context替换成之中某一个插件的context, + //如果多个不同插件的组件是通过PluginView标签添加的,则会通过注入PluginViewFactory去处理Classloader + + //这一行是为了配合RealHostClassLoader解决在宿主Activity被系统自动恢复时同时自动恢复了来自插件的Fragment而产生的ClassNotFound问题 + PluginInjector.hackHostClassLoaderIfNeeded(); + + //不管怎样,如果打开的是宿主的Activity,都需要注入一个Context,用来在宿主中startActivity和sendBroadcast时检查目标是否为插件组件 + Context mainContext = new PluginBaseContextWrapper(activity.getBaseContext()); + hackActivity.setBase(null); + hackActivity.attachBaseContext(mainContext); + return; + } + + } + + PluginActivityInfo pluginActivityInfo = pluginDescriptor.getActivityInfos().get(activity.getClass().getName()); + + ActivityInfo activityInfo = hackActivity.getActivityInfo(); + int pluginAppTheme = getPluginTheme(activityInfo, pluginActivityInfo, pluginDescriptor); + + LogUtil.e("Theme", "0x" + Integer.toHexString(pluginAppTheme), activity.getClass().getName()); + + //pluginActivityInfo != null的判断是为了避免在Fragment插件嵌入其他Activity时没有pluginActivityInfo造成NPE + if (pluginActivityInfo != null && pluginActivityInfo.isUseHostPackageName()) { + LogUtil.e("useHostPackageName true"); + ((PluginContextTheme)pluginContext).setUseHostPackageName(true); + } + + resetActivityContext(pluginContext, activity, pluginAppTheme); + + //如果是配置了PluginContainer注解和pluginId的宿主Activity,此宿主的Activity的全屏配置可能会被插件的主题覆盖而丢失,可以通过代码设置回去 + resetWindowConfig(pluginContext, pluginDescriptor, activity, activityInfo, pluginActivityInfo); + + String simpleName = activity.getClass().getSimpleName(); + activity.setTitle(simpleName!=null?simpleName:activity.getClass().getName()); + + } else { + // 如果是打开宿主程序的activity,注入一个无害的Context,用来在宿主程序中startService和sendBroadcast时检查打开的对象是否是插件中的对象 + // 插入Context + Context mainContext = new PluginBaseContextWrapper(activity.getBaseContext()); + hackActivity.setBase(null); + hackActivity.attachBaseContext(mainContext); + } + } + + static void resetActivityContext(final Context pluginContext, final Activity activity, + final int pluginAppTheme) { + if (pluginContext == null) { + return; + } + + // 重设BaseContext + HackContextThemeWrapper hackContextThemeWrapper = new HackContextThemeWrapper(activity); + hackContextThemeWrapper.setBase(null); + hackContextThemeWrapper.attachBaseContext(pluginContext); + + // 由于在attach的时候Resource已经被初始化了,所以需要重置Resource + hackContextThemeWrapper.setResources(null); + + CompatForSupportv7_23_2.fixResource(pluginContext, activity); + + // 重设theme + if (pluginAppTheme != 0) { + hackContextThemeWrapper.setTheme(null); + activity.setTheme(pluginAppTheme); + } + // 重设theme + ((PluginContextTheme)pluginContext).mTheme = null; + pluginContext.setTheme(pluginAppTheme); + + Window window = activity.getWindow(); + + HackWindow hackWindow = new HackWindow(window); + //重设mContext + hackWindow.setContext(pluginContext); + + //重设mWindowStyle + hackWindow.setWindowStyle(null); + + // 重设LayoutInflater + LogUtil.v(window.getClass().getName()); + //注意:这里getWindow().getClass().getName() 不一定是android.view.Window + //如miui下返回MIUI window + hackWindow.setLayoutInflater(window.getClass().getName(), LayoutInflater.from(activity)); + + // 如果api>=11,还要重设factory2 + if (Build.VERSION.SDK_INT >= 11) { + new HackLayoutInflater(window.getLayoutInflater()).setPrivateFactory(activity); + } + } + + static void resetWindowConfig(final Context pluginContext, final PluginDescriptor pd, + final Activity activity, + final ActivityInfo activityInfo, + final PluginActivityInfo pluginActivityInfo) { + + if (pluginActivityInfo != null) { + + //如果PluginContextTheme的getPackageName返回了插件包名,需要在这里对attribute修正 + activity.getWindow().getAttributes().packageName = FairyGlobal.getHostApplication().getPackageName(); + + if (null != pluginActivityInfo.getWindowSoftInputMode()) { + activity.getWindow().setSoftInputMode((int)Long.parseLong(pluginActivityInfo.getWindowSoftInputMode().replace("0x", ""), 16)); + } + if (Build.VERSION.SDK_INT >= 14) { + if (null != pluginActivityInfo.getUiOptions()) { + activity.getWindow().setUiOptions((int)Long.parseLong(pluginActivityInfo.getUiOptions().replace("0x", ""), 16)); + } + } + if (null != pluginActivityInfo.getScreenOrientation()) { + int orientation = (int)Long.parseLong(pluginActivityInfo.getScreenOrientation()); + //noinspection ResourceType + if (orientation != activityInfo.screenOrientation && !activity.isChild()) { + //noinspection ResourceType + //框架中只内置了unspec和landscape两种screenOrientation + //如果是其他类型,这里通过代码实现切换 + LogUtil.v("修改screenOrientation"); + activity.setRequestedOrientation(orientation); + } + } + if (Build.VERSION.SDK_INT >= 18 && !activity.isChild()) { + Boolean isImmersive = ResourceUtil.getBoolean(pluginActivityInfo.getImmersive(), pluginContext); + if (isImmersive != null) { + activity.setImmersive(isImmersive); + } + } + + String activityClassName = activity.getClass().getName(); + LogUtil.v(activityClassName, "immersive", pluginActivityInfo.getImmersive()); + LogUtil.v(activityClassName, "screenOrientation", pluginActivityInfo.getScreenOrientation()); + LogUtil.v(activityClassName, "launchMode", pluginActivityInfo.getLaunchMode()); + LogUtil.v(activityClassName, "windowSoftInputMode", pluginActivityInfo.getWindowSoftInputMode()); + LogUtil.v(activityClassName, "uiOptions", pluginActivityInfo.getUiOptions()); + } + + //如果是独立插件,由于没有合并资源,这里还需要替换掉 mActivityInfo, + //避免activity试图通过ActivityInfo中的资源id来读取资源时失败 + activityInfo.icon = pd.getApplicationIcon(); + activityInfo.logo = pd.getApplicationLogo(); + if (Build.VERSION.SDK_INT >= 19) { + activity.getWindow().setIcon(activityInfo.icon); + activity.getWindow().setLogo(activityInfo.logo); + } + } + + /*package*/static void replaceReceiverContext(Context baseContext, Context newBase) { + + if (HackContextImpl.instanceOf(baseContext)) { + ContextWrapper receiverRestrictedContext = new HackContextImpl(baseContext).getReceiverRestrictedContext(); + new HackContextWrapper(receiverRestrictedContext).setBase(newBase); + } + } + + //这里是因为在多进程情况下,杀死插件进程,自动恢复service时有个bug导致一个service同时存在多个service实例 + //这里做个遍历保护 + //break; + /*package*/static void replacePluginServiceContext(String serviceName) { + Map services = HackActivityThread.get().getServices(); + if (services != null) { + Iterator itr = services.values().iterator(); + while(itr.hasNext()) { + Service service = itr.next(); + if (service != null && service.getClass().getName().equals(serviceName) ) { + + replacePluginServiceContext(serviceName,service ); + } + + } + } + } + + public static void replacePluginServiceContext(String servieName, Service service) { + PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByClassName(servieName); + + LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pd.getPackageName()); + + HackService hackService = new HackService(service); + hackService.setBase( + PluginCreator.createNewPluginComponentContext(plugin.pluginContext, + service.getBaseContext(), pd.getApplicationTheme())); + hackService.setApplication(plugin.pluginApplication); + hackService.setClassName(PluginManagerProviderClient.bindStubService(service.getClass().getName())); + + } + + /*package*/static void replaceHostServiceContext(String serviceName) { + Map services = HackActivityThread.get().getServices(); + if (services != null) { + Iterator itr = services.values().iterator(); + while(itr.hasNext()) { + Service service = itr.next(); + if (service != null && service.getClass().getName().equals(serviceName) ) { + PluginInjector.injectBaseContext(service); + break; + } + + } + } + } + + /** + * 主题的选择顺序为 先选择插件Activity配置的主题,再选择插件Application配置的主题, + * 如果是非独立插件,再选择宿主Activity主题 + * 如果是独立插件,再选择系统默认主题 + * @param activityInfo + * @param pluginActivityInfo + * @param pd + * @return + */ + private static int getPluginTheme(ActivityInfo activityInfo, PluginActivityInfo pluginActivityInfo, PluginDescriptor pd) { + int pluginAppTheme = 0; + if (pluginActivityInfo != null ) { + pluginAppTheme = ResourceUtil.parseResId(pluginActivityInfo.getTheme()); + } + if (pluginAppTheme == 0) { + pluginAppTheme = pd.getApplicationTheme(); + } + + if (pluginAppTheme == 0 && pd.isStandalone()) { + pluginAppTheme = android.R.style.Theme_DeviceDefault; + } + + if (pluginAppTheme == 0) { + //If the activity defines a theme, that is used; else, the application theme is used. + pluginAppTheme = activityInfo.getThemeResource(); + } + return pluginAppTheme; + } + + /** + * 如果插件中不包含service、receiver,是不需要替换classloader的 + */ + public static void hackHostClassLoaderIfNeeded() { + LogUtil.v("hackHostClassLoaderIfNeeded"); + + HackApplication hackApplication = new HackApplication(FairyGlobal.getHostApplication()); + Object mLoadedApk = hackApplication.getLoadedApk(); + if (mLoadedApk == null) { + //重试一次 + mLoadedApk = hackApplication.getLoadedApk(); + } + if(mLoadedApk == null) { + //换个方式再试一次 + mLoadedApk = HackActivityThread.getLoadedApk(); + } + if (mLoadedApk != null) { + HackLoadedApk hackLoadedApk = new HackLoadedApk(mLoadedApk); + ClassLoader originalLoader = hackLoadedApk.getClassLoader(); + if (!(originalLoader instanceof HostClassLoader)) { + HostClassLoader newLoader = new HostClassLoader("", new RealHostClassLoader("", + FairyGlobal.getHostApplication().getCacheDir().getAbsolutePath(),/**这里这两个目录参数无实际意义**/ + FairyGlobal.getHostApplication().getCacheDir().getAbsolutePath(),/**这里这两个目录参数无实际意义**/ + originalLoader)); + hackLoadedApk.setClassLoader(newLoader); + } + } else { + LogUtil.e("What!!Why?"); + } + } + + public static void injectAppComponentFactory() { + if (Build.VERSION.SDK_INT < 28) { + return; + } + LogUtil.v("hackHostClassLoaderIfNeeded"); + + HackApplication hackApplication = new HackApplication(FairyGlobal.getHostApplication()); + Object mLoadedApk = hackApplication.getLoadedApk(); + if (mLoadedApk == null) { + //重试一次 + mLoadedApk = hackApplication.getLoadedApk(); + } + if(mLoadedApk == null) { + //换个方式再试一次 + mLoadedApk = HackActivityThread.getLoadedApk(); + } + if (mLoadedApk != null) { + HackLoadedApk hackLoadedApk = new HackLoadedApk(mLoadedApk); + //Android-P提供了组件钩子,用来拓展组件初始化流程 + //hackLoadedApk.setAppComponentFactory(new CompatForAppComponentFactoryApi28(hackLoadedApk.getAppComponentFactory())); + } else { + LogUtil.e("What!!Why?"); + } + } + + /** + * 将当前进程的resource替换掉,适用于宿主资源热修复以及所有插件和宿主的资源完全融合的插件方案 + * @param context + * @param newResourceFile + */ + public static void replaceResource(Context context, String newResourceFile) { + if (newResourceFile == null || context == null) { + return; + } + + try { + Class activityThread_clazz = Class.forName("android.app.ActivityThread"); + Class loadedApk_clazz = null; + try { + loadedApk_clazz = Class.forName("android.app.LoadedApk"); + } catch (ClassNotFoundException exception) { + loadedApk_clazz = Class.forName("android.app.ActivityThread$PackageInfo"); + } + Field resDir = findField(loadedApk_clazz, "mResDir"); + Field publicSourceDirField = findField(ApplicationInfo.class, "publicSourceDir"); + Field packagesField = findField(activityThread_clazz, "mPackages"); + Field[] packagesFields = null; + if (Build.VERSION.SDK_INT < 27) { + packagesFields = new Field[]{packagesField, findField(activityThread_clazz, "mResourcePackages")}; + } else { + packagesFields = new Field[]{packagesField}; + } + + Object activityThread = getActivityThread(context, activityThread_clazz); + ApplicationInfo appInfo = context.getApplicationInfo(); + + for(int i = 0; i < packagesFields.length; ++i) { + Object resourcesManager = packagesFields[i].get(activityThread); + Iterator iterator = ((Map)resourcesManager).entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry> entry = (Map.Entry)iterator.next(); + Object resourceImpl = ((WeakReference)entry.getValue()).get(); + if (resourceImpl != null) { + String resDirPath = (String)resDir.get(resourceImpl); + if (appInfo.sourceDir.equals(resDirPath)) { + resDir.set(resourceImpl, newResourceFile); + } + } + } + } + + AssetManager newAssetManager = (AssetManager)AssetManager.class.getConstructor().newInstance(); + Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); + mAddAssetPath.setAccessible(true); + Integer cookie = (Integer)mAddAssetPath.invoke(newAssetManager, newResourceFile); + if (cookie == null || cookie == 0) { + throw new IllegalMonitorStateException(); + } + + try { + Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); + mEnsureStringBlocks.setAccessible(true); + mEnsureStringBlocks.invoke(newAssetManager); + } catch (Exception e) { + } + + Collection references; + if (Build.VERSION.SDK_INT >= 19) { + Class resourcesManager_clazz = Class.forName("android.app.ResourcesManager"); + Method mGetInstance = resourcesManager_clazz.getDeclaredMethod("getInstance"); + mGetInstance.setAccessible(true); + Object resourcesManager = mGetInstance.invoke((Object)null); + try { + Field mAssets = resourcesManager_clazz.getDeclaredField("mActiveResources"); + mAssets.setAccessible(true); + Map> map = (Map)mAssets.get(resourcesManager); + references = map.values(); + } catch (NoSuchFieldException e) { + Field mResourcesImpl = resourcesManager_clazz.getDeclaredField("mResourceReferences"); + mResourcesImpl.setAccessible(true); + references = (Collection)mResourcesImpl.get(resourcesManager); + } + } else { + Field mAssets = activityThread_clazz.getDeclaredField("mActiveResources"); + mAssets.setAccessible(true); + Map> map = (Map)mAssets.get(activityThread); + references = map.values(); + } + + Iterator iterator = references.iterator(); + while(iterator.hasNext()) { + WeakReference weakReference = (WeakReference)iterator.next(); + Resources resources = (Resources)weakReference.get(); + if (resources != null) { + try { + Field mAssets = Resources.class.getDeclaredField("mAssets"); + mAssets.setAccessible(true); + mAssets.set(resources, newAssetManager); + } catch (Throwable throwable) { + Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); + mResourcesImpl.setAccessible(true); + Object resourceImpl = mResourcesImpl.get(resources); + Field mAssets = findField(resourceImpl.getClass(), "mAssets"); + mAssets.setAccessible(true); + mAssets.set(resourceImpl, newAssetManager); + } + resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); + } + } + + if (Build.VERSION.SDK_INT >= 24) { + try { + if (publicSourceDirField != null) { + publicSourceDirField.set(context.getApplicationInfo(), newResourceFile); + } + } catch (Throwable throwable) { + } + } + } catch (Throwable throwable) { + throw new IllegalStateException(throwable); + } + } + + /** + * 清理resource静态资源缓存 + * @param resources + */ + public static void refreshResourceCaches(Object resources) { + if (Build.VERSION.SDK_INT >= 21) { + try { + Field mTypedArrayPool = Resources.class.getDeclaredField("mTypedArrayPool"); + mTypedArrayPool.setAccessible(true); + Object arrayPool = mTypedArrayPool.get(resources); + Class poolClass = arrayPool.getClass(); + Method acquireMethod = poolClass.getDeclaredMethod("acquire"); + acquireMethod.setAccessible(true); + + Object typedArray; + do { + typedArray = acquireMethod.invoke(arrayPool); + } while(typedArray != null); + } catch (Throwable throwable) { + } + } + + if (Build.VERSION.SDK_INT >= 23) { + try { + Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); + mResourcesImpl.setAccessible(true); + resources = mResourcesImpl.get(resources); + } catch (Throwable throwable) { + } + } + + Object lock = null; + Field field; + if (Build.VERSION.SDK_INT >= 18) { + try { + field = resources.getClass().getDeclaredField("mAccessLock"); + field.setAccessible(true); + lock = field.get(resources); + } catch (Throwable throwable) { + } + } else { + try { + field = Resources.class.getDeclaredField("mTmpValue"); + field.setAccessible(true); + lock = field.get(resources); + } catch (Throwable throwable) { + } + } + + if (lock == null) { + lock = PluginInjector.class; + } + + synchronized(lock) { + refreshResourceCache(resources, "mDrawableCache"); + refreshResourceCache(resources, "mColorDrawableCache"); + refreshResourceCache(resources, "mColorStateListCache"); + if (Build.VERSION.SDK_INT >= 23) { + refreshResourceCache(resources, "mAnimatorCache"); + refreshResourceCache(resources, "mStateListAnimatorCache"); + } else if (Build.VERSION.SDK_INT == 19) { + refreshResourceCache(resources, "sPreloadedDrawables"); + refreshResourceCache(resources, "sPreloadedColorDrawables"); + refreshResourceCache(resources, "sPreloadedColorStateLists"); + } + + } + } + + private static boolean refreshResourceCache(Object resources, String fieldName) { + try { + Field cacheField = findField(resources.getClass(), fieldName); + if (cacheField == null) { + cacheField = Resources.class.getDeclaredField(fieldName); + } + + cacheField.setAccessible(true); + Object cache = cacheField.get(resources); + Class type = cacheField.getType(); + if (Build.VERSION.SDK_INT < 16) { + if (cache instanceof SparseArray) { + ((SparseArray)cache).clear(); + return true; + } + } else { + Method clearSparseMap; + if (Build.VERSION.SDK_INT < 23) { + if ("mColorStateListCache".equals(fieldName)) { + if (cache instanceof LongSparseArray) { + ((LongSparseArray)cache).clear(); + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (type.isAssignableFrom(ArrayMap.class)) { + clearSparseMap = Resources.class.getDeclaredMethod("clearDrawableCachesLocked", ArrayMap.class, Integer.TYPE); + clearSparseMap.setAccessible(true); + clearSparseMap.invoke(resources, cache, -1); + return true; + } + } + + if (type.isAssignableFrom(LongSparseArray.class)) { + try { + clearSparseMap = Resources.class.getDeclaredMethod("clearDrawableCachesLocked", LongSparseArray.class, Integer.TYPE); + clearSparseMap.setAccessible(true); + clearSparseMap.invoke(resources, cache, -1); + return true; + } catch (NoSuchMethodException e) { + if (cache instanceof LongSparseArray) { + ((LongSparseArray)cache).clear(); + return true; + } + } + } else if (type.isArray() && type.getComponentType().isAssignableFrom(LongSparseArray.class)) { + LongSparseArray[] arrays = (LongSparseArray[])((LongSparseArray[])cache); + for(int i = 0; i < arrays.length; ++i) { + LongSparseArray array = arrays[i]; + if (array != null) { + array.clear(); + } + } + + return true; + } + } + } else { + while(type != null) { + try { + clearSparseMap = type.getDeclaredMethod("onConfigurationChange", Integer.TYPE); + clearSparseMap.setAccessible(true); + clearSparseMap.invoke(cache, -1); + return true; + } catch (Throwable throwable) { + type = type.getSuperclass(); + } + } + } + } + } catch (Throwable throwable) { + } + + return false; + } + + private static Object getActivityThread(Context context, Class activityThread_clazz) { + try { + Method currentActivityThread_method = activityThread_clazz.getMethod("currentActivityThread"); + currentActivityThread_method.setAccessible(true); + Object currentActivityThread = currentActivityThread_method.invoke((Object)null); + if (currentActivityThread != null) { + return currentActivityThread; + } + if (context != null) { + Field mLoadedApk_field = context.getClass().getField("mLoadedApk"); + mLoadedApk_field.setAccessible(true); + Object mLoadedApk = mLoadedApk_field.get(context); + Field mActivityThread_field = mLoadedApk.getClass().getDeclaredField("mActivityThread"); + mActivityThread_field.setAccessible(true); + return mActivityThread_field.get(mLoadedApk); + } + } catch (Throwable throwable) { + } + return null; + } + + private static Field findField(Class clazz, String fieldName) { + if (clazz == null || fieldName == null) { + return null; + } + Class localClass = clazz; + while(localClass != Object.class) { + try { + Field field = localClass.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + localClass = localClass.getSuperclass(); + } + } + return null; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInstrumentionWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInstrumentionWrapper.java new file mode 100644 index 00000000..5768e9ef --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginInstrumentionWrapper.java @@ -0,0 +1,624 @@ +package com.limpoxe.fairy.core; + +import static com.limpoxe.fairy.core.PluginLauncher.instance; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; +import android.app.Fragment; +import android.app.Instrumentation; +import android.app.Service; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.UserHandle; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.android.HackContextImpl; +import com.limpoxe.fairy.core.android.HackInstrumentation; +import com.limpoxe.fairy.core.annotation.AnnotationProcessor; +import com.limpoxe.fairy.core.annotation.PluginContainer; +import com.limpoxe.fairy.core.loading.WaitForLoadingPluginActivity; +import com.limpoxe.fairy.core.localservice.LocalServiceManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidWebkitWebViewFactoryProvider; +import com.limpoxe.fairy.core.viewfactory.PluginViewFactory; +import com.limpoxe.fairy.manager.PluginActivityMonitor; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProviderClient; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.PackageVerifyer; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Set; + +/** + * 插件Activity免注册的主要实现原理。 如有必要,可以增加被代理的方法数量。 + * + * @author cailiming + * + */ +public class PluginInstrumentionWrapper extends Instrumentation { + + private static final String RELAUNCH_FLAG = "relaunch.category."; + + private final HackInstrumentation hackInstrumentation; + private Instrumentation real; + private PluginActivityMonitor monitor; + + public PluginInstrumentionWrapper(Instrumentation instrumentation) { + this.hackInstrumentation = new HackInstrumentation(instrumentation); + this.real = instrumentation; + this.monitor = new PluginActivityMonitor(); + } + + /** + * 此方法在application的attach之后被ActivityThread调用 + * @param app + */ + @Override + public void callApplicationOnCreate(Application app) { + //FIXME 对TabActivity的支持在9上有bug会导致插件Application会被创建两次 + + //ContentProvider的相关操作应该放在installContentProvider之后执行, + //而installContentProvider是ActivityThread在调用application的attach之后,onCreate之前执行 + //因此可以触发ContentProvider调用的最早时机就是这里了 + //下面这个函数内就会触发PluginManagerProvider的调用 + beforeHostCallApplicationOnCreate(); + real.callApplicationOnCreate(app); + } + + private static void beforeHostCallApplicationOnCreate() { + LocalServiceManager.init(); + boolean isHostVerionChanged = isHostVerionChanged(); + ArrayList pluginDescriptorList = PluginManagerHelper.getPlugins(); + //边循环边删除没问题,因为是不同的列表对象 + for(int i = 0; i < pluginDescriptorList.size(); i++) { + PluginDescriptor pluginDescriptor = pluginDescriptorList.get(i); + if (isHostVerionChanged && !PackageVerifyer.isCompatibleWithHost(pluginDescriptor)) { + LogUtil.e("插件RequiredHostVersionName:" + pluginDescriptor.getRequiredHostVersionName()); + LogUtil.e("当前宿主版本不支持此插件版本,卸载此插件 " + pluginDescriptor.getPackageName()); + PluginManagerHelper.remove(pluginDescriptor.getPackageName()); + LogUtil.e("卸载完成"); + } else if (pluginDescriptor.isBroken()) { + LogUtil.e("插件文件可能已损坏,卸载此插件 " + pluginDescriptor.getPackageName()); + PluginManagerHelper.remove(pluginDescriptor.getPackageName()); + LogUtil.e("卸载完成"); + } else { + if (ProcessUtil.isPluginProcess()) { + LogUtil.v("注册插件内定义的localService"); + LocalServiceManager.registerService(pluginDescriptor); + LogUtil.v("注册完成"); + } + if (pluginDescriptor.getAutoStart()) { + //宿主启动时自动唤醒自启动插件 + LogUtil.w("插件配置了自启动, 唤起插件:" + pluginDescriptor.getPackageName()); + PluginManagerHelper.wakeup(pluginDescriptor.getPackageName()); + } + } + } + } + + private static boolean isHostVerionChanged() { + //如果宿主进行了覆盖安装的升级操作,移除已经安装的对宿主版本有要求的非独立插件 + String KEY = "last_host_versionName"; + SharedPreferences prefs = FairyGlobal.getHostApplication().getSharedPreferences("fairy_configs", Context.MODE_PRIVATE); + String lastHostVersoinName = prefs.getString(KEY, null); + String hostVersionName = null; + try { + PackageManager packageManager = FairyGlobal.getHostApplication().getPackageManager(); + PackageInfo hostPackageInfo = packageManager.getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), PackageManager.GET_META_DATA); + hostVersionName = hostPackageInfo.versionName; + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("PluginLoader.isHostVerionChanged", e); + } + boolean isHostVerionChanged = hostVersionName != null && !hostVersionName.equals(lastHostVersoinName); + //版本号发生了变化, 保存新的版本号 + if (isHostVerionChanged) { + prefs.edit().putString(KEY, hostVersionName).apply(); + } + return isHostVerionChanged; + } + + @Override + public boolean onException(Object obj, Throwable throwable) { + try { + if (obj instanceof Activity) { + ((Activity) obj).finish(); + } else if (obj instanceof Service) { + ((Service) obj).stopSelf(); + } + } catch (Exception e1) { + // + } + return real.onException(obj, throwable); + } + + @Override + public Application newApplication(ClassLoader cl, String className, Context context) + throws InstantiationException, IllegalAccessException, + ClassNotFoundException { + if (ProcessUtil.isPluginProcess()) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + return instance().getRunningPlugin(pluginDescriptor.getPackageName()).pluginApplication; + } + } + return real.newApplication(cl, className, context); + } + + @Override + public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, + IllegalAccessException, ClassNotFoundException { + + ClassLoader orignalCl = cl; + String orginalClassName = className; + String orignalIntent = intent.toString(); + + if (ProcessUtil.isPluginProcess()) { + // 将PluginStubActivity替换成插件中的activity + if (PluginManagerProviderClient.isStub(className)) { + + String action = intent.getAction(); + + LogUtil.d("创建插件Activity", action, className); + + if (action != null && action.contains(PluginIntentResolver.CLASS_SEPARATOR)) { + String[] targetClassName = action.split(PluginIntentResolver.CLASS_SEPARATOR); + String pluginClassName = targetClassName[0]; + + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(pluginClassName); + + if (pluginDescriptor != null) { + boolean isRunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isRunning) { + if (FairyGlobal.getMinLoadingTime() > 0 && FairyGlobal.getLoadingResId() != 0) { + return waitForLoading(pluginDescriptor, pluginClassName); + } else { + //这个else是为了处理内嵌在tabactivity中的情况, 需要提前start,否则内嵌tab会被拉出tab单独显示 + PluginLauncher.instance().startPlugin(pluginDescriptor); + } + } + } + + Class clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, pluginClassName); + + if (clazz != null) { + className = pluginClassName; + cl = clazz.getClassLoader(); + + intent.setExtrasClassLoader(cl); + if (targetClassName.length >1) { + //之前为了传递classNae,intent的action被修改过 这里再把Action还原到原始的Action + intent.setAction(targetClassName[1]); + } else { + intent.setAction(null); + } + //添加一个标记符 + intent.addCategory(RELAUNCH_FLAG + className); + } else { + + //找不到class,加个容错处理 + className = RealHostClassLoader.TolerantActivity.class.getName(); + cl = RealHostClassLoader.TolerantActivity.class.getClassLoader(); + //添加一个标记符 + intent.addCategory(RELAUNCH_FLAG + className); + + if (pluginDescriptor != null) { + LogUtil.e("error, remove " + pluginDescriptor.getPackageName()); + PluginManagerHelper.remove(pluginDescriptor.getPackageName()); + } + + //收集状态 + LogUtil.e("ClassNotFound: pluginDescriptor=" + pluginDescriptor + + ", pluginClassName=" + pluginClassName + + ", " + (pluginDescriptor==null?"":(pluginDescriptor.getInstalledPath() + + ", " + pluginDescriptor.getInstallationTime() + + ", " + pluginDescriptor.getVersion() + + ", " + (new File(pluginDescriptor.getInstalledPath()).exists())))); + + } + } else if (PluginManagerProviderClient.isExact(className, PluginDescriptor.ACTIVITY)) { + + //这个逻辑是为了支持外部app唤起配置了stub_exact的插件Activity + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + + if (pluginDescriptor != null && FairyGlobal.getMinLoadingTime() > 0 && FairyGlobal.getLoadingResId() != 0) { + boolean isRunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isRunning) { + return waitForLoading(pluginDescriptor, className); + } + } + + Class clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, className); + + if (clazz != null) { + cl = clazz.getClassLoader(); + //添加一个标记符 + intent.addCategory(RELAUNCH_FLAG + className); + } else { + + //收集状态 + LogUtil.e("ClassNotFound: pluginDescriptor=" + pluginDescriptor + + ", pluginClassName=" + className + + ", " + (pluginDescriptor==null?"":(pluginDescriptor.getInstalledPath() + + ", " + pluginDescriptor.getInstallationTime() + + ", " + pluginDescriptor.getVersion() + + ", " + (new File(pluginDescriptor.getInstalledPath()).exists())))); + + //精确匹配却找不着目标,有多种可能,其中一个可能是收到外部发来的组件Intent时,插件还没安装 + //因此这里强行返回容错的class + className = RealHostClassLoader.TolerantActivity.class.getName(); + cl = RealHostClassLoader.TolerantActivity.class.getClassLoader(); + //添加一个标记符 + intent.addCategory(RELAUNCH_FLAG + className); + + } + } else { + //进入这个分支可能是因为activity重启了,比如横竖屏切换,由于上面的分支已经把Action还原到原始到Action了 + //这里只能通过之前添加的标记符来查找className + LogUtil.e("check with RELAUNCH_FLAG"); + boolean found = false; + Set category = intent.getCategories(); + if (category != null) { + Iterator itr = category.iterator(); + while (itr.hasNext()) { + String cate = itr.next(); + + if (cate.startsWith(RELAUNCH_FLAG)) { + className = cate.replace(RELAUNCH_FLAG, ""); + + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + + if (pluginDescriptor != null && FairyGlobal.getMinLoadingTime() > 0 && FairyGlobal.getLoadingResId() != 0) { + boolean isRunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isRunning) {//理论上这里的isRunning应当是true + return waitForLoading(pluginDescriptor, className); + } + } + + Class clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, className); + if (clazz != null) { + cl = clazz.getClassLoader(); + found = true; + } else { + //这里也需要处理STUB_EXACT匹配但插件尚未安装的情况 + if (className.equals(RealHostClassLoader.TolerantActivity.class.getName())) { + cl = RealHostClassLoader.TolerantActivity.class.getClassLoader(); + found = true; + } + } + break; + } + } + } + if (!found) { + throw new ClassNotFoundException("className : " + className + ", intent : " + intent.toString(), new Throwable()); + } + } + } else { + //到这里有2中种情况 + //1、确实是宿主Activity + //2、是插件Activity,但是上面的if没有识别出来(这种情况目前只发现在ActivityGroup情况下会出现,因为ActivityGroup不会触发resolveActivity方法,导致Intent没有更换) + //判断上述两种情况可以通过ClassLoader的类型来判断, 判断出来以后补一个resolveActivity方法 + if (cl instanceof PluginClassLoader || cl instanceof RealPluginClassLoader) { + PluginIntentResolver.resolveActivity(intent); + } else { + //Do Nothing + } + } + } + + try { + return real.newActivity(cl, className, intent); + } catch (ClassNotFoundException e) { + //收集状态,便于异常分析 + throw new ClassNotFoundException( + " orignalCl : " + orignalCl.toString() + + ", orginalClassName : " + orginalClassName + + ", orignalIntent : " + orignalIntent + + ", currentCl : " + cl.toString() + + ", currentClassName : " + className + + ", currentIntent : " + intent.toString() + + ", process : " + ProcessUtil.isPluginProcess() + + ", isStubActivity : " + PluginManagerProviderClient.isStub(orginalClassName) + + ", isExact : " + PluginManagerProviderClient.isExact(orginalClassName, PluginDescriptor.ACTIVITY), e); + } + } + + private Activity waitForLoading(PluginDescriptor pluginDescriptor, String targetClassName) { + WaitForLoadingPluginActivity waitForLoadingPluginActivity = new WaitForLoadingPluginActivity(); + waitForLoadingPluginActivity.setTargetPlugin(pluginDescriptor, targetClassName); + return waitForLoadingPluginActivity; + } + + @Override + public void callActivityOnCreate(Activity activity, Bundle icicle) { + if (icicle != null && ProcessUtil.isPluginProcess()) { + if (icicle.getParcelable("android:support:fragments") != null) { + if (AnnotationProcessor.getPluginContainer(activity.getClass()) != null) { + // 加了注解的Activity正在自动恢复且页面包含了Fragment。直接清除fragment, + // 防止如果被恢复的fragment来自插件时,在某些情况下会使用宿主的classloader加载插件fragment + // 导致classnotfound问题 + icicle.clear(); + icicle = null; + } + } + //处理androidx的fragment缓存在activity自动恢复时导致的classcast问题 + //androidx.fragment.app.FragmentActivity + //-->androidx.activity.ComponentActivity + //---->androidx.savedstate.SavedStateRegistryController + //------>androidx.savedstate.SavedStateRegistry.SAVED_COMPONENTS_KEY + if (icicle.getParcelable("androidx.lifecycle.BundlableSavedStateRegistry.key") != null) { + icicle.clear(); + icicle = null; + } + } + + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + PluginInjector.injectActivityContext(activity); + + Intent intent = activity.getIntent(); + + if (intent != null) { + intent.setExtrasClassLoader(activity.getClassLoader()); + } + + if (icicle != null) { + icicle.setClassLoader(activity.getClassLoader()); + } + + if (ProcessUtil.isPluginProcess()) { + + installPluginViewFactory(activity); + + if (activity instanceof WaitForLoadingPluginActivity) { + //NOTHING + } else { + AndroidWebkitWebViewFactoryProvider.switchWebViewContext(activity); + } + + if (activity.isChild()) { + //修正TabActivity中的Activity的ContextImpl的packageName + Context base = activity.getBaseContext(); + while(base instanceof ContextWrapper) { + base = ((ContextWrapper)base).getBaseContext(); + } + if (HackContextImpl.instanceOf(base)) { + HackContextImpl impl = new HackContextImpl(base); + String packageName = FairyGlobal.getHostApplication().getPackageName(); + String packageName1 = activity.getPackageName(); + impl.setBasePackageName(packageName); + impl.setOpPackageName(packageName); + } + } + } + + try { + real.callActivityOnCreate(activity, icicle); + } catch (RuntimeException e) { + throw new RuntimeException( + " activity : " + activity.getClassLoader() + + " pluginContainer : " + AnnotationProcessor.getPluginContainer(activity.getClass()) + + ", process : " + ProcessUtil.isPluginProcess(), e); + } + + monitor.onActivityCreate(activity); + + } + + private void installPluginViewFactory(Activity activity) { + PluginContainer container = AnnotationProcessor.getPluginContainer(activity.getClass()); + // 如果配置了插件容器注解,安装PluginViewFactory,用于支持 + if (container != null) { + new PluginViewFactory(activity, activity.getWindow(), new PluginViewCreator()).installViewFactory(); + } + } + + @Override + public void callActivityOnDestroy(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + monitor.onActivityDestory(activity); + + real.callActivityOnDestroy(activity); + } + + @Override + public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + if (savedInstanceState != null) { + savedInstanceState.setClassLoader(activity.getClassLoader()); + // defined by Activity.java + final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState"; + Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG); + if (windowState != null) { + windowState.setClassLoader(activity.getClassLoader()); + } + } + + real.callActivityOnRestoreInstanceState(activity, savedInstanceState); + } + + @Override + public void callActivityOnPostCreate(Activity activity, Bundle icicle) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + if (icicle != null) { + icicle.setClassLoader(activity.getClassLoader()); + } + + real.callActivityOnPostCreate(activity, icicle); + } + + @Override + public void callActivityOnNewIntent(Activity activity, Intent intent) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + if (intent != null) { + intent.setExtrasClassLoader(activity.getClassLoader()); + } + + real.callActivityOnNewIntent(activity, intent); + } + + @Override + public void callActivityOnStart(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnStart(activity); + } + + @Override + public void callActivityOnRestart(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnRestart(activity); + } + + @Override + public void callActivityOnResume(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnResume(activity); + + monitor.onActivityResume(activity); + } + + @Override + public void callActivityOnStop(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnStop(activity); + } + + @Override + public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + + if (outState != null) { + outState.setClassLoader(activity.getClassLoader()); + } + + real.callActivityOnSaveInstanceState(activity, outState); + } + + @Override + public void callActivityOnPause(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnPause(activity); + + monitor.onActivityPause(activity); + } + + @Override + public void callActivityOnUserLeaving(Activity activity) { + PluginInjector.injectInstrumetionFor360Safe(activity, this); + real.callActivityOnUserLeaving(activity); + } + + public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivity(who, contextThread, token, target, + intent, requestCode, options); + } + + public void execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, + Intent[] intents, Bundle options) { + + PluginIntentResolver.resolveActivity(intents); + + hackInstrumentation.execStartActivities(who, contextThread, token, target, intents, options); + } + + public void execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, + Intent[] intents, Bundle options, int userId) { + + PluginIntentResolver.resolveActivity(intents); + + hackInstrumentation.execStartActivitiesAsUser(who, contextThread, token, target, intents, options, userId); + } + + public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, + Fragment target, Intent intent, int requestCode, Bundle options) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivity(who, contextThread, token, target, intent, requestCode, options); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, UserHandle user) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivity(who, contextThread, token, target, intent, requestCode, options, user); + } + + + ///////////// Android 4.0.4及以下 /////////////// + + public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivity(who, contextThread, token, target, intent, requestCode); + } + + public void execStartActivities(Context who, IBinder contextThread, + IBinder token, Activity target, Intent[] intents) { + PluginIntentResolver.resolveActivity(intents); + + hackInstrumentation.execStartActivities(who, contextThread, token, target, intents); + } + + public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Fragment target, + Intent intent, int requestCode) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivity(who, contextThread, token, target, intent, requestCode); + } + + /////// For Android 5.1 + public ActivityResult execStartActivityAsCaller( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, int userId) { + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivityAsCaller(who, contextThread, token, target, intent, requestCode, options, userId); + } + + public void execStartActivityFromAppTask( + Context who, IBinder contextThread, Object appTask, + Intent intent, Bundle options) { + + PluginIntentResolver.resolveActivity(intent); + + hackInstrumentation.execStartActivityFromAppTask(who, contextThread, appTask, intent, options); + } + + //7.1? + public ActivityResult execStartActivityAsCaller(Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, boolean ignoreTargetSecurity, + int userId) { + + PluginIntentResolver.resolveActivity(intent); + + return hackInstrumentation.execStartActivityAsCaller(who, contextThread, token, target, intent, requestCode, options, ignoreTargetSecurity, userId); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginIntentResolver.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginIntentResolver.java new file mode 100644 index 00000000..d9099d00 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginIntentResolver.java @@ -0,0 +1,305 @@ +package com.limpoxe.fairy.core; + +import static com.limpoxe.fairy.manager.PluginCallback.ACTION_PLUGIN_CHANGED; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; + +import com.limpoxe.fairy.content.PluginActivityInfo; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginReceiverIntent; +import com.limpoxe.fairy.core.android.HackCreateServiceData; +import com.limpoxe.fairy.core.android.HackReceiverData; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProviderClient; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.util.ArrayList; +import java.util.Iterator; + +public class PluginIntentResolver { + + public static final String CLASS_SEPARATOR = "@";//字符串越短,判断时效率越高 + public static final String CLASS_PREFIX_RECEIVER = "#";//字符串越短,判断时效率越高 + public static final String CLASS_PREFIX_SERVICE = "%";//字符串越短,判断时效率越高 + + public static final String CLASS_PREFIX_RECEIVER_NOT_FOUND = CLASS_PREFIX_RECEIVER + "NOT_FOUND"; + public static final String CLASS_PREFIX_SERVICE_NOT_FOUND = CLASS_PREFIX_SERVICE + "NOT_FOUND"; + + public static void resolveService(Intent intent) { + ArrayList classNameList = matchPlugin(intent, PluginDescriptor.SERVICE); + if (classNameList != null && classNameList.size() > 0) { + //TODO 只取第一个,忽略了多Service匹配到同一个Intent的情况 + if (classNameList.size() > 1) { + LogUtil.w("只取第一个,忽略了多Service匹配到同一个Intent的情况"); + } + String stubServiceName = PluginManagerProviderClient.bindStubService(classNameList.get(0)); + if (stubServiceName != null) { + intent.setComponent(new ComponentName(FairyGlobal.getHostApplication().getPackageName(), stubServiceName)); + } + } else { + if (intent.getComponent() != null && null != PluginManagerHelper.getPluginDescriptorByPluginId(intent.getComponent().getPackageName())) { + intent.setComponent(new ComponentName(FairyGlobal.getHostApplication().getPackageName(), intent.getComponent().getClassName())); + } + } + } + + public static ArrayList resolveReceiver(final Intent intent) { + // 如果在插件中发现了匹配intent的receiver项目,替换掉ClassLoader + // 不需要在这里记录目标className,className将在Intent中传递 + ArrayList result = new ArrayList(); + ArrayList classNameList = matchPlugin(intent, PluginDescriptor.BROADCAST); + if (classNameList != null && classNameList.size() > 0) { + for(String className: classNameList) { + Intent newIntent = new Intent(intent); + newIntent.setComponent(new ComponentName(FairyGlobal.getHostApplication().getPackageName(), + //因为此时已经在插件中匹配到intent, + //此时不用关心原intent是不是精确匹配的intent,到了这一步时,是将目标替换为stub还是exact已经无所谓了, + // resolveReceiverForClassLoader都可以拿到真实classname + //只需给出默认的stubReceiver即可,因此这里的参数使用null + PluginManagerProviderClient.bindStubReceiver(null))); + //hackReceiverForClassLoader检测到这个标记后会进行替换 + newIntent.setAction(className + CLASS_SEPARATOR + (intent.getAction() == null ? "" : intent.getAction())); + result.add(newIntent); + } + } else { + if (intent.getComponent() != null && null != PluginManagerHelper.getPluginDescriptorByPluginId(intent.getComponent().getPackageName())) { + //如果intent是指向插件的,但是matchPlugin又没有找到目标,这时强行修正intent指向宿主 + intent.setComponent(new ComponentName(FairyGlobal.getHostApplication().getPackageName(), intent.getComponent().getClassName())); + } + } + + //fix 插件中对同一个广播同时注册了动态和静态广播的情况 + result.add(intent); + + return result; + } + + /* package */static Context resolveReceiverForClassLoader(final Object msgObj) { + + if (ProcessUtil.isPluginProcess()) { + + PluginInjector.hackHostClassLoaderIfNeeded(); + + HackReceiverData hackReceiverData = new HackReceiverData(msgObj); + final Intent intent = hackReceiverData.getIntent(); + //className要么是真组件,要么是stub,要么是exact的组件 + String className = intent.getComponent().getClassName(); + //当是stub或者exact时,需要处理className,供classloader使用 + if (PluginManagerProviderClient.isStub(className)) { + String realReceiverClassName = null; + String[] targetClassName = null; + if (PluginManagerProviderClient.isExact(className, PluginDescriptor.BROADCAST)) { + realReceiverClassName = className; + } else { + String action = intent.getAction(); + if (action != null) { + targetClassName = action.split(CLASS_SEPARATOR); + realReceiverClassName = targetClassName[0]; + } + } + if (realReceiverClassName == null) { + // Intent的目标Component是Stub, + // 但是没用找到对应的插件, + // 正常情况下后续流程会抛出Stub ClassNotFound, + // 这里加容错防crash + LogUtil.w("返回容错标记, 交给HostClassLoader处理"); + intent.setComponent(new ComponentName(intent.getComponent().getPackageName(), CLASS_PREFIX_RECEIVER_NOT_FOUND)); + hackReceiverData.getInfo().name = intent.getComponent().getClassName(); + return null; + } + + @SuppressWarnings("rawtypes") + Class clazz = PluginLoader.loadPluginClassByName(realReceiverClassName); + + if (clazz != null) { + intent.setExtrasClassLoader(clazz.getClassLoader()); + if (targetClassName != null) { + //由于之前intent被修改过 这里再吧Intent还原到原始的intent + if (targetClassName.length > 1) { + intent.setAction(targetClassName[1]); + } else {//length等于1的情况是因为原始的intent可能不是通过Action过来的,而是直接通过Component过来的 + intent.setAction(null); + } + } else { + //isExact 无需对intent进行恢复 + } + + // HostClassLoader检测到这个特殊标记后会进行替换,得到真实的className + intent.setComponent(new ComponentName(intent.getComponent().getPackageName(), CLASS_PREFIX_RECEIVER + realReceiverClassName)); + // TODO 部分9.0的设备上,改name没用??HMA-AL00 JSN-AL00? Redmi Note 7 Pro;Redmi K20 Pro; EML-AL00; MI 6X; + hackReceiverData.getInfo().name = intent.getComponent().getClassName(); + + //v0.0.58以后4.x的系统上需要setIntent,否则反序列化对象可能出现classloader问题 + //if (Build.VERSION.SDK_INT >= 21) { + if (intent.getExtras() != null) { + hackReceiverData.setIntent(new PluginReceiverIntent(intent)); + } + //} + return PluginLoader.getDefaultPluginContext(clazz); + } else { + // Intent的目标Component是Stub, + // 但是没用找到对应的插件, + // 正常情况下后续流程会抛出Stub ClassNotFound, + // 这里加容错防crash + LogUtil.w("返回容错标记, 交给HostClassLoader处理"); + intent.setComponent(new ComponentName(intent.getComponent().getPackageName(), CLASS_PREFIX_RECEIVER_NOT_FOUND)); + hackReceiverData.getInfo().name = intent.getComponent().getClassName(); + return null; + } + } + } + return null; + } + + /* package */static String resolveServiceForClassLoader(Object msgObj) { + + HackCreateServiceData hackCreateServiceData = new HackCreateServiceData(msgObj); + ServiceInfo info = hackCreateServiceData.getInfo(); + + if (info == null) { + LogUtil.e("反射失败?"); + return null; + } + + if (ProcessUtil.isPluginProcess()) { + + PluginInjector.hackHostClassLoaderIfNeeded(); + + //通过映射查找 + String targetClassName = PluginManagerProviderClient.getBindedPluginServiceName(info.name); + //TODO 或许可以通过这个方式来处理service + //info.applicationInfo = XXX + + LogUtil.v("hackServiceName", info.name, info.packageName, info.processName, "targetClassName", targetClassName, info.applicationInfo.packageName); + + if (targetClassName != null) { + info.name = CLASS_PREFIX_SERVICE + targetClassName; + } else if (PluginManagerProviderClient.isStub(info.name)) { + String dumpString = PluginManagerProviderClient.dumpServiceInfo(); + LogUtil.w("没有找到映射关系, 可能映射表出了异常", info.name, dumpString); + LogUtil.w("返回容错标记, 交给HostClassLoader处理"); + info.name = CLASS_PREFIX_SERVICE_NOT_FOUND; + } else { + LogUtil.v("是宿主service", info.name); + } + } + + return info.name; + } + + public static void resolveActivity(final Intent intent) { + // 如果在插件中发现Intent的匹配项,记下匹配的插件Activity的ClassName + ArrayList classNameList = matchPlugin(intent, PluginDescriptor.ACTIVITY); + if (classNameList != null && classNameList.size() > 0) { + //TODO 只取第一个,忽略了多Activity匹配到同一个Intent的情况 + if (classNameList.size() > 1) { + LogUtil.w("只取第一个,忽略了多Activity匹配到同一个Intent的情况"); + } + String className = classNameList.get(0); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + + PluginActivityInfo pluginActivityInfo = pluginDescriptor.getActivityInfos().get(className); + + String stubActivityName = PluginManagerProviderClient.bindStubActivity(className, + (int)Long.parseLong(pluginActivityInfo.getLaunchMode()), + pluginDescriptor.getPackageName(), + pluginActivityInfo.getTheme(), + pluginActivityInfo.getScreenOrientation()); + + if (stubActivityName == null) { + LogUtil.e("绑定StubAtivity失败", + className, + (int)Long.parseLong(pluginActivityInfo.getLaunchMode()), + pluginDescriptor.getPackageName(), + pluginActivityInfo.getTheme(), + pluginActivityInfo.getScreenOrientation()); + return; + } else { + LogUtil.v("绑定StubAtivity成功", className); + } + + intent.setComponent( + new ComponentName(FairyGlobal.getHostApplication().getPackageName(), stubActivityName)); + //PluginInstrumentationWrapper检测到这个标记后会进行替换 + intent.setAction(className + CLASS_SEPARATOR + (intent.getAction()==null?"":intent.getAction())); + } else { + if (intent.getComponent() != null) { + //如果没有匹配到,但是intent里面指定的packageName是插件的,强行修正packageName + String targetPackageName = intent.getComponent().getPackageName(); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(targetPackageName); + if (pluginDescriptor != null) { + intent.setComponent(new ComponentName(FairyGlobal.getHostApplication().getPackageName(), intent.getComponent().getClassName())); + } + } + } + } + + /* package */static void resolveActivity(Intent[] intent) { + // 不常用。需要时再实现此方法, + } + + /** + */ + public static ArrayList matchPlugin(Intent intent, int type) { + + //快速排除一些确定的非插件Intent + if (intent.getAction() != null && ( + intent.getAction().endsWith(".STUB_DEFAULT") || + intent.getAction().endsWith(".STUB_EXACT") || + intent.getAction().contains(CLASS_SEPARATOR) || + intent.getAction().equals(ACTION_PLUGIN_CHANGED))) { + return null; + } + + LogUtil.v("开始尝试匹配插件Intent"); + + ArrayList result = null; + + String packageName = intent.getPackage(); + if (packageName == null && intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } + //如果指定了packname,就不用遍历插件列表了 + if (packageName != null && !packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + if (pluginDescriptor != null) { + result = pluginDescriptor.matchPlugin(intent, type); + if (result != null && result.size() > 0) { + LogUtil.v(packageName, "插件Intent匹配成功"); + } else { + LogUtil.w(packageName, "目标是插件,但在插件Maniest中未找到匹配的IntentFilter", type); + } + } else { + LogUtil.w(packageName, "目标不是插件,也可能是插件未正确安装"); + } + } else { + //没有指定packageName,开始遍历插件列表 + ArrayList pluginList = PluginManagerHelper.getPlugins(); + LogUtil.v("已安装插件数量", pluginList.size()); + Iterator itr = pluginList.iterator(); + while (itr.hasNext()) { + PluginDescriptor pluginDescriptor = itr.next(); + LogUtil.v("正在匹配插件", pluginDescriptor.getPackageName()); + ArrayList list = pluginDescriptor.matchPlugin(intent, type); + if (list != null) { + if (result == null) { + result = new ArrayList<>(); + } + result.addAll(list); + } + } + + if (result == null || result.size() == 0) { + LogUtil.v("未匹配到插件Intent, 说明目标不是插件,也可能是插件未正确安装", packageName, intent.toString()); + } else { + LogUtil.v(packageName, "插件Intent匹配成功"); + } + } + return result; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLauncher.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLauncher.java new file mode 100644 index 00000000..9859bab1 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLauncher.java @@ -0,0 +1,514 @@ +package com.limpoxe.fairy.core; + + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; +import android.app.Application.ActivityLifecycleCallbacks; +import android.app.Instrumentation; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.android.HackActivityThread; +import com.limpoxe.fairy.core.android.HackActivityThreadProviderClientRecord; +import com.limpoxe.fairy.core.android.HackAndroidXLocalboarcastManager; +import com.limpoxe.fairy.core.android.HackApplication; +import com.limpoxe.fairy.core.android.HackContentProvider; +import com.limpoxe.fairy.core.android.HackSupportV4LocalboarcastManager; +import com.limpoxe.fairy.core.compat.CompatForFragmentClassCache; +import com.limpoxe.fairy.core.compat.CompatForSupportv7ViewInflater; +import com.limpoxe.fairy.core.compat.CompatForWebViewFactoryApi21; +import com.limpoxe.fairy.core.localservice.LocalServiceManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidWebkitWebViewFactoryProvider; +import com.limpoxe.fairy.manager.PluginActivityMonitor; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + *
+ * @author cailiming
+ * 
+ * + */ +public class PluginLauncher implements Serializable { + + private static PluginLauncher runtime; + + private ConcurrentHashMap loadedPluginMap = new ConcurrentHashMap(); + + private PluginLauncher() { + if (!ProcessUtil.isPluginProcess()) { + throw new IllegalAccessError("本类仅在插件进程使用"); + } + } + + public static PluginLauncher instance() { + if (runtime == null) { + synchronized (PluginLauncher.class) { + if (runtime == null) { + runtime = new PluginLauncher(); + } + } + } + return runtime; + } + + public LoadedPlugin getRunningPlugin(String packageName) { + return loadedPluginMap.get(packageName); + } + + public LoadedPlugin startPlugin(String packageName) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + if (pluginDescriptor != null) { + return startPlugin(pluginDescriptor); + } else { + LogUtil.e("插件未找到", packageName); + } + return null; + } + + public LoadedPlugin startPlugin(final PluginDescriptor pluginDescriptor) { + LoadedPlugin plugin = getRunningPlugin(pluginDescriptor.getPackageName()); + if (plugin != null) { + return plugin; + } + //当前线程不可以持有PluginManagerService.mLock的锁,否则可能会造成死锁导致ANR + return SyncRunnable.runOnMainSync(new Runner() { + @Override + public LoadedPlugin run() { + LoadedPlugin plugin = getRunningPlugin(pluginDescriptor.getPackageName()); + if (plugin != null) { + return plugin; + } + LogUtil.w("startPlugin", pluginDescriptor.getPackageName()); + long startAt = System.currentTimeMillis(); + LogUtil.w("正在初始化插件 " + pluginDescriptor.getPackageName() + ": Resources, DexClassLoader, Context, Application"); + LogUtil.w("插件信息", pluginDescriptor.getVersion(), pluginDescriptor.getInstalledPath()); + Resources pluginRes = PluginCreator.createPluginResource( + FairyGlobal.getHostApplication().getApplicationInfo().sourceDir, + FairyGlobal.getHostApplication().getResources(), pluginDescriptor); + if (pluginRes == null) { + postRemove(pluginDescriptor); + return null; + } + + long t1 = System.currentTimeMillis(); + LogUtil.w("初始化插件资源耗时:" + (t1 - startAt)); + + ClassLoader pluginClassLoader = PluginCreator.createPluginClassLoader( + pluginDescriptor.getPackageName(), + pluginDescriptor.getInstalledPath(), + pluginDescriptor.getDalvikCacheDir(), + pluginDescriptor.getNativeLibDir(), + pluginDescriptor.isStandalone(), + pluginDescriptor.getDependencies(), + pluginDescriptor.getMuliDexList()); + + long t12 = System.currentTimeMillis(); + LogUtil.w("初始化插件DexClassLoader耗时:" + (t12 - t1)); + + PluginContextTheme pluginContext = (PluginContextTheme)PluginCreator.createPluginContext( + pluginDescriptor, + FairyGlobal.getHostApplication().getBaseContext(), + pluginRes, + pluginClassLoader); + + //插件Context默认主题设置为插件application主题 + pluginContext.setTheme(pluginDescriptor.getApplicationTheme()); + + long t13 = System.currentTimeMillis(); + LogUtil.w("初始化插件Theme耗时:" + (t13 - t12)); + + plugin = new LoadedPlugin(pluginDescriptor.getPackageName(), + pluginDescriptor.getInstalledPath(), + pluginContext, + pluginClassLoader); + + //inflate data in meta-data + PluginDescriptor.inflateMetaData(pluginDescriptor, pluginRes); + + Application application = initApplication(pluginContext, pluginClassLoader, pluginRes, pluginDescriptor, plugin); + if (application == null) { + postRemove(pluginDescriptor); + return null; + } + + LogUtil.w("add to loadedPluginMap", pluginDescriptor.getPackageName()); + loadedPluginMap.put(pluginDescriptor.getPackageName(), plugin); + return plugin; + } + }); + } + + private void postRemove(final PluginDescriptor pluginDescriptor) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + LogUtil.e("初始化插件失败: " + pluginDescriptor.getPackageName()); + LogUtil.e("插件文件可能已损坏,卸载此插件 " + pluginDescriptor.getPackageName()); + PluginManagerHelper.remove(pluginDescriptor.getPackageName()); + LogUtil.e("卸载完成"); + } + }); + } + + private Application initApplication(Context pluginContext, ClassLoader pluginClassLoader, Resources pluginRes, PluginDescriptor pluginDescriptor, LoadedPlugin plugin) { + long t1 = System.currentTimeMillis(); + Application pluginApplication = callPluginApplicationOnCreate(pluginContext, pluginClassLoader, pluginDescriptor); + if (pluginApplication != null) { + plugin.pluginApplication = pluginApplication;//这里之所以不放在LoadedPlugin的构造器里面,是因为contentprovider在安装时loadclass,造成死循环 + plugin.applicationOnCreateCalled = true; + try { + HackActivityThread.installPackageInfo(FairyGlobal.getHostApplication(), pluginDescriptor.getPackageName(), pluginDescriptor, + pluginClassLoader, pluginRes, pluginApplication); + } catch (ClassNotFoundException e) { + LogUtil.printException("PluginLauncher.initApplication", e); + } + // 解决插件中webview加载html时控件出错的问题,兼容性待验证 + CompatForWebViewFactoryApi21.addWebViewAssets(pluginRes.getAssets()); + } + long t2 = System.currentTimeMillis(); + LogUtil.i("初始化插件Application: " + pluginDescriptor.getPackageName() + " " + pluginDescriptor.getApplicationName() + " 耗时:" + (t2 - t1)); + return pluginApplication; + } + + private Application callPluginApplicationOnCreate(Context pluginContext, ClassLoader classLoader, PluginDescriptor pluginDescriptor) { + LogUtil.d("创建插件Application", pluginDescriptor.getApplicationName()); + Application pluginApplication = null; + try { + //为了支持插件中使用multidex + ((PluginContextTheme)pluginContext).setCrackPackageManager(true); + + pluginApplication = Instrumentation.newApplication(classLoader.loadClass(pluginDescriptor.getApplicationName()), + pluginContext); + + //为了支持插件中使用multidex + ((PluginContextTheme)pluginContext).setCrackPackageManager(false); + } catch (Exception e) { + //java.io.IOException: Failed to find magic in xxx.apk + //Error openning archive xxx.apk: Invalid file + //Failed to open Zip archive xxx.apk + LogUtil.e("newApplication fail"); + return null; + } + + ((PluginContextTheme)pluginContext).setPluginApplication(pluginApplication); + + //安装ContentProvider, 在插件Application对象构造以后,oncreate调用之前 + PluginInjector.installContentProviders(FairyGlobal.getHostApplication(), pluginApplication, pluginDescriptor.getProviderInfos().values()); + + LogUtil.v("屏蔽插件中的UncaughtExceptionHandler"); + // 1、先拿到宿主的crashHandler + Thread.UncaughtExceptionHandler old = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(null); + + //2、执行onCreate + try { + pluginApplication.onCreate(); + } catch (final Exception e) { + LogUtil.printException("call plugin Application onCreate failed, must crash!", e); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + LogUtil.e("call plugin Application onCreate failed, must crash!"); + throw e; + } + }); + return null; + } + + // 3、再还原宿主的crashHandler,这里之所以需要还原CrashHandler, + // 是因为如果插件中自己设置了自己的crashHandler(通常是在oncreate中), + // 会导致当前进程的主线程的handler被意外修改。 + // 如果有多个插件都有设置自己的crashHandler,也会导致混乱 + Thread.UncaughtExceptionHandler pluginExHandler = Thread.getDefaultUncaughtExceptionHandler(); + if (old == null && pluginExHandler == null) { + //do nothing + } else if (old == null && pluginExHandler != null) { + UncaugthExceptionWrapper handlerWrapper = new UncaugthExceptionWrapper(); + handlerWrapper.addHandler(pluginDescriptor.getPackageName(), pluginExHandler); + Thread.setDefaultUncaughtExceptionHandler(handlerWrapper); + } else if (old != null && pluginExHandler == null) { + Thread.setDefaultUncaughtExceptionHandler(old); + } else if (old != null && pluginExHandler != null) { + if (old == pluginExHandler) { + //do nothing + } else { + if (old instanceof UncaugthExceptionWrapper) { + ((UncaugthExceptionWrapper) old).addHandler(pluginDescriptor.getPackageName(), pluginExHandler); + Thread.setDefaultUncaughtExceptionHandler(old); + } else { + //old是宿主设置和handler + UncaugthExceptionWrapper handlerWrapper = new UncaugthExceptionWrapper(); + handlerWrapper.setHostHandler(old); + handlerWrapper.addHandler(pluginDescriptor.getPackageName(), pluginExHandler); + + Thread.setDefaultUncaughtExceptionHandler(handlerWrapper); + } + } + } + + if (Build.VERSION.SDK_INT >= 14) { + // ActivityLifecycleCallbacks 的回调实际是由Activity内部在自己的声明周期函数内主动调用application的注册的callback触发的 + //由于我们把插件Activity内部的application成员变量替换调用了 会导致不会触发宿主中注册的ActivityLifecycleCallbacks + //那么我们在这里给插件的Application对象注册一个callback bridge。将插件的call发给宿主的call, + //从而使得宿主application中注册的callback能监听到插件Activity的声明周期 + pluginApplication.registerActivityLifecycleCallbacks(new LifecycleCallbackBridge(FairyGlobal.getHostApplication())); + } else { + //对于小于14的版本,影响是,StubActivity的绑定关系不能被回收, + // 意味着宿主配置的非Stand的StubActivity的个数不能小于插件中对应的类型的个数的总数,否则可能会出现找不到映射的StubActivity + } + + return pluginApplication; + } + + public void stopPlugin(String packageName, PluginDescriptor pluginDescriptor) { + LogUtil.w("停止插件", packageName); + if (pluginDescriptor == null) { + LogUtil.w("插件不存在", packageName); + return; + } + final LoadedPlugin plugin = getRunningPlugin(packageName); + if (plugin == null) { + LogUtil.w("插件未运行", packageName); + return; + } + //退出LocalService + LogUtil.d("退出LocalService"); + LocalServiceManager.unRegistService(pluginDescriptor); + //TODO 还要通知宿主进程退出localService,不过不通知其实本身也不会坏影响。 + + //退出Activity + LogUtil.d("退出Activity"); + Intent stopPluginIntent = new Intent(plugin.pluginPackageName + PluginActivityMonitor.ACTION_STOP_PLUGIN); + stopPluginIntent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + FairyGlobal.getHostApplication().sendBroadcast(stopPluginIntent); + + //退出 LocalBroadcastManager + LogUtil.d("退出LocalBroadcastManager"); + Object mInstance = HackSupportV4LocalboarcastManager.getInstance(); + if (mInstance != null) { + HackSupportV4LocalboarcastManager hackSupportV4LocalboarcastManager = new HackSupportV4LocalboarcastManager(mInstance); + HashMap> mReceivers = hackSupportV4LocalboarcastManager.getReceivers(); + if (mReceivers != null) { + Iterator ir = mReceivers.keySet().iterator(); + ArrayList needRemoveList = new ArrayList<>(); + while(ir.hasNext()) { + BroadcastReceiver item = ir.next(); + if (item.getClass().getClassLoader() == plugin.pluginClassLoader.getParent() //RealPluginClassLoader + || (item.getClass().getClassLoader() instanceof RealPluginClassLoader + && ((RealPluginClassLoader)item.getClass().getClassLoader()).pluginPackageName.equals(plugin.pluginPackageName))) {//RealPluginClassLoader, 也有可能不是同一个实例 + needRemoveList.add(item); + } + } + for(BroadcastReceiver broadcastReceiver : needRemoveList) { + LogUtil.e("SupportV4 unregisterReceiver", broadcastReceiver.getClass().getName()); + hackSupportV4LocalboarcastManager.unregisterReceiver(broadcastReceiver); + } + } + } + Object mInstanceX = HackAndroidXLocalboarcastManager.getInstance(); + if (mInstanceX != null) { + HackAndroidXLocalboarcastManager hackAndroidXLocalboarcastManager = new HackAndroidXLocalboarcastManager(mInstanceX); + HashMap mReceivers = hackAndroidXLocalboarcastManager.getReceivers(); + if (mReceivers != null) { + Iterator ir = mReceivers.keySet().iterator(); + ArrayList needRemoveList = new ArrayList<>(); + while(ir.hasNext()) { + BroadcastReceiver item = ir.next(); + if (item.getClass().getClassLoader() == plugin.pluginClassLoader.getParent() //RealPluginClassLoader + || (item.getClass().getClassLoader() instanceof RealPluginClassLoader + && ((RealPluginClassLoader)item.getClass().getClassLoader()).pluginPackageName.equals(plugin.pluginPackageName))) {//RealPluginClassLoader, 也有可能不是同一个实例 + needRemoveList.add(item); + } + } + for(BroadcastReceiver broadcastReceiver : needRemoveList) { + LogUtil.e("AndroidX unregisterReceiver", broadcastReceiver.getClass().getName()); + hackAndroidXLocalboarcastManager.unregisterReceiver(broadcastReceiver); + } + } + } + + LogUtil.d("退出Service"); + //bindservie启动的service应该不需要处理,退出activity的时候会unbind + Map map = HackActivityThread.get().getServices(); + if (map != null) { + Collection list = map.values(); + for (Service s :list) { + if (s.getClass().getClassLoader() == plugin.pluginClassLoader.getParent() //RealPluginClassLoader + //这里判断是否是当前被stop的插件的组件时,与上面LocalBroadcast的判断逻辑时一样的 + //只不过sercie有getPackageName函数,所以不需要通过classloader的pluginPackageName来判断了 + || s.getPackageName().equals(plugin.pluginPackageName)) { + Intent intent = new Intent(); + intent.setClassName(plugin.pluginPackageName, s.getClass().getName()); + s.stopService(intent); + } + } + } + + //退出AssetManager + //pluginDescriptor.getPluginContext().getResources().getAssets().close(); + + LogUtil.d("退出ContentProvider"); + HashMap pluginProviderMap = pluginDescriptor.getProviderInfos(); + if (pluginProviderMap != null) { + HackActivityThread hackActivityThread = HackActivityThread.get(); + // The lock of mProviderMap protects the following variables. + Map mProviderMap = hackActivityThread.getProviderMap(); + if (mProviderMap != null) { + + Map mLocalProviders = hackActivityThread.getLocalProviders(); + Map mLocalProvidersByName = hackActivityThread.getLocalProvidersByName(); + + Collection collection = pluginProviderMap.values(); + for(PluginProviderInfo pluginProviderInfo : collection) { + String auth = pluginProviderInfo.getAuthority(); + synchronized (mProviderMap) { + removeProvider(auth, mProviderMap); + removeProvider(auth, mLocalProviders); + removeProvider(auth, mLocalProvidersByName); + } + } + } + } + + LogUtil.d("清理fragment class 缓存"); + //即退出由FragmentManager保存的Fragment + CompatForSupportv7ViewInflater.clearViewInflaterConstructorCache(); + CompatForFragmentClassCache.clearFragmentClassCache(); + CompatForFragmentClassCache.clearSupportV4FragmentClassCache(); + CompatForFragmentClassCache.clearAndroidXFragmentClassCache(); + + LogUtil.d("移除插件注册的crashHandler"); + //这里不一定能清理干净,因为UncaugthExceptionWrapper可能会被创建多个实例。不过也没什么大的影响 + Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + if (exceptionHandler instanceof UncaugthExceptionWrapper) { + ((UncaugthExceptionWrapper) exceptionHandler).removeHandler(packageName); + } + + loadedPluginMap.remove(packageName); + + //需要在UI线程运行 + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + LogUtil.d("还原WebView Context"); + AndroidWebkitWebViewFactoryProvider.switchWebViewContext(FairyGlobal.getHostApplication()); + } catch (Exception e) { + LogUtil.printException("stopPlugin", e); + } + try { + //退出BroadcastReceiver + //广播一般有个注册方式 + //1、activity、service注册 + // 这种方式,在上一步Activitiy、service退出时会自然退出,所以不用处理 + //2、application注册 + // 这里需要处理这种方式注册的广播,这种方式注册的广播会被PluginContextTheme对象记录下来 + LogUtil.v("退出BroadcastReceiver"); + if (plugin.pluginApplication != null) { + ((PluginContextTheme) plugin.pluginApplication.getBaseContext()).unregisterAllReceiver(); + } + } catch (Exception e) { + LogUtil.printException("stopPlugin", e); + } + try { + //给插件一个机会自己做一些清理工作 + LogUtil.d("调用插件Application.onTerminate()"); + plugin.pluginApplication.onTerminate(); + } catch (Exception e) { + LogUtil.printException("stopPlugin", e); + } + } + }); + } + + private static void removeProvider(String authority, Map map) { + if (map == null || authority == null) { + return; + } + Iterator iterator = map.entrySet().iterator(); + while(iterator.hasNext()) { + Map.Entry entry = iterator.next(); + ContentProvider contentProvider = new HackActivityThreadProviderClientRecord(entry.getValue()).getProvider(); + if (contentProvider != null && authority.equals(new HackContentProvider(contentProvider).getAuthority())) { + iterator.remove(); + LogUtil.e("remove plugin contentprovider from map for " + authority); + break; + } + } + } + + public boolean isRunning(String packageName) { + LoadedPlugin loadedPlugin = getRunningPlugin(packageName); + return loadedPlugin != null && loadedPlugin.applicationOnCreateCalled; + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + static class LifecycleCallbackBridge implements ActivityLifecycleCallbacks { + + private HackApplication hackPluginApplication; + + public LifecycleCallbackBridge(Application pluginApplication) { + this.hackPluginApplication = new HackApplication(pluginApplication); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + hackPluginApplication.dispatchActivityCreated(activity, savedInstanceState); + } + + @Override + public void onActivityStarted(Activity activity) { + hackPluginApplication.dispatchActivityStarted(activity); + } + + @Override + public void onActivityResumed(Activity activity) { + hackPluginApplication.dispatchActivityResumed(activity); + } + + @Override + public void onActivityPaused(Activity activity) { + hackPluginApplication.dispatchActivityPaused(activity); + } + + @Override + public void onActivityStopped(Activity activity) { + hackPluginApplication.dispatchActivityStopped(activity); + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + hackPluginApplication.dispatchActivitySaveInstanceState(activity, outState); + } + + @Override + public void onActivityDestroyed(Activity activity) { + hackPluginApplication.dispatchActivityDestroyed(activity); + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLoader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLoader.java new file mode 100644 index 00000000..ba122933 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginLoader.java @@ -0,0 +1,219 @@ +package com.limpoxe.fairy.core; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.android.HackLayoutInflater; +import com.limpoxe.fairy.core.compat.CompatForFragmentClassCache; +import com.limpoxe.fairy.core.compat.CompatForSupportv7ViewInflater; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidAppIActivityManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidAppINotificationManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidAppIPackageManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidOsServiceManager; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidWebkitWebViewFactoryProvider; +import com.limpoxe.fairy.core.proxy.systemservice.AndroidWidgetToast; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProviderClient; +import com.limpoxe.fairy.manager.mapping.StubActivityMappingProcessor; +import com.limpoxe.fairy.manager.mapping.StubReceiverMappingProcessor; +import com.limpoxe.fairy.manager.mapping.StubServiceMappingProcessor; +import com.limpoxe.fairy.util.FreeReflection; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +public class PluginLoader { + + private PluginLoader() { + } + + /** + * 初始化loader, 只可调用一次 + * + * @param app + */ + public static synchronized void initLoader(Application app) { + if (FairyGlobal.isInited()) { + return; + } + + LogUtil.v("插件框架初始化中..."); + long t1 = System.currentTimeMillis(); + + if (Build.VERSION.SDK_INT >= 28) { + boolean ret = FreeReflection.exemptAll(app); + LogUtil.v("hidden api exempt " + ret); + } + + FairyGlobal.setApplication(app); + FairyGlobal.registStubMappingProcessor(new StubActivityMappingProcessor()); + FairyGlobal.registStubMappingProcessor(new StubServiceMappingProcessor()); + FairyGlobal.registStubMappingProcessor(new StubReceiverMappingProcessor()); + + //这里的isPluginProcess方法需要在安装AndroidAppIActivityManager之前执行一次。 + //原因见AndroidAppIActivityManager的getRunningAppProcesses()方法 + boolean isPluginProcess = ProcessUtil.isPluginProcess(); + if(ProcessUtil.isPluginProcess()) { + AndroidOsServiceManager.installProxy(); + AndroidWidgetToast.installProxy(); + } + + AndroidAppIActivityManager.installProxy(); + AndroidAppINotificationManager.installProxy(); + AndroidAppIPackageManager.installProxy(FairyGlobal.getHostApplication().getPackageManager()); + + if (isPluginProcess) { + HackLayoutInflater.installPluginCustomViewConstructorCache(); + CompatForSupportv7ViewInflater.installPluginCustomViewConstructorCache(); + CompatForFragmentClassCache.installFragmentClassCache(); + CompatForFragmentClassCache.installSupportV4FragmentClassCache(); + CompatForFragmentClassCache.installAndroidXFragmentClassCache(); + //不可在主进程中同步安装,因为此时ActivityThread还没有准备好, 会导致空指针。 + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + AndroidWebkitWebViewFactoryProvider.installProxy(); + } + }); + } + + PluginInjector.injectHandlerCallback();//本来宿主进程是不需要注入handlecallback的,这里加上是为了对抗360安全卫士等软件,提高Instrumentation的成功率 + PluginInjector.injectInstrumentation(); + PluginInjector.injectBaseContext(FairyGlobal.getHostApplication()); + PluginInjector.injectAppComponentFactory(); + + if (isPluginProcess) { + if (Build.VERSION.SDK_INT >= 14) { + FairyGlobal.getHostApplication().registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityResumed(Activity activity) { + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + Intent intent = activity.getIntent(); + if (intent != null && intent.getComponent() != null) { + LogUtil.v("回收绑定关系"); + PluginManagerProviderClient.unBindLaunchModeStubActivity(intent.getComponent().getClassName(), activity.getClass().getName()); + } + } + }); + } + } + + FairyGlobal.setIsInited(true); + + long t2 = System.currentTimeMillis(); + LogUtil.w("插件框架初始化完成", "耗时:" + (t2-t1)); + } + + public static Context fixBaseContextForReceiver(Context superApplicationContext) { + if (superApplicationContext instanceof ContextWrapper) { + return ((ContextWrapper)superApplicationContext).getBaseContext(); + } else { + return superApplicationContext; + } + } + + + /** + * 根据插件中的classId加载一个插件中的class + * + * @param clazzId + * @return + */ + @SuppressWarnings("rawtypes") + public static Class loadPluginFragmentClassById(String clazzId) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByFragmentId(clazzId); + if (pluginDescriptor != null) { + String clazzName = pluginDescriptor.getPluginClassNameById(clazzId); + return loadPluginClassByName(pluginDescriptor, clazzName); + } else { + LogUtil.e("PluginDescriptor Not Found for classId ", clazzId); + } + return null; + + } + + @SuppressWarnings("rawtypes") + public static Class loadPluginClassByName(String clazzName) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(clazzName); + return loadPluginClassByName(pluginDescriptor, clazzName); + } + + public static Class loadPluginClassByName(PluginDescriptor pluginDescriptor, String clazzName) { + + if (pluginDescriptor != null && clazzName != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + return plugin.loadClassByName(clazzName); + } else { + LogUtil.e("Plugin is not running", clazzName); + } + } else { + LogUtil.e("loadPluginClass Fail for clazzName ", clazzName, pluginDescriptor==null?"pluginDescriptor = null":"pluginDescriptor not null"); + } + + return null; + } + + /** + * 获取当前class所在插件的Context + * 每个插件只有1个DefaultContext, + * 是当前插件中所有class公用的Context + * + * @param clazz + * @return + */ + public static Context getDefaultPluginContext(@SuppressWarnings("rawtypes") Class clazz) { + + Context pluginContext = null; + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(clazz.getName()); + + if (pluginDescriptor != null) { + LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + if (plugin != null) { + pluginContext = plugin.pluginContext;; + } else { + LogUtil.e("Plugin is not running", clazz.getName()); + } + } else { + LogUtil.e("PluginDescriptor Not Found for ", clazz.getName()); + } + + if (pluginContext == null) { + LogUtil.e("Context Not Found for ", clazz.getName()); + } + + return pluginContext; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginResourceWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginResourceWrapper.java new file mode 100644 index 00000000..05bf85fa --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginResourceWrapper.java @@ -0,0 +1,175 @@ +package com.limpoxe.fairy.core; + +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.lang.reflect.Field; +import java.util.HashSet; + +/** + * 根据不同的rom,可能需要重写更多的方法,目前发现的几个机型的问题暂时只需要重写下面2个方法。 + * + * @author cailiming + */ +public class PluginResourceWrapper extends Resources { + + private HashSet idCaches = new HashSet<>(5); + + private PluginDescriptor mPluginDescriptor; + + public PluginResourceWrapper(AssetManager assets, DisplayMetrics metrics, + Configuration config, PluginDescriptor pluginDescriptor) { + super(assets, metrics, config); + this.mPluginDescriptor = pluginDescriptor; + } + + @Override + public String getResourcePackageName(int resid) throws NotFoundException { + if (idCaches.contains(resid)) { + return FairyGlobal.getHostApplication().getPackageName(); + } + try { + return super.getResourcePackageName(resid); + } catch (NotFoundException e) { + LogUtil.e("NotFoundException Try Following", Integer.toHexString(resid)); + //就目前测试的情况来看,只有Coolpad、vivo、oppo等手机会在上面抛异常,走到这里来, + //华为、三星、小米等手机不会到这里来。 + if (!mPluginDescriptor.isStandalone() && ResourceUtil.isMainResId(resid)) { + idCaches.add(resid); + return FairyGlobal.getHostApplication().getPackageName(); + } + LogUtil.printStackTrace(); + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + } + + @Override + public String getResourceName(int resid) throws NotFoundException { + try { + return super.getResourceName(resid); + } catch (NotFoundException e) { + LogUtil.e("NotFoundException Try Following"); + //vivo + if (!mPluginDescriptor.isStandalone() && ResourceUtil.isMainResId(resid)) { + return FairyGlobal.getHostApplication().getResources().getResourceName(resid); + } + LogUtil.printStackTrace(); + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + } + + @Override + public String getResourceEntryName(int resid) { + try { + return super.getResourceEntryName(resid); + } catch (NotFoundException e) { + LogUtil.e("NotFoundException Try Following"); + //vivo + if (!mPluginDescriptor.isStandalone() && ResourceUtil.isMainResId(resid)) { + return FairyGlobal.getHostApplication().getResources().getResourceEntryName(resid); + } + LogUtil.printStackTrace(); + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + } + + @Override + public String getResourceTypeName(int resid) { + try { + return super.getResourceTypeName(resid); + } catch (NotFoundException e) { + LogUtil.e("NotFoundException Try Following"); + //vivo + if (!mPluginDescriptor.isStandalone() && ResourceUtil.isMainResId(resid)) { + return FairyGlobal.getHostApplication().getResources().getResourceTypeName(resid); + } + LogUtil.printStackTrace(); + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + } + + /** + * 重写这个方法主要是为了解决非独立插件可以反查宿主资源id的问题 + */ + @Override + public int getIdentifier(String name, String defType, String defPackage) { + + if (TextUtils.isDigitsOnly(name)) { + return super.getIdentifier(name, defType, defPackage); + } + + //传了packageName,而且不是宿主的packageName, 则直接返回 + if (!TextUtils.isEmpty(defPackage) && !FairyGlobal.getHostApplication().getPackageName().equals(defPackage)) { + return super.getIdentifier(name, defType, defPackage); + } + + //package:type/entry + //第一段 “package:“ 第二段 ”type/“ 第三段 “entry” + String packageName = null; + String type = null; + String entry = null; + + String[] pte = name.split(":"); + String[] te; + if (pte.length == 2) { + packageName = pte[0]; + te = pte[1].split("/"); + } else { + te = pte[0].split("/"); + } + + if (te.length == 2) { + type = te[0]; + entry = te[1]; + } else { + entry = te[0]; + } + + if (packageName == null) { + packageName = defPackage; + } + + if (type == null) { + type = defType; + } + + //传了宿主的packageName + if (FairyGlobal.getHostApplication().getPackageName().equals(packageName)) { + if (mPluginDescriptor.isStandalone()) { + //如果是独立插件, 取不到宿主资源, 这里强制切换到插件 + packageName = mPluginDescriptor.getPackageName(); + } else { + // 判断是否在真的在宿主中 + Class rClass = null; + try { + String className = packageName + ".R$" + type; + rClass = this.getClass().getClassLoader().loadClass(className); + Field field = rClass.getDeclaredField(entry); + if (field == null) { + //不在宿主中,切换到插件 + packageName = mPluginDescriptor.getPackageName(); + } else { + //在宿主中, 通过宿主的Context获取 + return FairyGlobal.getHostApplication().getResources().getIdentifier(entry, type, packageName); + } + } catch (Exception e) { + //不在宿主中,切换到插件 + packageName = mPluginDescriptor.getPackageName(); + } + } + } + return super.getIdentifier(entry, type, packageName); + } +} + diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginThemeHelper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginThemeHelper.java new file mode 100644 index 00000000..8633ac54 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginThemeHelper.java @@ -0,0 +1,97 @@ +package com.limpoxe.fairy.core; + +import android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Field; +import java.util.HashMap; + +public class PluginThemeHelper { + + public static int getPluginThemeIdByName(String pluginId, String themeName) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if (pluginDescriptor != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + return plugin.pluginResource.getIdentifier(themeName, "style", pluginDescriptor.getPackageName()); + } + } + return 0; + } + + public static HashMap getAllPluginThemes(String pluginId) { + HashMap themes = new HashMap(); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if (pluginDescriptor != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + try { + Class pluginRstyle = plugin.pluginClassLoader.loadClass(pluginId + ".R$style"); + if (pluginRstyle != null) { + Field[] fields = pluginRstyle.getDeclaredFields(); + if (fields != null) { + for (Field field : fields) { + field.setAccessible(true); + if (field.getType().isPrimitive()) { + int themeResId = field.getInt(null); + themes.put(field.getName(), themeResId); + } + } + } + } + } catch (IllegalAccessException e) { + LogUtil.printException("PluginThemeHelper.getAllPluginThemes", e); + } catch (ClassNotFoundException e) { + LogUtil.printException("PluginThemeHelper.getAllPluginThemes", e); + } + } + } + return themes; + } + + /** + * Used by host for skin + * 宿主程序使用插件主题 + */ + public static void applyPluginTheme(Activity activity, String pluginId, int themeResId) { + LayoutInflater layoutInflater = LayoutInflater.from(activity); + if (layoutInflater.getFactory() == null) { + if (!(activity.getBaseContext() instanceof PluginContextTheme)) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if (pluginDescriptor != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + //注入插件上下文和主题 + Context defaultContext = plugin.pluginContext; + Context pluginContext = PluginCreator.createNewPluginComponentContext(defaultContext, + ((PluginBaseContextWrapper)activity.getBaseContext()).getBaseContext(), 0); + PluginInjector.resetActivityContext(pluginContext, activity, themeResId); + } + } + } + } else { + LogUtil.e("启用了控件级插件的页面 不能使用换肤功能呢"); + } + } + + /** + * Used by plugin for Theme + * 插件使用插件主题 + */ + public static void setTheme(Context pluginContext, int resId) { + if (pluginContext instanceof PluginContextTheme) { + ((PluginContextTheme)pluginContext).mTheme = null; + pluginContext.setTheme(resId); + } + } + +} \ No newline at end of file diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginViewCreator.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginViewCreator.java new file mode 100644 index 00000000..7346e3f8 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/PluginViewCreator.java @@ -0,0 +1,100 @@ +package com.limpoxe.fairy.core; + +import android.content.Context; +import android.content.ContextWrapper; +import android.util.AttributeSet; +import android.view.InflateException; +import android.view.LayoutInflater; +import android.view.View; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Constructor; + +/** + * 控件级插件的实现原理 + * + * @author cailiming + * + */ +public class PluginViewCreator implements LayoutInflater.Factory { + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + + //可以在这里全局替换控件类型 + if ("TextView".equals(name)) { + //return new CustomTextView(); + } else if ("ImageView".equals(name)) { + //return new CustomImageView(); + } + + return createViewFromTag(context, name, attrs); + + } + + private View createViewFromTag(Context context, String name, AttributeSet attrs) { + if (name.equals("pluginView")) { + + String pluginId = attrs.getAttributeValue(null, "context"); + String viewClassName = attrs.getAttributeValue(null, "class"); + + LogUtil.v("创建插件view", pluginId, viewClassName); + + try { + View view = createView(context, pluginId, viewClassName, attrs); + if (view != null) { + return view; + } + } catch (Exception e) { + LogUtil.printException("PluginViewCreator.createViewFromTag", e); + } finally { + } + + View view = new View(context, attrs); + view.setVisibility(View.GONE); + return view; + } + + return null; + } + + private View createView(Context Context, String pluginId, String viewClassName, AttributeSet atts) + throws InflateException { + try { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if (pluginDescriptor != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + Context baseContext = Context; + if (!(baseContext instanceof PluginContextTheme)) { + baseContext = ((ContextWrapper)baseContext).getBaseContext(); + } + if (baseContext instanceof PluginContextTheme) { + baseContext = ((PluginContextTheme) baseContext).getBaseContext(); + } + Context pluginViewContext = PluginCreator.createNewPluginComponentContext(plugin.pluginContext, baseContext, pluginDescriptor.getApplicationTheme()); + Class clazz = pluginViewContext.getClassLoader() + .loadClass(viewClassName).asSubclass(View.class); + + Constructor constructor = clazz.getConstructor(new Class[] { + Context.class, AttributeSet.class}); + constructor.setAccessible(true); + return constructor.newInstance(new Object[]{pluginViewContext , atts}); + } else { + LogUtil.e("插件启动失败 " + pluginId); + } + } else { + LogUtil.e("未找到插件" + pluginId + ",请确认是否已安装"); + } + } catch (Exception e) { + LogUtil.printException("createView", e); + } + return null; + } + +} \ No newline at end of file diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealHostClassLoader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealHostClassLoader.java new file mode 100644 index 00000000..1394ae0c --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealHostClassLoader.java @@ -0,0 +1,159 @@ +package com.limpoxe.fairy.core; + +import android.app.Activity; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Process; +import android.widget.Toast; + +import com.limpoxe.fairy.core.bridge.PluginShadowService; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import dalvik.system.DexClassLoader; + +/** + * 为了支持Receiver和Service,增加此类。 + * + * @author Administrator + * + */ +public class RealHostClassLoader extends DexClassLoader { + + public RealHostClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { + super(dexPath, optimizedDirectory, libraryPath, parent); + } + + @Override + public String findLibrary(String name) { + LogUtil.v("findLibrary", name); + return super.findLibrary(name); + } + + @Override + protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { + + //Just for Receiver and Service + + if (className.startsWith(PluginIntentResolver.CLASS_PREFIX_SERVICE)) { + + LogUtil.v("className ", className); + + // 这里返回PluginShadowService是因为service的构造函数以及onCreate函数 + // 2个函数在ActivityThread的同一个函数中被调用,框架没机会在构造器执行之后,oncreate执行之前, + // 插入一段代码, 注入context. + // 因此这里返回一个fake的service, 在fake service的oncreate方法里面手动调用构造器和oncreate + // 这里返回了这个Service以后, 由于在框架中hook了ActivityManager的serviceDoneExecuting方法, + // 在serviceDoneExecuting这个方法里面, 会将这个service再还原成插件的servcie对象 + if (!className.equals(PluginIntentResolver.CLASS_PREFIX_SERVICE_NOT_FOUND)) { + return PluginShadowService.class; + } + + LogUtil.e("到了这里说明出bug了,这里做个容错处理, 避免出现classnotfound", className); + return RealHostClassLoader.TolerantService.class; + + } else if (className.startsWith(PluginIntentResolver.CLASS_PREFIX_RECEIVER)) { + + LogUtil.v("className ", className); + + if (!className.equals(PluginIntentResolver.CLASS_PREFIX_RECEIVER_NOT_FOUND)) { + String realName = className.replace(PluginIntentResolver.CLASS_PREFIX_RECEIVER, ""); + Class clazz = PluginLoader.loadPluginClassByName(realName); + if (clazz != null) { + return clazz; + } + } + + LogUtil.e("到了这里说明出bug了,这里做个容错处理, 避免出现classnotfound", className); + return RealHostClassLoader.TolerantBroadcastReceiver.class; + } + + //如果这里出现classnotfound,但是className确实是一个插件的receiver或者service, + //那么很可能是PluginAppTrace没有替换成功,或者替换成功了但是又被其他东西覆盖替换掉了。 + return super.loadClass(className, resolve); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + //宿主Activity内嵌几个来自插件的Fragment时 + //如果这个宿主Activity在后台被系统回收了,用户重新回到这个Activity时系统自动恢复被回收的Activity + //同时这个Activity内如果有Fragment也会被自动恢复,如果被恢复的Fragment是来自插件,则会发送ClassNotFound + //因为恢复Fragment时使用的ClassLoader就是当前宿主Activity的getClassLoader + //重写这个方法,就是为了处理这个问题,使得自动恢复Fragment不会产生ClassNotFound + //这里只关心被列入插件Manifest中的组件的类 + if(ProcessUtil.isPluginProcess()) { + Class clazz = PluginLoader.loadPluginClassByName(name); + if (clazz != null) { + return clazz; + } + } + return super.findClass(name); + } + + public static class TolerantBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + LogUtil.w("容错TolerantBroadcastReceiver被触发"); + } + } + + public static class TolerantService extends Service { + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtil.w("容错TolerantService onStartCommand被触发"); + stopSelf(); + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtil.w("容错TolerantService onDestroy被触发"); + } + } + + public static class TolerantActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LogUtil.w("容错TolerantActivity被触发"); + } + + @Override + protected void onResume() { + super.onResume(); + finish(); + + Toast.makeText(this, "正在退出...", Toast.LENGTH_LONG).show(); + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + LogUtil.w("killProcess,exit"); + Process.killProcess(Process.myPid()); + System.exit(10); + } + }, 1000); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + LogUtil.w("killProcess,exit"); + Process.killProcess(Process.myPid()); + System.exit(10); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealPluginClassLoader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealPluginClassLoader.java new file mode 100644 index 00000000..7ed1f2cd --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/RealPluginClassLoader.java @@ -0,0 +1,179 @@ +package com.limpoxe.fairy.core; + +import android.os.Build; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.util.FileUtil; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import dalvik.system.DexClassLoader; + +/** + * 插件间依赖以及so管理 + * + * @author Administrator + * + */ +public class RealPluginClassLoader extends DexClassLoader { + public final String pluginPackageName; + public final boolean isStandalone; + private static Hashtable soClassloaderMapper = new Hashtable(); + + private String[] dependencies; + private List multiDexClassLoaderList; + + public RealPluginClassLoader(String pluginPackageName, String dexPath, String[] dependencies, + String optimizedDirectory, String libraryPath, + List multiDexList, boolean isStandalone) { + super(dexPath, optimizedDirectory, libraryPath, isStandalone ? ClassLoader.getSystemClassLoader().getParent() : RealPluginClassLoader.class.getClassLoader()); + this.dependencies = dependencies; + this.pluginPackageName = pluginPackageName; + this.isStandalone = isStandalone; + if (multiDexList != null) { + if (multiDexClassLoaderList == null) { + multiDexClassLoaderList = new ArrayList(multiDexList.size()); + for(String path: multiDexList) { + multiDexClassLoaderList.add(new DexClassLoader(path, optimizedDirectory, libraryPath, getParent())); + } + } + } + } + + @Override + public String findLibrary(String name) { + + final String thisLoader = getClass().getName() + '@' + Integer.toHexString(hashCode()); + final String soPath = super.findLibrary(name); + + LogUtil.v("findLibrary", "orignal so path : " + soPath + ", current classloader : " + thisLoader); + + if (soPath != null) { + final String soLoader = soClassloaderMapper.get(soPath); + if (soLoader == null || soLoader.equals(thisLoader)) { + soClassloaderMapper.put(soPath, thisLoader); + LogUtil.v("findLibrary", "acturely so path : " + soPath + ", current classloader : " + thisLoader); + return soPath; + } else { + //classloader发生了变化, 创建so副本并返回副本路径, 限制最多10个副本 + for (int i = 1; i < 5; i++) { + + String soPathOfCopyN = tryPath(soPath, i); + String soLoaderOfCopyN = soClassloaderMapper.get(soPathOfCopyN); + + if (thisLoader.equals(soLoaderOfCopyN)) { + LogUtil.v("findLibrary", "acturely so path : " + soPathOfCopyN + ", current classloader : " + thisLoader); + return soPathOfCopyN; + } else if (soLoaderOfCopyN == null) { + if(!new File(soPathOfCopyN).exists()) { + boolean isSuccess = FileUtil.copyFile(soPath, soPathOfCopyN); + if (isSuccess) { + soClassloaderMapper.put(soPathOfCopyN, thisLoader); + LogUtil.v("findLibrary", "acturely so path : " + soPathOfCopyN + ", current classloader : " + thisLoader); + return soPathOfCopyN; + } else { + return null; + } + } else { + soClassloaderMapper.put(soPathOfCopyN, thisLoader); + LogUtil.v("findLibrary", "acturely so path : " + soPathOfCopyN + ", current classloader : " + thisLoader); + return soPathOfCopyN; + } + } + } + LogUtil.e("findLibrary", "最多创建5个副本..."); + } + } + return null; + } + + private String tryPath(String orignalPath, int i) { + StringBuilder soPathBuilder = new StringBuilder(orignalPath); + soPathBuilder.delete(orignalPath.length() - 3, orignalPath.length());//移除.so后缀 + soPathBuilder.append("_").append(i).append(".so"); + return soPathBuilder.toString(); + } + + @Override + protected Class findClass(String className) throws ClassNotFoundException { + Class clazz = null; + ClassNotFoundException suppressed = null; + try { + clazz = super.findClass(className); + } catch (ClassNotFoundException e) { + suppressed = e; + } + + //这里判断android.view 是为了解决webview的问题 + if (clazz == null && !className.startsWith("android.view")) { + + if (multiDexClassLoaderList != null) { + for(DexClassLoader dexLoader : multiDexClassLoaderList) { + try { + clazz = dexLoader.loadClass(className); + } catch (ClassNotFoundException e) { + } + if (clazz != null) { + break; + } + } + } + + if (clazz == null && dependencies != null) { + for (String dependencePluginId: dependencies) { + + //被依赖的插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(dependencePluginId); + + if (plugin != null) { + try { + clazz = plugin.pluginClassLoader.loadClass(className); + } catch (ClassNotFoundException e) { + } + if (clazz != null) { + break; + } + } else { + LogUtil.e("PluginClassLoader", "未找到当前插件所必需的、通过在此插件manifest中使用标签配置的依赖插件", dependencePluginId, className); + LogUtil.e("PluginClassLoader", "极有可能在运行此插件时GG,这里应该直接抛个异常"); + //throw new IllegalStateException("未找到当前插件所必需基础插件" + dependencePluginId); + } + } + } + + if (clazz == null && isStandalone) { + try { + // 插件捞class没捞着,最后回头到宿主的classloader里面捞一次 + Class classInHostButNotReallyInHost = RealPluginClassLoader.class.getClassLoader().loadClass(className); + // 如果捞到了,先不要开心,还需要排除一下这个类是不是在宿主class所在的classloader中 + // 进这个case的典型场景就是独立插件中使用了use-libray + // 因为从android10开始use-libray既不会加到主包的classloader里面,也不会加到系统的classloader + // 而是在中间多了一个ClassLoader[] sharedLibraryLoaders用来存储use-libray附加的classloader + // android9又不太一样,是合并到宿主的PatchClassloader的dexElements列表中 + // 这里的逻辑就是为了在sharedLibraryLoaders里面再捞一次 + // 不影响非独立插件的原因是非独立插件的parent就是宿主,搜索链路中已经包含它了 + if (Build.VERSION.SDK_INT >= 29) { + if (classInHostButNotReallyInHost.getClassLoader() != RealPluginClassLoader.class.getClassLoader()) { + return classInHostButNotReallyInHost; + } + } else if (Build.VERSION.SDK_INT == 28) { + if (classInHostButNotReallyInHost.getClassLoader() == RealPluginClassLoader.class.getClassLoader()) { + return classInHostButNotReallyInHost; + } + } + } catch (ClassNotFoundException e) { + } + } + } + + if (clazz == null && suppressed != null) { + throw suppressed; + } + + return clazz; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/Runner.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/Runner.java new file mode 100644 index 00000000..ee1aa56c --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/Runner.java @@ -0,0 +1,5 @@ +package com.limpoxe.fairy.core; + +public interface Runner { + T run(); +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/SyncRunnable.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/SyncRunnable.java new file mode 100644 index 00000000..4c1d5170 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/SyncRunnable.java @@ -0,0 +1,60 @@ +package com.limpoxe.fairy.core; + +import android.os.Handler; +import android.os.Looper; +import android.os.Process; + +import com.limpoxe.fairy.util.LogUtil; + +public class SyncRunnable implements Runnable { + private final Runner mTarget; + private Object mResult; + private boolean mComplete; + private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); + + private SyncRunnable(Runner target) { + mTarget = target; + } + + @Override + public void run() { + try { + mResult = mTarget.run(); + } catch (Exception e) { + LogUtil.printException("Exception kill", e); + LogUtil.e("Kill", "发生了无法处理的异常,杀掉当前进程: " + Process.myPid()); + Process.killProcess(Process.myPid()); + } + synchronized (this) { + mComplete = true; + notifyAll(); + } + } + + private Object waitForComplete() { + synchronized (this) { + while (!mComplete) { + try { + wait(); + } catch (InterruptedException e) { + } + } + return mResult; + } + } + + /** + * 如果在主线程中,则直接调用runner, 如果不在主线程中,则转到主线程中调用,并等待主线程返回 + * @param runner + */ + public static T runOnMainSync(Runner runner) { + if (Looper.myLooper() == Looper.getMainLooper()) { + return runner.run(); + } else { + SyncRunnable sr = new SyncRunnable(runner); + sMainHandler.post(sr); + return (T)sr.waitForComplete(); + } + } + +} \ No newline at end of file diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/UncaugthExceptionWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/UncaugthExceptionWrapper.java new file mode 100644 index 00000000..77624426 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/UncaugthExceptionWrapper.java @@ -0,0 +1,112 @@ +package com.limpoxe.fairy.core; + +import android.os.Process; + +import com.limpoxe.fairy.util.LogUtil; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Created by cailiming on 2017/6/27. + */ + +public class UncaugthExceptionWrapper implements Thread.UncaughtExceptionHandler { + + final HashMap pluginExHandlers = new HashMap(); + + boolean isCalled = false; + Thread.UncaughtExceptionHandler hostHandler = null; + + public UncaugthExceptionWrapper() { + //可能会被创建多个实例 + } + + public void addHandler(String packageName, Thread.UncaughtExceptionHandler handler) { + pluginExHandlers.put(packageName, handler); + } + + public void removeHandler(String packageName) { + pluginExHandlers.remove(packageName); + } + + public void setHostHandler(Thread.UncaughtExceptionHandler handler) { + this.hostHandler = handler; + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + if (isCalled) { + LogUtil.e("这个方法只能被调用1此,防止递归,退出"); + Process.killProcess(Process.myPid()); + System.exit(10); + return; + } + isCalled = true; + + //判断异常来源,是插件还是宿主 + StackTraceElement[] elements = e.getStackTrace(); + boolean isTargetHandlerFound = sendTarget(elements, t, e); + + if (!isTargetHandlerFound) { + //尝试再识别一次 + Throwable cause = e.getCause(); + if (cause != null) { + StackTraceElement[] ste2 = cause.getStackTrace(); + isTargetHandlerFound = sendTarget(ste2, t, e); + } + } + + if (!isTargetHandlerFound) { + if (hostHandler != null) { + LogUtil.e("未识别出此异常来源,交给宿主继续识别或处理"); + hostHandler.uncaughtException(t, e); + } else { + //Exception not Handled + LogUtil.e("插件和宿主都未处理此异常,退出"); + Process.killProcess(Process.myPid()); + System.exit(10); + } + } else { + LogUtil.e("插件已处理此异常,退出"); + Process.killProcess(Process.myPid()); + System.exit(10); + } + + } + + private boolean sendTarget(StackTraceElement[] elements, Thread t, Throwable e) { + boolean isTargetHandlerFound = false; + if (elements != null) { + Iterator> itr = pluginExHandlers.entrySet().iterator(); + while (itr.hasNext()) { + + Map.Entry entry = itr.next(); + String packageName = entry.getKey(); + Thread.UncaughtExceptionHandler pluginHandler = entry.getValue(); + + if (pluginHandler != null) { + for(int i = 0; i < elements.length; i++) { + StackTraceElement element = elements[i]; + //异常栈里面包含插件的包名,则认为是这个插件抛出的异常 + LogUtil.d("EEE", element.getClassName() + " packageName=" + packageName); + //这里只是简单的通过判断异常调用栈是否包含插件的包名来判断插件来源,不一定准确 + //不过应该也可以覆盖大部分情况了 + if (element.getClassName() != null && element.getClassName().startsWith(packageName)) { + LogUtil.e("识别出此异常来源,交给插件处理", packageName); + pluginHandler.uncaughtException(t, e); + isTargetHandlerFound = true; + break; + } + } + } + + if (isTargetHandlerFound) { + break; + } + } + } + return isTargetHandlerFound; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivity.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivity.java new file mode 100644 index 00000000..eb21e7f0 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivity.java @@ -0,0 +1,39 @@ +package com.limpoxe.fairy.core.android; + +import android.app.Application; +import android.app.Instrumentation; +import android.content.pm.ActivityInfo; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackActivity extends HackContextThemeWrapper { + private static final String ClassName = "android.app.Activity"; + + private static final String Field_mActivityInfo = "mActivityInfo"; + private static final String Field_mApplication = "mApplication"; + private static final String Field_mInstrumentation = "mInstrumentation"; + + public HackActivity(Object instance) { + super(instance); + } + + public final ActivityInfo getActivityInfo() { + return (ActivityInfo) RefInvoker.getField(instance, ClassName, Field_mActivityInfo); + } + + public final void setApplication(Application application) { + RefInvoker.setField(instance, ClassName, Field_mApplication, application); + } + + public final void setInstrumentation(Instrumentation instrumentation) { + RefInvoker.setField(instance, ClassName, Field_mInstrumentation, instrumentation); + } + + public final Instrumentation getInstrumentation() { + return (Instrumentation) RefInvoker.getField(instance, ClassName, Field_mInstrumentation); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManager.java new file mode 100644 index 00000000..dc91e1b0 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManager.java @@ -0,0 +1,23 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 17/5/18. + */ + +public class HackActivityManager { + + private static final String ClassName = "android.app.ActivityManager"; + + private static final String Field_IActivityManagerSingleton = "IActivityManagerSingleton"; + + public static Object getIActivityManagerSingleton() { + return RefInvoker.getField(null, ClassName, Field_IActivityManagerSingleton); + } + + public static void setIActivityManagerSingleton(Object activityManagerSingleton) { + RefInvoker.setField(null, ClassName, Field_IActivityManagerSingleton, activityManagerSingleton); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManagerNative.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManagerNative.java new file mode 100644 index 00000000..745ac743 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityManagerNative.java @@ -0,0 +1,28 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackActivityManagerNative { + + private static final String ClassName = "android.app.ActivityManagerNative"; + + private static final String Method_getDefault = "getDefault"; + + private static final String Field_gDefault = "gDefault"; + + public static Object getDefault() { + return RefInvoker.invokeMethod(null, ClassName, Method_getDefault, (Class[])null, (Object[])null); + } + + public static Object getGDefault() { + return RefInvoker.getField(null, ClassName, Field_gDefault); + } + + public static void setGDefault(Object gDefault) { + RefInvoker.setField(null, ClassName, Field_gDefault, gDefault); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThread.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThread.java new file mode 100644 index 00000000..aefbc7e9 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThread.java @@ -0,0 +1,264 @@ +package com.limpoxe.fairy.core.android; + +import android.app.Application; +import android.app.Instrumentation; +import android.app.Service; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; +import android.os.Handler; +import android.os.IBinder; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginAppTrace; +import com.limpoxe.fairy.core.PluginInstrumentionWrapper; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import java.io.File; +import java.util.List; +import java.util.Map; + +public class HackActivityThread { + + private static final String ClassName = "android.app.ActivityThread"; + + private static final String Method_currentActivityThread = "currentActivityThread"; + private static final String Method_getHandler = "getHandler"; + private static final String Method_installContentProviders = "installContentProviders"; + private static final String Method_getPackageInfoNoCheck = "getPackageInfoNoCheck"; + + private static final String Field_mInstrumentation = "mInstrumentation"; + private static final String Field_mServices = "mServices"; + private static final String Field_mBoundApplication = "mBoundApplication"; + private static final String Field_sPackageManager = "sPackageManager"; + + private static final String Field_mProviderMap = "mProviderMap"; + private static final String Field_mLocalProviders = "mLocalProviders"; + private static final String Field_mLocalProvidersByName = "mLocalProvidersByName"; + + private static final String Field_SERVICE_DONE_EXECUTING_ANON = "SERVICE_DONE_EXECUTING_ANON"; + private static final String Field_SERVICE_DONE_EXECUTING_START = "SERVICE_DONE_EXECUTING_START"; + private static final String Field_SERVICE_DONE_EXECUTING_STOP = "SERVICE_DONE_EXECUTING_STOP"; + + private static HackActivityThread hackActivityThread; + + private Object instance; + + private HackActivityThread(Object instance) { + this.instance = instance; + } + + //这个方法必须在主线程调用,因为它是从ThreadLocal中取出来的,在其他线程中取出来一定是null + public static synchronized HackActivityThread get() { + if (hackActivityThread == null) { + Object instance = currentActivityThread(); + if (instance != null) { + hackActivityThread = new HackActivityThread(instance); + } + } + return hackActivityThread; + } + + public static Class clazz() { + try { + return RefInvoker.forName(ClassName); + } catch (ClassNotFoundException e) { + LogUtil.printException("HackActivityThread.clazz", e); + } + return null; + } + + private static Object currentActivityThread() { + // 从ThreadLocal中取出来的 + LogUtil.v("从宿主程序中取出ActivityThread对象备用"); + Object sCurrentActivityThread = RefInvoker.invokeMethod(null, ClassName, + Method_currentActivityThread, + (Class[]) null, (Object[]) null); + + //有些情况下上面的方法拿不到,下面再换个方法尝试一次 + if (sCurrentActivityThread == null) { + Object impl = HackContextImpl.getImpl(FairyGlobal.getHostApplication()); + if (impl != null) { + sCurrentActivityThread = new HackContextImpl(impl).getMainThread(); + } + } + return sCurrentActivityThread; + } + + public static void wrapHandler() { + HackActivityThread hackActivityThread = get(); + if (hackActivityThread != null) { + Handler handler = hackActivityThread.getHandler(); + Handler.Callback callback = new PluginAppTrace(handler); + new HackHandler(handler).setCallback(callback); + } else { + LogUtil.e("wrapHandler fail!!"); + } + } + + public static void wrapInstrumentation() { + HackActivityThread hackActivityThread = get(); + if (hackActivityThread != null) { + Instrumentation originalInstrumentation = hackActivityThread.getInstrumentation(); + if (!(originalInstrumentation instanceof PluginInstrumentionWrapper)) { + hackActivityThread.setInstrumentation(new PluginInstrumentionWrapper(originalInstrumentation)); + } + } else { + LogUtil.e("wrapInstrumentation fail!!"); + } + } + + public static Object getResCompatibilityInfo() { + //貌似没啥用 + HackActivityThread hackActivityThread = get(); + if (hackActivityThread != null) { + Object mBoundApplication = hackActivityThread.getBoundApplicationData(); + Object compatInfo = new HackAppBindData(mBoundApplication).getCompatInfo(); + return compatInfo; + } + return null; + } + + public static Object getLoadedApk() { + HackActivityThread hackActivityThread = get(); + if (hackActivityThread != null) { + //貌似没啥用 + Object mBoundApplication = hackActivityThread.getBoundApplicationData(); + Object info = new HackAppBindData(mBoundApplication).getInfo(); + return info; + } + return null; + } + + //For TabHostActivity + public static void installPackageInfo(Context hostContext, String pluginId, PluginDescriptor pluginDescriptor, + ClassLoader pluginClassLoader, Resources pluginResource, + Application pluginApplication) throws ClassNotFoundException { + + Object applicationLoaders = RefInvoker.invokeMethod(null, "android.app.ApplicationLoaders", "getDefault", (Class[]) null, (Object[]) null); + Map mLoaders = (Map)RefInvoker.getField(applicationLoaders, "android.app.ApplicationLoaders", "mLoaders"); + if (mLoaders == null) { + //what!! + return; + } + mLoaders.put(pluginDescriptor.getInstalledPath(), pluginClassLoader); + try { + //先保存 + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + //会触发替换 + HackActivityThread hackActivityThread = HackActivityThread.get(); + if (hackActivityThread != null) { + ApplicationInfo info = hostContext.getPackageManager().getApplicationInfo(pluginId, PackageManager.GET_SHARED_LIBRARY_FILES); + Object compatibilityInfo = getResCompatibilityInfo();//Not Sure + Object pluginLoadedApk = hackActivityThread.getPackageInfoNoCheck(info, compatibilityInfo); + if (pluginLoadedApk != null) { + HackLoadedApk loadedApk = new HackLoadedApk(pluginLoadedApk); + loadedApk.setApplication(pluginApplication); + loadedApk.setResources(pluginResource); + loadedApk.setDataDirFile(new File(FairyGlobal.getHostApplication().getApplicationInfo().dataDir)); + loadedApk.setDataDir(FairyGlobal.getHostApplication().getApplicationInfo().dataDir); + //TODO 需要时再说 + //loadedApk.setLibDir(); + } + } + //再还原 + Thread.currentThread().setContextClassLoader(classLoader); + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("HackActivityThread.installPackageInfo", e); + } + } + + public Object getBoundApplicationData() { + Object mBoundApplication = RefInvoker.getField(instance, ClassName, Field_mBoundApplication); + return mBoundApplication; + } + + public void installContentProviders(Context context, List providers) { + RefInvoker.invokeMethod(instance, + ClassName, Method_installContentProviders, + new Class[]{Context.class, List.class}, new Object[]{context, providers}); + } + + public Handler getHandler() { + return (Handler)RefInvoker.invokeMethod(instance, + ClassName, Method_getHandler, + (Class[]) null, (Object[]) null); + } + + public Instrumentation getInstrumentation() { + return (Instrumentation) RefInvoker.getField(instance, + ClassName, Field_mInstrumentation); + } + + public void setInstrumentation(Instrumentation instrumentation) { + RefInvoker.setField(instance, ClassName, + Field_mInstrumentation, + instrumentation); + } + + public Map getServices() { + Map services = (Map)RefInvoker.getField(instance, ClassName, Field_mServices); + return services; + } + + public Object getPackageInfoNoCheck(ApplicationInfo info, Object compatibilityInfo) { + try { + Object pluginLoadedApk = RefInvoker.invokeMethod(instance, ClassName, Method_getPackageInfoNoCheck, + new Class[]{ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")}, + new Object[]{info, compatibilityInfo}); + return pluginLoadedApk; + } catch (ClassNotFoundException e) { + LogUtil.printException("HackActivityThread.getPackageInfoNoCheck", e); + } + return null; + } + + public static Object getPackageManager() { + return RefInvoker.getField(null, ClassName, Field_sPackageManager); + } + + public static void setPackageManager(Object packageManager) { + RefInvoker.setField(null, ClassName, Field_sPackageManager, packageManager); + } + + public static Integer getSERVICE_DONE_EXECUTING_ANON() { + Integer ret = (Integer) RefInvoker.getField(null, ClassName, Field_SERVICE_DONE_EXECUTING_ANON); + if (ret == null) { + ret = 0;//default is 0 + } + return ret; + } + + public static Integer getSERVICE_DONE_EXECUTING_START() { + Integer ret = (Integer) RefInvoker.getField(null, ClassName, Field_SERVICE_DONE_EXECUTING_START); + if (ret == null) { + ret = 0;//default is 0 + } + return ret; + } + + public static Integer getSERVICE_DONE_EXECUTING_STOP() { + Integer ret = (Integer) RefInvoker.getField(null, ClassName, Field_SERVICE_DONE_EXECUTING_STOP); + if (ret == null) { + ret = 0;//default is 0 + } + return ret; + } + + public Map getProviderMap() { + return (Map) RefInvoker.getField(instance, ClassName, Field_mProviderMap); + } + + public Map getLocalProviders() { + return (Map) RefInvoker.getField(instance, ClassName, Field_mLocalProviders); + + } + + public Map getLocalProvidersByName() { + return (Map) RefInvoker.getField(instance, ClassName, Field_mLocalProvidersByName); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThreadProviderClientRecord.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThreadProviderClientRecord.java new file mode 100644 index 00000000..f4296287 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackActivityThreadProviderClientRecord.java @@ -0,0 +1,36 @@ +package com.limpoxe.fairy.core.android; + +import android.content.ContentProvider; + +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +public class HackActivityThreadProviderClientRecord { + + private static final String ClassName = "android.app.ActivityThread$ProviderClientRecord"; + + private static final String Field_mProvider = "mLocalProvider"; + + private Object instance; + + public HackActivityThreadProviderClientRecord(Object instance) { + this.instance = instance; + } + + public static Class clazz() { + try { + return RefInvoker.forName(ClassName); + } catch (ClassNotFoundException e) { + LogUtil.printException("HackActivityThreadProviderClientRecord.clazz", e); + } + return null; + } + + public ContentProvider getProvider() { + Object o = RefInvoker.getField(instance, ClassName, Field_mProvider); + if (o instanceof ContentProvider) {//maybe ContentProviderProxy + return (ContentProvider) o; + } + return null; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAndroidXLocalboarcastManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAndroidXLocalboarcastManager.java new file mode 100644 index 00000000..7feab1d2 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAndroidXLocalboarcastManager.java @@ -0,0 +1,41 @@ +package com.limpoxe.fairy.core.android; + +import android.content.BroadcastReceiver; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.util.HashMap; + +/** + * Created by cailiming on 19/11/3. + */ + +public class HackAndroidXLocalboarcastManager { + + private static final String ClassName = "androidx.localbroadcastmanager.content.LocalBroadcastManager"; + + private static final String Field_mInstance = "mInstance"; + private static final String Field_mReceivers = "mReceivers"; + + private static final String Method_unregisterReceiver = "unregisterReceiver"; + + private Object instance ; + + public HackAndroidXLocalboarcastManager(Object instance) { + this.instance = instance; + } + + public static Object getInstance() { + return RefInvoker.getField(null, ClassName, Field_mInstance); + } + + public HashMap getReceivers() { + return (HashMap)RefInvoker.getField(instance, ClassName, Field_mReceivers); + } + + public void unregisterReceiver(BroadcastReceiver item) { + RefInvoker.invokeMethod(instance, ClassName, Method_unregisterReceiver, new Class[]{BroadcastReceiver.class}, new Object[]{item}); + + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAppBindData.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAppBindData.java new file mode 100644 index 00000000..94fbd636 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAppBindData.java @@ -0,0 +1,29 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackAppBindData { + private static final String ClassName = "android.app.ActivityThread$AppBindData"; + + private static final String Field_compatInfo = "compatInfo"; + private static final String Field_info = "info"; + + private Object instance; + + public HackAppBindData(Object instance) { + this.instance = instance; + } + + public Object getInfo() { + return RefInvoker.getField(instance, ClassName, Field_info); + } + + public Object getCompatInfo() { + return RefInvoker.getField(instance, ClassName, Field_compatInfo); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplication.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplication.java new file mode 100644 index 00000000..84523d09 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplication.java @@ -0,0 +1,61 @@ +package com.limpoxe.fairy.core.android; + +import android.app.Activity; +import android.os.Bundle; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/3/11. + */ +public class HackApplication { + private static final String ClassName = "android.app.Application"; + + private static final String Method_dispatchActivityCreated = "dispatchActivityCreated"; + private static final String Method_dispatchActivityStarted = "dispatchActivityStarted"; + private static final String Method_dispatchActivityResumed = "dispatchActivityResumed"; + private static final String Method_dispatchActivityPaused = "dispatchActivityPaused"; + private static final String Method_dispatchActivityStopped = "dispatchActivityStopped"; + private static final String Method_dispatchActivitySaveInstanceState = "dispatchActivitySaveInstanceState"; + private static final String Method_dispatchActivityDestroyed = "dispatchActivityDestroyed"; + + private static final String Field_mLoadedApk = "mLoadedApk"; + + private Object instance; + + public HackApplication(Object instance) { + this.instance = instance; + } + + public void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityCreated, new Class[]{Activity.class, Bundle.class}, new Object[]{activity, savedInstanceState}); + } + + public void dispatchActivityStarted(Activity activity) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityStarted, new Class[]{Activity.class}, new Object[]{activity}); + } + + public void dispatchActivityResumed(Activity activity) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityResumed, new Class[]{Activity.class}, new Object[]{activity}); + } + + public void dispatchActivityPaused(Activity activity) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityPaused, new Class[]{Activity.class}, new Object[]{activity}); + } + + public void dispatchActivityStopped(Activity activity) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityStopped, new Class[]{Activity.class}, new Object[]{activity}); + } + + public void dispatchActivitySaveInstanceState(Activity activity, Bundle outState) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivitySaveInstanceState, new Class[]{Activity.class, Bundle.class}, new Object[]{activity, outState}); + } + + public void dispatchActivityDestroyed(Activity activity) { + RefInvoker.invokeMethod(instance, ClassName, Method_dispatchActivityDestroyed, new Class[]{Activity.class}, new Object[]{activity}); + } + + public Object getLoadedApk() { + return RefInvoker.getField(instance, ClassName, Field_mLoadedApk); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplicationPackageManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplicationPackageManager.java new file mode 100644 index 00000000..0f5065c8 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackApplicationPackageManager.java @@ -0,0 +1,23 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackApplicationPackageManager { + private static final String ClassName = "android.app.ApplicationPackageManager"; + + private static final String Field_mPM = "mPM"; + + private Object instance; + + public HackApplicationPackageManager(Object instance) { + this.instance = instance; + } + + public void setPM(Object pm) { + RefInvoker.setField(instance, ClassName, Field_mPM, pm); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAssetManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAssetManager.java new file mode 100644 index 00000000..9bd7d066 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackAssetManager.java @@ -0,0 +1,48 @@ +package com.limpoxe.fairy.core.android; + +import android.util.SparseArray; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackAssetManager { + + //maybe android.content.res.MiuiAssetManager / android.content.res.BaiduAssetManager + private static final String ClassName = "android.content.res.AssetManager"; + + private static final String Method_addAssetPath = "addAssetPath"; + private static final String Method_addAssetPaths = "addAssetPaths"; + private static final String Method_ensureStringBlocks = "ensureStringBlocks"; + private static final String Method_getAssignedPackageIdentifiers = "getAssignedPackageIdentifiers"; + + private Object instance; + + public HackAssetManager(Object instance) { + this.instance = instance; + } + + public void addAssetPath(String path) { + RefInvoker.invokeMethod(instance, ClassName, Method_addAssetPath, new Class[]{String.class}, new Object[]{path}); + } + + public void addAssetPaths(String[] assetPaths) { + RefInvoker.invokeMethod(instance, ClassName, Method_addAssetPaths, + new Class[] { String[].class }, new Object[] { assetPaths }); + + } + + //Android L + public SparseArray getAssignedPackageIdentifiers() { + SparseArray packageIdentifiers = (SparseArray) RefInvoker.invokeMethod(instance, + ClassName, Method_getAssignedPackageIdentifiers, null, null); + return packageIdentifiers; + } + + public Object[] ensureStringBlocks() { + return (Object[])RefInvoker.invokeMethod(instance, + ClassName, Method_ensureStringBlocks, null, null); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackComponentName.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackComponentName.java new file mode 100644 index 00000000..f0a31499 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackComponentName.java @@ -0,0 +1,27 @@ +package com.limpoxe.fairy.core.android; + +import android.content.ComponentName; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackComponentName extends HackContextThemeWrapper { + private static final String ClassName = ComponentName.class.getName(); + private static final String Field_mPackage = "mPackage"; + private static final String Field_mClass = "mClass"; + + public HackComponentName(Object instance) { + super(instance); + } + + public final void setPackageName(String packageName) { + RefInvoker.setField(instance, ClassName, Field_mPackage, packageName); + } + + public final void setClassName(String className) { + RefInvoker.setField(instance, ClassName, Field_mClass, className); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProvider.java new file mode 100644 index 00000000..7e6a1d12 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProvider.java @@ -0,0 +1,21 @@ +package com.limpoxe.fairy.core.android; + +import android.content.ContentProvider; + +import com.limpoxe.fairy.util.RefInvoker; + +public class HackContentProvider { + + private static final String Field_mAuthority = "mAuthority"; + + private Object instance; + + public HackContentProvider(ContentProvider instance) { + this.instance = instance; + } + + public String getAuthority() { + return (String)RefInvoker.getField(instance, ContentProvider.class, Field_mAuthority); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderClient.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderClient.java new file mode 100644 index 00000000..62a507a0 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderClient.java @@ -0,0 +1,24 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackContentProviderClient { + private static final String ClassName = "android.content.ContentProviderClient"; + + private static final String Field_mContentProvider = "mContentProvider"; + + private Object instance; + + public HackContentProviderClient(Object instance) { + this.instance = instance; + } + + public Object getContentProvider() { + return RefInvoker.getField(instance, ClassName, Field_mContentProvider); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderHolder.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderHolder.java new file mode 100644 index 00000000..9905df75 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContentProviderHolder.java @@ -0,0 +1,37 @@ +package com.limpoxe.fairy.core.android; + +import android.content.pm.ProviderInfo; +import android.os.Build; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 2017/11/27. + */ + +public class HackContentProviderHolder { + private static final String ClassName = "android.app.IActivityManager$ContentProviderHolder"; + private static final String ClassName8 = "android.app.ContentProviderHolder"; + + private static final String Field_mLocal = "mLocal"; + + private Object instance; + + public HackContentProviderHolder(Object instance) { + this.instance = instance; + } + + public static Object newInstance(ProviderInfo info) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + return RefInvoker.newInstance(ClassName, new Class[]{ProviderInfo.class}, new Object[]{info}); + } else { + return RefInvoker.newInstance(ClassName8, new Class[]{ProviderInfo.class}, new Object[]{info}); + } + } + + public void setLocal(boolean local) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + RefInvoker.setField(instance, ClassName8, Field_mLocal, local); + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextImpl.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextImpl.java new file mode 100644 index 00000000..c2094bc8 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextImpl.java @@ -0,0 +1,93 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.util.ArrayMap; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.io.File; + +/** + * Created by cailiming on 16/10/25. + */ + +public class HackContextImpl { + + private static final String ClassName = "android.app.ContextImpl"; + + private static final String Field_mReceiverRestrictedContext = "mReceiverRestrictedContext"; + private static final String Field_mSharedPrefsPaths = "mSharedPrefsPaths"; + private static final String Field_mPreferencesDir = "mPreferencesDir"; + private static final String Field_mMainThread = "mMainThread"; + private static final String Field_mBasePackageName = "mBasePackageName"; + private static final String Field_mOpPackageName = "mOpPackageName"; + private static final String Field_sSharedPrefs = "sSharedPrefs"; + + private static final String Method_getOuterContext = "getOuterContext"; + private static final String Method_setOuterContext = "setOuterContext"; + private static final String Method_getImpl = "getImpl"; + private static final String Method_getReceiverRestrictedContext = "getReceiverRestrictedContext"; + + private Object instance; + + public HackContextImpl(Object instance) { + this.instance = instance; + } + + public void setReceiverRestrictedContext(Object value) { + RefInvoker.setField(instance, ClassName, Field_mReceiverRestrictedContext, value); + } + + public ContextWrapper getReceiverRestrictedContext() { + return (ContextWrapper)RefInvoker.invokeMethod(instance, ClassName, Method_getReceiverRestrictedContext, null, null); + } + + public ArrayMap getSharedPrefsPaths() { + return (ArrayMap)RefInvoker.getField(instance, ClassName, Field_mSharedPrefsPaths); + } + + public void setPreferencesDir(Object value) { + RefInvoker.setField(instance, ClassName, Field_mPreferencesDir, value); + } + + public File getPreferencesDir() { + return (File)RefInvoker.getField(instance, ClassName, Field_mPreferencesDir); + } + + public static Object getSharedPrefs() { + return RefInvoker.getField(null, ClassName, Field_sSharedPrefs); + } + + public Object getMainThread() { + return RefInvoker.getField(instance, ClassName, Field_mMainThread); + } + + public Context getOuterContext() { + return (Context)RefInvoker.invokeMethod(instance, ClassName, Method_getOuterContext, null, null); + } + + public void setOuterContext(Object paramValues) { + RefInvoker.invokeMethod(instance, ClassName, Method_setOuterContext, new Class[]{Context.class}, new Object[]{paramValues}); + } + + public static boolean instanceOf(Object object) { + return object.getClass().getName().equals(ClassName); + } + + public static Object getImpl(Object paramValues) { + return RefInvoker.invokeMethod(null, ClassName, Method_getImpl, new Class[]{Context.class}, new Object[]{paramValues}); + } + + public void setBasePackageName(Object value) { + RefInvoker.setField(instance, ClassName, Field_mBasePackageName, value); + } + + public void setOpPackageName(Object value) { + if (Build.VERSION.SDK_INT > 18) { + RefInvoker.setField(instance, ClassName, Field_mOpPackageName, value); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextThemeWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextThemeWrapper.java new file mode 100644 index 00000000..fb301dfa --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextThemeWrapper.java @@ -0,0 +1,38 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackContextThemeWrapper extends HackContextWrapper { + private static final String ClassName = "android.view.ContextThemeWrapper"; + + private static final String Field_mResources = "mResources"; + private static final String Field_mTheme = "mTheme"; + + private static final String Method_attachBaseContext = "attachBaseContext"; + + public HackContextThemeWrapper(Object instance) { + super(instance); + } + + public final void attachBaseContext(Object paramValues) { + RefInvoker.invokeMethod(instance, ClassName, Method_attachBaseContext, new Class[]{Context.class}, new Object[]{paramValues}); + } + + public final void setResources(Resources resources) { + if (Build.VERSION.SDK_INT > 16) { + RefInvoker.setField(instance, ClassName, Field_mResources, resources); + } + } + + public final void setTheme(Resources.Theme theme) { + RefInvoker.setField(instance, ClassName, Field_mTheme, theme); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextWrapper.java new file mode 100644 index 00000000..b1168153 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackContextWrapper.java @@ -0,0 +1,29 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Context; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackContextWrapper { + private static final String ClassName = "android.content.ContextWrapper"; + + private static final String Field_mBase = "mBase"; + + protected Object instance; + + public HackContextWrapper(Object instance) { + this.instance = instance; + } + + public final Context getBase() { + return (Context)RefInvoker.getField(instance, ClassName, Field_mBase); + } + + public final void setBase(Context context) { + RefInvoker.setField(instance, ClassName, Field_mBase, context); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackCreateServiceData.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackCreateServiceData.java new file mode 100644 index 00000000..23504aac --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackCreateServiceData.java @@ -0,0 +1,29 @@ +package com.limpoxe.fairy.core.android; + +import android.content.pm.ServiceInfo; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackCreateServiceData { + private static final String ClassName = "android.app.ActivityThread$CreateServiceData"; + + private static final String Field_info = "info"; + + private Object instance; + + public HackCreateServiceData(Object instance) { + this.instance = instance; + } + + public ServiceInfo getInfo() { + return (ServiceInfo)RefInvoker.getField(instance, ClassName, Field_info); + } + + public void setInfo(ServiceInfo info) { + RefInvoker.setField(instance, ClassName, Field_info, info); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackHandler.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackHandler.java new file mode 100644 index 00000000..04a22838 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackHandler.java @@ -0,0 +1,25 @@ +package com.limpoxe.fairy.core.android; + +import android.os.Handler; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackHandler { + private static final String ClassName = "android.os.Handler"; + + private static final String Field_mCallback = "mCallback"; + private Object instance; + + public HackHandler(Object instance) { + this.instance = instance; + } + + public void setCallback(Handler.Callback callback) { + RefInvoker.setField(instance, ClassName, Field_mCallback, callback); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackIContentProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackIContentProvider.java new file mode 100644 index 00000000..c17fada1 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackIContentProvider.java @@ -0,0 +1,27 @@ +package com.limpoxe.fairy.core.android; + +import android.os.Bundle; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackIContentProvider { + private static final String ClassName = "android.content.IContentProvider"; + + private static final String Methdo_call = "call"; + + private Object instance; + + public HackIContentProvider(Object instance) { + this.instance = instance; + } + + public Object call(String method, String arg, Bundle extras) { + return RefInvoker.invokeMethod(instance, ClassName, Methdo_call, + new Class[]{String.class, String.class, Bundle.class}, + new Object[]{method, arg, extras}); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackInstrumentation.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackInstrumentation.java new file mode 100644 index 00000000..83e138ab --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackInstrumentation.java @@ -0,0 +1,166 @@ +package com.limpoxe.fairy.core.android; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Fragment; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.UserHandle; + +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackInstrumentation { + private static final String ClassName = "android.app.Instrumentation"; + + private static final String Method_execStartActivity = "execStartActivity"; + private static final String Method_execStartActivities = "execStartActivities"; + private static final String Method_execStartActivitiesAsUser = "execStartActivitiesAsUser"; + private static final String Method_execStartActivityAsCaller = "execStartActivityAsCaller"; + private static final String Method_execStartActivityFromAppTask = "execStartActivityFromAppTask"; + + private Object instance; + + public HackInstrumentation(Object instance) { + this.instance = instance; + } + + public Instrumentation.ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivity, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent.class, int.class, Bundle.class }, new Object[] { who, contextThread, token, target, + intent, requestCode, options }); + + return (Instrumentation.ActivityResult) result; + } + + public void execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, + Intent[] intents, Bundle options) { + + RefInvoker + .invokeMethod(instance, Instrumentation.class.getName(), Method_execStartActivities , + new Class[]{Context.class, IBinder.class, IBinder.class, Activity.class, Intent[].class, + Bundle.class}, new Object[]{who, contextThread, token, target, intents, options}); + } + + public void execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, + Intent[] intents, Bundle options, int userId) { + + RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivitiesAsUser, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent[].class, Bundle.class, int.class }, new Object[] { who, contextThread, token, target, + intents, options, userId }); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public Instrumentation.ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, + Fragment target, Intent intent, int requestCode, Bundle options) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivity , new Class[] { Context.class, IBinder.class, IBinder.class, + Fragment.class, Intent.class, int.class, Bundle.class }, new Object[] { who, + contextThread, token, target, intent, requestCode, options }); + + return (Instrumentation.ActivityResult) result; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public Instrumentation.ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, UserHandle user) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivity, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent.class, int.class, Bundle.class, UserHandle.class }, new Object[] { who, contextThread, + token, target, intent, requestCode, options, user }); + + return (Instrumentation.ActivityResult) result; + } + + + ///////////// Android 4.0.4及以下 /////////////// + + public Instrumentation.ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivity, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent.class, int.class }, new Object[] { who, contextThread, + token, target, intent, requestCode }); + + return (Instrumentation.ActivityResult) result; + } + + public void execStartActivities(Context who, IBinder contextThread, + IBinder token, Activity target, Intent[] intents) { + + RefInvoker + .invokeMethod(instance, Instrumentation.class.getName(), Method_execStartActivities, + new Class[]{Context.class, IBinder.class, IBinder.class, Activity.class, Intent[].class}, + new Object[]{who, contextThread, token, target, intents}); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public Instrumentation.ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Fragment target, + Intent intent, int requestCode) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivity, new Class[] { Context.class, IBinder.class, IBinder.class, Fragment.class, + Intent.class, int.class }, new Object[] { who, contextThread, + token, target, intent, requestCode }); + + return (Instrumentation.ActivityResult) result; + } + + /////// For Android 5.1 + public Instrumentation.ActivityResult execStartActivityAsCaller( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, int userId) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivityAsCaller, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent.class, int.class, Bundle.class, int.class}, new Object[] { who, contextThread, + token, target, intent, requestCode, options, userId}); + + return (Instrumentation.ActivityResult)result; + } + + public void execStartActivityFromAppTask( + Context who, IBinder contextThread, Object appTask, + Intent intent, Bundle options) { + + try { + RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivityFromAppTask, new Class[]{Context.class, IBinder.class, + Class.forName("android.app.IAppTask"), Intent.class, Bundle.class,}, + new Object[]{who, contextThread, appTask, intent, options}); + } catch (ClassNotFoundException e) { + LogUtil.printException("HackInstrumentation.execStartActivityFromAppTask", e); + } + } + + //7.1? + public Instrumentation.ActivityResult execStartActivityAsCaller( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options, boolean ignoreTargetSecurity, + int userId) { + + Object result = RefInvoker.invokeMethod(instance, Instrumentation.class.getName(), + Method_execStartActivityAsCaller, new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, + Intent.class, int.class, Bundle.class, boolean.class, int.class}, new Object[] { who, contextThread, + token, target, intent, requestCode, options, ignoreTargetSecurity, userId}); + + return (Instrumentation.ActivityResult)result; + } +} diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidViewLayoutInflater.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLayoutInflater.java similarity index 70% rename from PluginCore/src/com/plugin/core/systemservice/AndroidViewLayoutInflater.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLayoutInflater.java index 4c63d153..b3346e64 100644 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidViewLayoutInflater.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLayoutInflater.java @@ -1,17 +1,39 @@ -package com.plugin.core.systemservice; +package com.limpoxe.fairy.core.android; import android.view.LayoutInflater; import android.view.View; -import com.plugin.util.RefInvoker; +import com.limpoxe.fairy.util.RefInvoker; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; -public class AndroidViewLayoutInflater { +public class HackLayoutInflater { - private static final String android_view_LayoutInflater_sConstructorMap = "sConstructorMap"; + private static final String ClassName = "android.view.LayoutInflater"; + + private static final String Field_sConstructorMap = "sConstructorMap"; + + private static final String Method_setPrivateFactory = "setPrivateFactory"; + + private Object instance; + + public HackLayoutInflater(LayoutInflater instance) { + this.instance = instance; + } + + public static Map getConstructorMap() { + return (Map)RefInvoker.getField(null, ClassName, Field_sConstructorMap); + } + + public static void setConstructorMap(Map map) { + RefInvoker.setField(null, ClassName, Field_sConstructorMap, map); + } + + public void setPrivateFactory(Object factory) { + RefInvoker.invokeMethod(instance, ClassName, Method_setPrivateFactory, new Class[]{LayoutInflater.Factory2.class}, new Object[]{factory}); + } private static final HashMap> sConstructorMap = new HashMap>(); @@ -29,11 +51,11 @@ public class AndroidViewLayoutInflater { * */ public static void installPluginCustomViewConstructorCache() { - Map cache = (Map)RefInvoker.getFieldObject(null, LayoutInflater.class, android_view_LayoutInflater_sConstructorMap); + Map cache = getConstructorMap(); if (cache != null) { ConstructorHashMap> newCacheMap = new ConstructorHashMap>(); newCacheMap.putAll(cache); - RefInvoker.setFieldObject(null, LayoutInflater.class, android_view_LayoutInflater_sConstructorMap, newCacheMap); + setConstructorMap(newCacheMap); } } @@ -42,7 +64,7 @@ public static class ConstructorHashMap extends HashMap { @Override public V put(K key, V value) { if (systemClassloader == null) { - systemClassloader = AndroidViewLayoutInflater.class.getClassLoader().getParent(); + systemClassloader = HackLayoutInflater.class.getClassLoader().getParent(); } Constructor constructor = (Constructor)value; // 如果是系统控件,才缓存。如果是自定义控件,无论是来自插件还是来自宿主,都不缓存 diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLoadedApk.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLoadedApk.java new file mode 100644 index 00000000..b436bcd6 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackLoadedApk.java @@ -0,0 +1,77 @@ +package com.limpoxe.fairy.core.android; + +import android.app.Application; +import android.content.res.Resources; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.io.File; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackLoadedApk { + private static final String ClassName = "android.app.LoadedApk"; + + private static final String Field_mApplication = "mApplication"; + private static final String Field_mResources = "mResources"; + private static final String Field_mDataDirFile = "mDataDirFile"; + private static final String Field_mDataDir = "mDataDir"; + private static final String Field_mLibDir = "mLibDir"; + private static final String Field_mClassLoader = "mClassLoader"; + private static final String Field_mActivityThread = "mActivityThread"; + private static final String Field_mAppComponentFactory = "mAppComponentFactory"; + private static final String Field_mReceivers = "mReceivers"; + + private Object instance; + + public HackLoadedApk(Object instance) { + this.instance = instance; + } + + public void setApplication(Application pluginApplication) { + RefInvoker.setField(instance, ClassName, Field_mApplication, pluginApplication); + } + + public void setResources(Resources pluginResource) { + RefInvoker.setField(instance, ClassName, Field_mResources, pluginResource); + } + + public void setDataDirFile(File dirFile) { + RefInvoker.setField(instance, ClassName, Field_mDataDirFile, dirFile); + } + + public void setDataDir(String dataDir) { + RefInvoker.setField(instance, ClassName, Field_mDataDir, dataDir); + } + + public void setLibDir(String libDir) { + RefInvoker.setField(instance, ClassName, Field_mLibDir, libDir); + } + + public ClassLoader getClassLoader() { + return (ClassLoader) RefInvoker.getField(instance, ClassName, Field_mClassLoader); + } + + public void setClassLoader(ClassLoader classLoader) { + RefInvoker.setField(instance, ClassName, Field_mClassLoader, classLoader); + } + + public Object getActivityThread() { + return RefInvoker.getField(instance, ClassName, Field_mActivityThread); + } + + public Object getReceivers() { + return RefInvoker.getField(instance, ClassName, Field_mReceivers); + } + +// @TargetApi(28) +// public AppComponentFactory getAppComponentFactory() { +// return (AppComponentFactory)RefInvoker.getField(instance, ClassName, Field_mAppComponentFactory); +// } + +// public void setAppComponentFactory(AppComponentFactory appComponentFactory) { +// RefInvoker.setField(instance, ClassName, Field_mAppComponentFactory, appComponentFactory); +// } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackNotificationManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackNotificationManager.java new file mode 100644 index 00000000..629f244d --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackNotificationManager.java @@ -0,0 +1,24 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackNotificationManager { + + private static final String ClassName = "android.app.NotificationManager"; + + private static final String Method_getService = "getService"; + + private static final String Field_sService = "sService"; + + public static Object getService() { + return RefInvoker.invokeMethod(null, ClassName, Method_getService, (Class[])null, (Object[])null); + } + + public static void setService(Object serv) { + RefInvoker.setField(null, ClassName, Field_sService, serv); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackParceledListSlice.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackParceledListSlice.java new file mode 100644 index 00000000..4b61cd3d --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackParceledListSlice.java @@ -0,0 +1,37 @@ +package com.limpoxe.fairy.core.android; + +import android.content.pm.ResolveInfo; +import android.os.Build; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.util.List; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackParceledListSlice { + + private static final String ClassName = "android.content.pm.ParceledListSlice"; + + private static final String Method_getList = "getList"; + + private Object instance; + + public HackParceledListSlice(Object instance) { + this.instance = instance; + } + + public Object getList() { + return RefInvoker.invokeMethod(instance, ClassName, Method_getList, (Class[])null, (Object[])null); + } + + public static Object newParecledListSlice(List itemList) { + if (Build.VERSION.SDK_INT >= 21) { + return RefInvoker.newInstance(ClassName, new Class[]{List.class}, new Object[]{itemList}); + } else { + return null; + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackPendingIntent.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackPendingIntent.java new file mode 100644 index 00000000..c6135f52 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackPendingIntent.java @@ -0,0 +1,26 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Intent; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackPendingIntent { + + private static final String ClassName = "android.app.PendingIntent"; + + private static final String Method_getIntent = "getIntent"; + + private Object instance; + + public HackPendingIntent(Object instance) { + this.instance = instance; + } + + public Intent getIntent() { + return (Intent)RefInvoker.invokeMethod(instance, ClassName, Method_getIntent, (Class[]) null, (Object[]) null); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackReceiverData.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackReceiverData.java new file mode 100644 index 00000000..5db51331 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackReceiverData.java @@ -0,0 +1,35 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Intent; +import android.content.pm.ActivityInfo; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackReceiverData { + private static final String ClassName = "android.app.ActivityThread$ReceiverData"; + + private static final String Field_intent = "intent"; + private static final String Field_info = "info"; + + private Object instance; + + public HackReceiverData(Object instance) { + this.instance = instance; + } + + public Intent getIntent() { + return (Intent)RefInvoker.getField(instance, ClassName, Field_intent); + } + + public void setIntent(Intent intent) { + RefInvoker.setField(instance, ClassName, Field_intent, intent); + } + + public ActivityInfo getInfo() { + return (ActivityInfo)RefInvoker.getField(instance, ClassName, Field_info); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackRemoteViews.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackRemoteViews.java new file mode 100644 index 00000000..d08fb70b --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackRemoteViews.java @@ -0,0 +1,39 @@ +package com.limpoxe.fairy.core.android; + +import android.content.pm.ApplicationInfo; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackRemoteViews { + private static final String ClassName = "android.widget.RemoteViews"; + + private static final String Field_mLayoutId = "mLayoutId"; + private static final String Field_mApplication = "mApplication"; + private static final String Field_mPackage = "mPackage"; + + private Object instance; + + public HackRemoteViews(Object instance) { + this.instance = instance; + } + + public Integer getLayoutId() { + return (Integer)RefInvoker.getField(instance, ClassName, Field_mLayoutId); + } + + public void setLayoutId(int layoutId) { + RefInvoker.setField(instance, ClassName, Field_mLayoutId, new Integer(layoutId)); + } + + public void setApplicationInfo(ApplicationInfo info) { + RefInvoker.setField(instance, ClassName, Field_mApplication, info); + } + + public void setPackage(String packageName) { + RefInvoker.setField(instance, ClassName, Field_mPackage, packageName); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackResources.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackResources.java new file mode 100644 index 00000000..a206bba6 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackResources.java @@ -0,0 +1,19 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackResources { + private static final String ClassName = "android.content.res.Resources"; + + private static final String Method_selectDefaultTheme = "selectDefaultTheme"; + + public static Integer selectDefaultTheme(int mThemeResource, + int targetSdkVersion) { + return (Integer) RefInvoker.invokeMethod(null, ClassName, Method_selectDefaultTheme, new Class[]{ + int.class, int.class}, new Object[]{mThemeResource, targetSdkVersion}); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackService.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackService.java new file mode 100644 index 00000000..37f4a0e0 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackService.java @@ -0,0 +1,76 @@ +package com.limpoxe.fairy.core.android; + +import android.app.Application; +import android.content.Context; +import android.os.IBinder; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackService extends HackContextWrapper { + private static final String ClassName = "android.app.Service"; + + private static final String Field_mApplication = "mApplication"; + private static final String Field_mClassName = "mClassName"; + private static final String Field_mThread = "mThread"; + private static final String Field_mToken = "mToken"; + private static final String Field_mActivityManager = "mActivityManager"; + private static final String Field_mStartCompatibility = "mStartCompatibility"; + + private static final String Method_attach = "attach"; + + public HackService(Object instance) { + super(instance); + } + + public void attach(Context mBaseContext, + Object mThread, + String mClassName, + IBinder mToken, + Application mApplication, + Object mActivityManager) { + + RefInvoker.invokeMethod(instance, ClassName, Method_attach, + new Class[]{Context.class, + HackActivityThread.clazz(), String.class, IBinder.class, + Application.class, Object.class}, + new Object[]{mBaseContext, mThread, mClassName, mToken, + mApplication, mActivityManager}); + + } + + public void setApplication(Application application) { + RefInvoker.setField(instance, ClassName, Field_mApplication, application); + } + + public void setClassName(String name) { + RefInvoker.setField(instance, ClassName, Field_mClassName, name); + } + + public String getClassName() { + return (String)RefInvoker.getField(instance, ClassName, Field_mClassName); + } + + public Object getThread() { + return RefInvoker.getField(instance, ClassName, Field_mThread); + } + + public IBinder getToken() { + return (IBinder)RefInvoker.getField(instance, ClassName, Field_mToken); + } + + public Object getActivityManager() { + return RefInvoker.getField(instance, ClassName, Field_mActivityManager); + } + + public Boolean getStartCompatibility() { + return (Boolean)RefInvoker.getField(instance, ClassName, Field_mStartCompatibility); + } + + public void setStartCompatibility(Boolean compat) { + RefInvoker.setField(instance, ClassName, Field_mStartCompatibility, compat); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackServiceManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackServiceManager.java new file mode 100644 index 00000000..aec21edf --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackServiceManager.java @@ -0,0 +1,37 @@ +package com.limpoxe.fairy.core.android; + +import android.os.IBinder; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.util.Map; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackServiceManager { + private static final String ClassName = "android.os.ServiceManager"; + + private static final String Field_sServiceManager = "sServiceManager"; + private static final String Field_sCache = "sCache"; + + private static final String Method_getIServiceManager = "getIServiceManager"; + private static final String Method_getService = "getService"; + + public static Object getIServiceManager() { + return RefInvoker.invokeMethod(null, ClassName, Method_getIServiceManager, (Class[])null, (Object[])null); + } + + public static void setServiceManager(Object serviceManager) { + RefInvoker.setField(null, ClassName, Field_sServiceManager, serviceManager); + } + + public static Map getCache() { + return (Map)RefInvoker.getField(null, ClassName, Field_sCache); + } + + public static IBinder getService(String name) { + return (IBinder)RefInvoker.invokeMethod(null, ClassName, Method_getService, new Class[]{String.class}, new Object[]{name}); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSingleton.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSingleton.java new file mode 100644 index 00000000..da3430aa --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSingleton.java @@ -0,0 +1,24 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackSingleton { + private static final String ClassName = "android.util.Singleton"; + + private static final String Field_mInstance = "mInstance"; + + private Object instance; + + public HackSingleton(Object instance) { + this.instance = instance; + } + + public void setInstance(Object object) { + RefInvoker.setField(instance, ClassName, Field_mInstance, object); + + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSupportV4LocalboarcastManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSupportV4LocalboarcastManager.java new file mode 100644 index 00000000..0f3077d3 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackSupportV4LocalboarcastManager.java @@ -0,0 +1,47 @@ +package com.limpoxe.fairy.core.android; + +import android.content.BroadcastReceiver; +import android.content.IntentFilter; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackSupportV4LocalboarcastManager { + + /** + * 实际上项目迁移到AndroidX之后,最新的AGP脚本会自动在编译时通过修改字节码的方式将这个字符串修改为AndroidX的 + * 通过debug可以看到这个字段已经被改变了 + */ + private static final String ClassName = "android.support.v4.content.LocalBroadcastManager"; + + private static final String Field_mInstance = "mInstance"; + private static final String Field_mReceivers = "mReceivers"; + + private static final String Method_unregisterReceiver = "unregisterReceiver"; + + private Object instance ; + + public HackSupportV4LocalboarcastManager(Object instance) { + this.instance = instance; + } + + public static Object getInstance() { + return RefInvoker.getField(null, ClassName, Field_mInstance); + } + + public HashMap> getReceivers() { + return (HashMap>)RefInvoker.getField(instance, ClassName, Field_mReceivers); + } + + public void unregisterReceiver(BroadcastReceiver item) { + RefInvoker.invokeMethod(instance, ClassName, Method_unregisterReceiver, new Class[]{BroadcastReceiver.class}, new Object[]{item}); + + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackToast.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackToast.java new file mode 100644 index 00000000..b7f271fd --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackToast.java @@ -0,0 +1,21 @@ +package com.limpoxe.fairy.core.android; + +import com.limpoxe.fairy.util.RefInvoker; + +public class HackToast { + + private static final String ClassName = "android.widget.Toast"; + + private static final String Field_INotificationManager_sService = "sService"; + + private static final String Method_sService = "getService"; + + public static Object getService() { + return RefInvoker.invokeMethod(null, ClassName, Method_sService, (Class[])null, (Object[])null); + } + + public static void setService(Object inotificationManager) { + RefInvoker.setField(null, ClassName, Field_INotificationManager_sService, inotificationManager); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWebViewFactory.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWebViewFactory.java new file mode 100644 index 00000000..ed705e3e --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWebViewFactory.java @@ -0,0 +1,31 @@ +package com.limpoxe.fairy.core.android; + +import android.content.pm.PackageInfo; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackWebViewFactory { + + private static final String ClassName = "android.webkit.WebViewFactory"; + + private static final String Field_sProviderInstance = "sProviderInstance"; + + private static final String Method_getProvider = "getProvider"; + private static final String Method_getLoadedPackageInfo = "getLoadedPackageInfo"; + + public static Object getProvider() { + return RefInvoker.invokeMethod(null, ClassName, Method_getProvider, (Class[]) null, (Object[]) null); + } + + public static void setProviderInstance(Object provider) { + RefInvoker.setField(null, ClassName, Field_sProviderInstance, provider); + } + + public static PackageInfo getLoadedPackageInfo() { + return (PackageInfo) RefInvoker.invokeMethod(null, ClassName, Method_getLoadedPackageInfo, (Class[]) null, (Object[]) null); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWindow.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWindow.java new file mode 100644 index 00000000..700c3bc1 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/android/HackWindow.java @@ -0,0 +1,37 @@ +package com.limpoxe.fairy.core.android; + +import android.content.Context; +import android.view.LayoutInflater; + +import com.limpoxe.fairy.util.RefInvoker; + +/** + * Created by cailiming on 16/10/30. + */ + +public class HackWindow { + private static final String ClassName = "android.view.Window"; + + private static final String Field_mContext = "mContext"; + private static final String Field_mWindowStyle = "mWindowStyle"; + private static final String Field_mLayoutInflater = "mLayoutInflater"; + + private Object instance; + + public HackWindow(Object instance) { + this.instance = instance; + } + + public void setContext(Context context) { + RefInvoker.setField(instance, ClassName, Field_mContext, context); + } + + public void setWindowStyle(Object style) { + RefInvoker.setField(instance, ClassName, Field_mWindowStyle, style); + } + + public void setLayoutInflater(String className, LayoutInflater layoutInflater) { + RefInvoker.setField(instance, className, Field_mLayoutInflater, layoutInflater); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/AnnotationProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/AnnotationProcessor.java new file mode 100644 index 00000000..3f25025f --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/AnnotationProcessor.java @@ -0,0 +1,10 @@ +package com.limpoxe.fairy.core.annotation; + +public class AnnotationProcessor { + + public static PluginContainer getPluginContainer(Class clazz) { + PluginContainer container = (PluginContainer)clazz.getAnnotation(PluginContainer.class); + return container; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/PluginContainer.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/PluginContainer.java new file mode 100644 index 00000000..100549b6 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/annotation/PluginContainer.java @@ -0,0 +1,21 @@ +package com.limpoxe.fairy.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 在activity中标记这个注解, + * 用来通知插件框架,这个activity需要替换上下文,用来嵌入来自其他插件的组件 + * 例如这个宿主Activity需要内嵌插件Fragment/View时; + * 同时配置了这个注解的Activity需要运行再插件进程中 + */ + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface PluginContainer { + public String pluginId() default ""; +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProvider.java new file mode 100644 index 00000000..4fc3248c --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProvider.java @@ -0,0 +1,116 @@ +package com.limpoxe.fairy.core.bridge; + +import static com.limpoxe.fairy.core.bridge.ProviderClientUnsafeProxy.TARGET_URL; + +import android.annotation.TargetApi; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.util.Log; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.manager.PluginManagerProvider; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.FileNotFoundException; + +public class PluginShadowProvider extends ContentProvider { + + private static Uri CONTENT_URI; + + public static Uri buildUri() { + if (CONTENT_URI == null) { + CONTENT_URI = Uri.parse("content://"+ FairyGlobal.getHostApplication().getPackageName() + ".bridge" + "/"); + } + return CONTENT_URI; + } + + public PluginShadowProvider() { + Log.e("PluginShadowProvider", "create instance"); + } + + @Override + public boolean onCreate() { + Log.d("PluginShadowProvider", "onCreate, Thread id " + Thread.currentThread().getId() + " name " + Thread.currentThread().getName() + " pid " + Process.myPid()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("query", targetUrl.toString()); + return getContext().getContentResolver().query(targetUrl, projection, selection, selectionArgs, sortOrder); + } + + @TargetApi(Build.VERSION_CODES.O) + @Override + public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("query", targetUrl.toString()); + return getContext().getContentResolver().query(targetUrl, projection, queryArgs, cancellationSignal); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("query", targetUrl.toString()); + return getContext().getContentResolver().query(targetUrl, projection, selection, selectionArgs, sortOrder, cancellationSignal); + } + + @Override + public String getType(Uri uri) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("getType", targetUrl.toString()); + return getContext().getContentResolver().getType(targetUrl); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("insert", targetUrl.toString()); + return getContext().getContentResolver().insert(targetUrl, values); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("delete", targetUrl.toString()); + return getContext().getContentResolver().delete(targetUrl, selection, selectionArgs); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("update", targetUrl.toString()); + return getContext().getContentResolver().update(targetUrl, values, selection, selectionArgs); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + Uri targetUrl = Uri.parse(uri.getQueryParameter(TARGET_URL)); + LogUtil.d("openFile", targetUrl.toString()); + return getContext().getContentResolver().openFileDescriptor(targetUrl, mode); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (extras != null && extras.getParcelable(TARGET_URL) != null) { + Uri targetUrl = extras.getParcelable(TARGET_URL); + LogUtil.d("call", targetUrl.toString()); + //安全防范 + if (!targetUrl.getAuthority().contains(PluginManagerProvider.buildUri().getAuthority())) { + return getContext().getContentResolver().call(targetUrl, method, arg, extras); + } + } + return null; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProviderClient.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProviderClient.java new file mode 100644 index 00000000..2fd1ccb7 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowProviderClient.java @@ -0,0 +1,126 @@ +package com.limpoxe.fairy.core.bridge; + +import static com.limpoxe.fairy.core.bridge.ProviderClientUnsafeProxy.TARGET_URL; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.compat.CompatForContentProvider; +import com.limpoxe.fairy.util.LogUtil; + +public class PluginShadowProviderClient { + + private static Uri buildUri(Uri url) { + return PluginShadowProvider.buildUri().buildUpon().appendQueryParameter(TARGET_URL, url.toString()).build(); + } + + public static Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.query(newUri, projection, selection, selectionArgs, sortOrder); + } catch (Exception e) { + LogUtil.printException("query " + url, e); + } + return null; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public static Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.query(newUri, projection, selection, selectionArgs, sortOrder, cancellationSignal); + } catch (Exception e) { + LogUtil.printException("query " + url, e); + } + return null; + } + + @TargetApi(Build.VERSION_CODES.O) + public static Cursor query(Uri url, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.query(newUri, projection, queryArgs, cancellationSignal); + } catch (Exception e) { + LogUtil.printException("query " + url, e); + } + return null; + } + + public static String getType(Uri url) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.getType(newUri); + } catch (Exception e) { + LogUtil.printException("getType " + url, e); + } + return null; + } + + public static Uri insert(Uri url, ContentValues contentValues) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.insert(newUri, contentValues); + } catch (Exception e) { + LogUtil.printException("insert " + url, e); + } + return null; + } + + public static int delete(Uri url, String where, String[] selectionArgs) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.delete(newUri, where, selectionArgs); + } catch (Exception e) { + LogUtil.printException("delete " + url, e); + } + return -1; + } + + public static int update(Uri url, ContentValues values, String where, String[] selectionArgs) { + Uri newUri = buildUri(url); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.update(newUri, values, where, selectionArgs); + } catch (Exception e) { + LogUtil.printException("update " + url, e); + } + return -1; + } + + public static ParcelFileDescriptor openFile(Uri uri, String mode) { + Uri newUri = buildUri(uri); + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + try { + return resolver.openFileDescriptor(newUri, mode); + } catch (Exception e) { + LogUtil.printException("openFile " + uri + ", " + newUri, e); + } + return null; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static Bundle call(Uri url, String method, String arg, Bundle extras) { + Uri newUri = buildUri(url); + if(extras == null) { + extras = new Bundle(); + } + //newUri里面的TARGET_URL参数在转到provider的call函数后会丢失,所以call函数的url需要放到extras中 + extras.putParcelable(TARGET_URL, url); + return CompatForContentProvider.call(newUri, method, arg, extras); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowService.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowService.java new file mode 100644 index 00000000..7250179f --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/PluginShadowService.java @@ -0,0 +1,100 @@ +package com.limpoxe.fairy.core.bridge; + +import android.app.Application; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import com.limpoxe.fairy.core.PluginInjector; +import com.limpoxe.fairy.core.PluginIntentResolver; +import com.limpoxe.fairy.core.PluginLoader; +import com.limpoxe.fairy.core.android.HackContextImpl; +import com.limpoxe.fairy.core.android.HackService; +import com.limpoxe.fairy.util.LogUtil; + +/** + * 此类用于修正service的中的context + */ +public class PluginShadowService extends Service { + + public Context mBaseContext = null; + public Object mThread = null; + public String mClassName = null; + public IBinder mToken = null; + public Application mApplication = null; + public Object mActivityManager = null; + public Boolean mStartCompatibility = false; + + public Service realService; + + public PluginShadowService() { + LogUtil.d("PluginShadowService()"); + } + + @Override + public void onCreate() { + super.onCreate(); + + getAttachParam(); + + callServiceOnCreate(); + } + + private void getAttachParam() { + mBaseContext = getBaseContext(); + HackService hackService = new HackService(this); + mThread = hackService.getThread(); + mClassName = hackService.getClassName(); + mToken = hackService.getToken(); + mApplication = getApplication(); + mActivityManager = hackService.getActivityManager(); + mStartCompatibility = hackService.getStartCompatibility(); + } + + private void callServiceOnCreate() { + String realName = mClassName; + try { + realName = mClassName.replace(PluginIntentResolver.CLASS_PREFIX_SERVICE, ""); + LogUtil.v("className ", mClassName, "target", realName); + Class clazz = PluginLoader.loadPluginClassByName(realName); + realService = (Service) clazz.newInstance(); + } catch (Exception e) { + LogUtil.e("callServiceOnCreate", "Unable to instantiate service " + mClassName + + ", realName " + realName + " : " + e.toString()); + } + + if(realService == null) { + return; + } + + try { + new HackContextImpl(mBaseContext).setOuterContext(realService); + HackService hackService = new HackService(realService); + hackService.attach(mBaseContext, mThread, mClassName, mToken, mApplication, mActivityManager); + hackService.setStartCompatibility(mStartCompatibility); + + //拿到创建好的service,重新 设置mBase和mApplicaiton + PluginInjector.replacePluginServiceContext(realName, realService); + + realService.onCreate(); + } catch (Exception e) { + LogUtil.e("callServiceOnCreate", "Unable to create service " + mClassName + + ": " + e.toString()); + } + } + + @Override + public IBinder onBind(Intent intent) { + LogUtil.d("onBind", "PluginShadowService -> " + mClassName); + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // AndroidAppIActivityManager.serviceDoneExecuting会将realService替换ApplicationThread中的mServices的内容 + // 如果PluginShadowService的onStartCommand被触发则说明realService替换失败了 + LogUtil.e("onStartCommand", "PluginShadowService should not call onStartCommand! -> " + mClassName); + return super.onStartCommand(intent, flags, startId); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/ProviderClientUnsafeProxy.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/ProviderClientUnsafeProxy.java new file mode 100644 index 00000000..7b9b6a5f --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/bridge/ProviderClientUnsafeProxy.java @@ -0,0 +1,171 @@ +package com.limpoxe.fairy.core.bridge; + +import static com.limpoxe.fairy.manager.PluginManifestParser.PREVIOUS; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import java.io.FileNotFoundException; + +public class ProviderClientUnsafeProxy extends ContentProvider { + /** + * @see com.limpoxe.fairy.core.proxy.systemservice.AndroidAppIActivityManager.getContentProvider + */ + static final String TARGET_URL = "targetUrl"; + + private ProviderInfo mProviderInfo; + private String mAuthority = null; + + public ProviderClientUnsafeProxy() { + Log.d("ProviderUnsafeProxy", "create unsafe provider proxy instance"); + } + + @Override + public boolean onCreate() { + mAuthority = (String)RefInvoker.getField(this, this.getClass(), "mAuthority"); + Log.d("ProviderUnsafeProxy", "onCreate called " + mAuthority); + return false; + } + + @Override + public void attachInfo(Context context, ProviderInfo info) { + mProviderInfo = info; + LogUtil.d("attachInfo", info.authority, info.name); + super.attachInfo(context, info); + } + + private Uri buildUri(Uri uri) { + if (getClass().isMemberClass()) { + Uri target = Uri.parse(uri.toString().replace(uri.getHost(), PREVIOUS + uri.getHost())); + return target; + } else { + return uri; + } + } + + @Override + public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) { + LogUtil.d("query", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.query(buildUri(uri), strings, s, strings1, s1); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { + LogUtil.d("query", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.query(buildUri(uri), projection, queryArgs, cancellationSignal); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + LogUtil.d("query", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.query(buildUri(uri), projection, selection, selectionArgs, sortOrder, cancellationSignal); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public String getType(Uri uri) { + LogUtil.d("getType", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.getType(buildUri(uri)); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + LogUtil.d("insert", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.insert(buildUri(uri), contentValues); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public int delete(Uri uri, String s, String[] strings) { + LogUtil.d("delete", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.delete(buildUri(uri), s, strings); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { + LogUtil.d("update", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.update(buildUri(uri), contentValues, s, strings); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + LogUtil.d("openFile", uri); + long tokoen = Binder.clearCallingIdentity(); + try { + return PluginShadowProviderClient.openFile(buildUri(uri), mode); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + Uri uri = buildUri(Uri.parse("content://" + mProviderInfo.authority)); + long tokoen = Binder.clearCallingIdentity(); + try { + if (extras == null) { + extras = new Bundle(); + } + extras.putParcelable(TARGET_URL, uri); + return PluginShadowProviderClient.call(uri, method, arg, extras); + } finally { + Binder.restoreCallingIdentity(tokoen); + } + } + + public static class ProviderClientUnsafeProxy0 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy1 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy2 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy3 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy4 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy5 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy6 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy7 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy8 extends ProviderClientUnsafeProxy {}; + public static class ProviderClientUnsafeProxy9 extends ProviderClientUnsafeProxy {}; +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForContentProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForContentProvider.java new file mode 100644 index 00000000..f6fdd20f --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForContentProvider.java @@ -0,0 +1,50 @@ +package com.limpoxe.fairy.core.compat; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackContentProviderClient; +import com.limpoxe.fairy.core.android.HackIContentProvider; +import com.limpoxe.fairy.util.LogUtil; + +/** + * Created by cailiming on 16/4/14. + */ +public class CompatForContentProvider { + + public static Bundle call(Uri uri, String method, String arg, Bundle extras) { + + ContentResolver resolver = FairyGlobal.getHostApplication().getContentResolver(); + + if (Build.VERSION.SDK_INT >= 11) { + try { + return resolver.call(uri, method, arg, extras); + } catch (Exception e) { + LogUtil.e("call uri fail", uri, method, arg, extras); + } + return null; + } else { + ContentProviderClient client = resolver.acquireContentProviderClient(uri); + if (client == null) { + throw new IllegalArgumentException("Unknown URI " + uri); + } + try { + HackContentProviderClient hackContentProviderClient = new HackContentProviderClient(client); + Object mContentProvider = hackContentProviderClient.getContentProvider(); + if (mContentProvider != null) { + //public Bundle call(String method, String request, Bundle args) + Object result = new HackIContentProvider(mContentProvider).call(method, arg, extras); + return (Bundle) result; + } + + } finally { + client.release(); + } + return null; + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForFragmentClassCache.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForFragmentClassCache.java new file mode 100644 index 00000000..96568136 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForFragmentClassCache.java @@ -0,0 +1,162 @@ +package com.limpoxe.fairy.core.compat; + +import android.content.Context; + +import androidx.collection.SimpleArrayMap; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.util.HashMap; +import java.util.Map; + +/** + * for supportv7 + */ +public class CompatForFragmentClassCache { + + private static final String androidx_fragment_app_Fragment = "androidx.fragment.app.Fragment"; + private static final String androidx_fragment_app_Fragment_sClassMap = "sClassMap"; + + private static final String androidx_fragment_app_FragmentFactory = "androidx.fragment.app.FragmentFactory"; + private static final String androidx_fragment_app_FragmentFactory_sClassMap = "sClassCacheMap"; + + private static final String android_support_v4_app_Fragment = "android.support.v4.app.Fragment"; + private static final String android_support_v4_app_Fragment_sClassMap = "sClassMap"; + + private static final String android_app_Fragment = "android.app.Fragment"; + private static final String android_app_Fragment_sClassMap = "sClassMap"; + + //阻止class缓存 + public static void installFragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(android_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, android_app_Fragment_sClassMap); + if (slCassMap != null) { + //4.3及以下是 HashMap> + if (slCassMap.getClass().isAssignableFrom(HashMap.class)) { + RefInvoker.setField(null, FragmentClass, android_app_Fragment_sClassMap, new EmptyHashMap>()); + } else { + //4.4+ android.util.ArrayMap> + } + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.installFragmentClassCache", e); + } + } + + //阻止class缓存 + public static void installSupportV4FragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(android_support_v4_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, android_support_v4_app_Fragment_sClassMap); + if (slCassMap != null) { + //4.3及以下是 HashMap> + if (slCassMap.getClass().isAssignableFrom(HashMap.class)) { + RefInvoker.setField(null, FragmentClass, android_support_v4_app_Fragment_sClassMap, new EmptyHashMap>()); + } else { + //4.4+ android.support.v4.util.SimpleArrayMap> + } + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.installSupportV4FragmentClassCache", e); + } + } + + //阻止class缓存 + public static void installAndroidXFragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(androidx_fragment_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, androidx_fragment_app_Fragment_sClassMap); + if (slCassMap != null) { + if (slCassMap instanceof Map) { + RefInvoker.setField(null, FragmentClass, androidx_fragment_app_Fragment_sClassMap, new EmptyHashMap()); + } else if (slCassMap instanceof SimpleArrayMap) { + RefInvoker.setField(null, FragmentClass, androidx_fragment_app_Fragment_sClassMap, new EmptySimpleArrayMap()); + } + } else { + FragmentClass = Class.forName(androidx_fragment_app_FragmentFactory); + slCassMap = RefInvoker.getField(null, FragmentClass, androidx_fragment_app_FragmentFactory_sClassMap); + if (slCassMap != null) { + if (slCassMap instanceof Map) { + RefInvoker.setField(null, FragmentClass, androidx_fragment_app_FragmentFactory_sClassMap, new EmptyHashMap()); + } else if (slCassMap instanceof SimpleArrayMap) { + RefInvoker.setField(null, FragmentClass, androidx_fragment_app_FragmentFactory_sClassMap, new EmptySimpleArrayMap()); + } + } + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.installAndroidXFragmentClassCache", e); + } + } + + //清理class缓存 + public static void clearFragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(android_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, android_app_Fragment_sClassMap); + if (slCassMap != null) { + RefInvoker.invokeMethod(slCassMap, slCassMap.getClass(), "clear", (Class[])null, (Object[])null); + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.clearFragmentClassCache", e); + } + } + + //清理class缓存 + public static void clearSupportV4FragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(android_support_v4_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, android_support_v4_app_Fragment_sClassMap); + if (slCassMap != null) { + RefInvoker.invokeMethod(slCassMap, slCassMap.getClass(), "clear", (Class[])null, (Object[])null); + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.clearSupportV4FragmentClassCache", e); + } + } + + //清理class缓存 + public static void clearAndroidXFragmentClassCache() { + Class FragmentClass = null; + try { + FragmentClass = Class.forName(androidx_fragment_app_Fragment); + Object slCassMap = RefInvoker.getField(null, FragmentClass, androidx_fragment_app_Fragment_sClassMap); + if (slCassMap == null) { + FragmentClass = Class.forName(androidx_fragment_app_FragmentFactory); + slCassMap = RefInvoker.getField(null, FragmentClass, androidx_fragment_app_FragmentFactory_sClassMap); + } + if (slCassMap != null) { + RefInvoker.invokeMethod(slCassMap, slCassMap.getClass(), "clear", (Class[])null, (Object[])null); + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForFragmentClassCache.clearAndroidXFragmentClassCache", e); + } + } + + /** + * 提前将fragment类的缓存添加到fragment的缓存池中 + * 注意:这里提前缓存的逻辑和框架初始化时执行的阻止缓存的逻辑从冲突的 + * @param fragmentContext + * @param fname + */ + public static void forceCache(Context fragmentContext, String fname) { + try { + //框架并不知道实际可能是什么类型,所以都试一下 + //调用下面这几个函数,会触发函数内的缓存逻辑,将fname对应的class缓存到其内部的静态map中 + android.app.Fragment.instantiate(fragmentContext, fname, null); + RefInvoker.invokeMethod(null, android_support_v4_app_Fragment, + "isSupportFragmentClass",new Class[]{Context.class, String.class}, new Object[]{fragmentContext, fname}); + RefInvoker.invokeMethod(null, androidx_fragment_app_Fragment, + "isSupportFragmentClass",new Class[]{Context.class, String.class}, new Object[]{fragmentContext, fname}); + RefInvoker.invokeMethod(null, androidx_fragment_app_FragmentFactory, + "isFragmentClass",new Class[]{ClassLoader.class, String.class}, new Object[]{fragmentContext.getClassLoader(), fname}); + } catch (Exception e) { + //e.printStackTrace(); + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSharedPreferencesImpl.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSharedPreferencesImpl.java new file mode 100644 index 00000000..8d4162e2 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSharedPreferencesImpl.java @@ -0,0 +1,78 @@ +package com.limpoxe.fairy.core.compat; + +import com.limpoxe.fairy.util.LogUtil; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Created by cailiming on 16/4/14. + */ +public class CompatForSharedPreferencesImpl { + + private static Class SharedPreferencesImpl; + private static boolean has2 = true; + private static boolean has3 = true; + private static Constructor SharedPreferencesImpl_Constructor_2; + private static Constructor SharedPreferencesImpl_Constructor_3; + + public static Object newSharedPreferencesImpl(File prefsFile, int mode, String packageName) { + if (SharedPreferencesImpl == null) { + try { + SharedPreferencesImpl = Class.forName("android.app.SharedPreferencesImpl"); + } catch (ClassNotFoundException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + return null; + } + } + + if (has2 && SharedPreferencesImpl_Constructor_2 == null) { + try { + SharedPreferencesImpl_Constructor_2 = SharedPreferencesImpl.getDeclaredConstructor(File.class, int.class); + SharedPreferencesImpl_Constructor_2.setAccessible(true); + } catch (NoSuchMethodException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + has2 = false; + } + } + + if (SharedPreferencesImpl_Constructor_2 != null) { + try { + return SharedPreferencesImpl_Constructor_2.newInstance(prefsFile, mode); + } catch (InstantiationException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } catch (IllegalAccessException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } catch (InvocationTargetException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } + } + + //compat for moto 4.0.4 + if (has3 && SharedPreferencesImpl_Constructor_3 == null) { + try { + SharedPreferencesImpl_Constructor_3 = SharedPreferencesImpl.getDeclaredConstructor(File.class, int.class, String.class); + SharedPreferencesImpl_Constructor_3.setAccessible(true); + } catch (NoSuchMethodException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + has3 = false; + } + } + + if (SharedPreferencesImpl_Constructor_3 != null) { + try { + return SharedPreferencesImpl_Constructor_3.newInstance(prefsFile, mode, packageName); + } catch (InstantiationException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } catch (IllegalAccessException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } catch (InvocationTargetException e) { + LogUtil.printException("CompatForSharedPreferencesImpl.newSharedPreferencesImpl", e); + } + } + + return null; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7ViewInflater.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7ViewInflater.java new file mode 100644 index 00000000..71820682 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7ViewInflater.java @@ -0,0 +1,88 @@ +package com.limpoxe.fairy.core.compat; + +import android.view.View; + +import androidx.collection.SimpleArrayMap; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.lang.reflect.Constructor; +import java.util.Map; + +/** + * for supportv7 + */ +public class CompatForSupportv7ViewInflater { + + private static final String android_support_v7_app_AppCompatViewInflater = "android.support.v7.app.AppCompatViewInflater"; + private static final String android_support_v7_app_AppCompatViewInflater_sConstructorMap = "sConstructorMap"; + + private static final String androidx_app_AppCompatViewInflater = "androidx.appcompat.app.AppCompatViewInflater"; + private static final String androidx_app_AppCompatViewInflater_sConstructorMap = "sConstructorMap"; + + public static void installPluginCustomViewConstructorCache() { + Class AppCompatViewInflater = null; + try { + AppCompatViewInflater = Class.forName(androidx_app_AppCompatViewInflater); + Object cache = RefInvoker.getField(null, AppCompatViewInflater, + androidx_app_AppCompatViewInflater_sConstructorMap); + if (cache != null) { + if (cache instanceof Map) { + EmptyHashMap> newCacheMap = new EmptyHashMap>(); + newCacheMap.putAll((Map)cache); + RefInvoker.setField(null, AppCompatViewInflater, + androidx_app_AppCompatViewInflater_sConstructorMap, newCacheMap); + } else if (cache instanceof SimpleArrayMap) { + EmptySimpleArrayMap> newCacheMap = new EmptySimpleArrayMap>(); + newCacheMap.putAll((SimpleArrayMap)cache); + RefInvoker.setField(null, AppCompatViewInflater, + androidx_app_AppCompatViewInflater_sConstructorMap, newCacheMap); + } + return; + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForSupportv7ViewInflater.installAndroidXPluginCustomViewConstructorCache", e); + } + + try { + AppCompatViewInflater = Class.forName(android_support_v7_app_AppCompatViewInflater); + Object cache = RefInvoker.getField(null, AppCompatViewInflater, + android_support_v7_app_AppCompatViewInflater_sConstructorMap); + if (cache != null) { + if (cache instanceof Map) { + EmptyHashMap> newCacheMap = new EmptyHashMap>(); + newCacheMap.putAll((Map)cache); + RefInvoker.setField(null, AppCompatViewInflater, + android_support_v7_app_AppCompatViewInflater_sConstructorMap, newCacheMap); + } else if (cache instanceof SimpleArrayMap) { + EmptySimpleArrayMap> newCacheMap = new EmptySimpleArrayMap>(); + newCacheMap.putAll((SimpleArrayMap)cache); + RefInvoker.setField(null, AppCompatViewInflater, + android_support_v7_app_AppCompatViewInflater_sConstructorMap, newCacheMap); + } + return; + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForSupportv7ViewInflater.installPluginCustomViewConstructorCache", e); + } + } + + public static void clearViewInflaterConstructorCache() { + try { + Class AppCompatViewInflater = Class.forName(androidx_app_AppCompatViewInflater); + Object sConstructorMap = RefInvoker.getField(null, AppCompatViewInflater, + androidx_app_AppCompatViewInflater_sConstructorMap); + if (sConstructorMap == null) { + AppCompatViewInflater = Class.forName(android_support_v7_app_AppCompatViewInflater); + sConstructorMap = RefInvoker.getField(null, AppCompatViewInflater, + android_support_v7_app_AppCompatViewInflater_sConstructorMap); + } + if (sConstructorMap != null) { + RefInvoker.invokeMethod(sConstructorMap, sConstructorMap.getClass(), "clear", (Class[])null, (Object[])null); + } + } catch (ClassNotFoundException e) { + //LogUtil.printException("CompatForSupportv7ViewInflater.clearViewInflaterConstructorCache", e); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7_23_2.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7_23_2.java new file mode 100644 index 00000000..308386bc --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForSupportv7_23_2.java @@ -0,0 +1,78 @@ +package com.limpoxe.fairy.core.compat; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; + +import com.limpoxe.fairy.util.RefInvoker; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Created by cailiming on 16/4/14. + */ +public class CompatForSupportv7_23_2 { + + /** + * supportv7的23.2的版本中,AppCompatActivity这个类重写了getResource方法,返回了一个TintResources的对象 + * 这里需要特别处理一下, 否者接下来的setTheme方法会导致crash + * 其他版本,包括更低和更高版本的AppCompatActivity都没有重写这个方法. + *
+     *       public Resources getResources() {
+     *           if (mResources == null) {
+     *               mResources = new TintResources(this, super.getResources());
+     *           }
+     *           return mResources;
+     *       }
+     *  
+ * @param pluginContext + * @param activity + */ + public static void fixResource(Context pluginContext, Activity activity) { + try { + Class AppCompatActivity = pluginContext.getClassLoader().loadClass("android.support.v7.app.AppCompatActivity"); + if (AppCompatActivity != null && AppCompatActivity.isAssignableFrom(activity.getClass())) { + //判断Activity的getResource的类型是否为TintResources + Resources activiyResource = activity.getResources(); + Class TintResources = pluginContext.getClassLoader().loadClass("android.support.v7.widget.TintResources"); + if (TintResources != null && TintResources.isAssignableFrom(activiyResource.getClass())) { + RefInvoker.setField(activity, AppCompatActivity, "mResources", pluginContext.getResources()); + Class TintContextWrapper = pluginContext.getClassLoader().loadClass("android.support.v7.widget.TintContextWrapper"); + if (TintContextWrapper != null) { + Object sCache = (Object)RefInvoker.getField(null, TintContextWrapper, "sCache"); + if (!(sCache instanceof TintContextWrapperArrayList)) { + RefInvoker.setField(null, TintContextWrapper, "sCache", new TintContextWrapperArrayList(TintContextWrapper)); + } + } + } + } + } catch (ClassNotFoundException e) { + //nothing + } + } + + public static class TintContextWrapperArrayList extends ArrayList { + + private Class TintContextWrapper; + + public TintContextWrapperArrayList(Class TintContextWrapper) { + this.TintContextWrapper = TintContextWrapper; + } + + @Override + public boolean add(V object) { + WeakReference ref = (WeakReference)object; + Object tintContextWrapper = ref.get(); + if (tintContextWrapper != null) { + Resources resources = ((ContextWrapper)tintContextWrapper).getBaseContext().getResources(); + RefInvoker.setField(tintContextWrapper, TintContextWrapper, "mResources", resources); + Resources.Theme theme = resources.newTheme(); + theme.setTo(((ContextWrapper)tintContextWrapper).getBaseContext().getTheme()); + RefInvoker.setField(tintContextWrapper, TintContextWrapper, "mTheme", theme); + } + return super.add(object); + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForWebViewFactoryApi21.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForWebViewFactoryApi21.java new file mode 100644 index 00000000..0d5f7b11 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/CompatForWebViewFactoryApi21.java @@ -0,0 +1,114 @@ +package com.limpoxe.fairy.core.compat; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.Build; +import android.util.SparseArray; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackAssetManager; +import com.limpoxe.fairy.core.android.HackWebViewFactory; +import com.limpoxe.fairy.util.LogUtil; + +/** + * Created by cailiming on 16/10/23. + * not used + */ + +public class CompatForWebViewFactoryApi21 { + + public static void addWebViewAssets(AssetManager pluginAssetManager) { + if (!FairyGlobal.isLocalHtmlEnable()) { + return; + } + PackageInfo packageInfo = HackWebViewFactory.getLoadedPackageInfo(); + if (packageInfo != null) { + HackAssetManager hackAssetManager = new HackAssetManager(pluginAssetManager); + SparseArray packageIdentifiers = null; + //Android L 及以上AssetManager才有getAssignedPackageIdentifiers这个函数 + if (Build.VERSION.SDK_INT >= 21) { + packageIdentifiers = hackAssetManager.getAssignedPackageIdentifiers(); + //Beign:Just For Debug + HackAssetManager hackhostAssetManager = new HackAssetManager(FairyGlobal.getHostApplication().getAssets()); + SparseArray hostPackageIdentifiers = hackhostAssetManager.getAssignedPackageIdentifiers(); + printPackages(hostPackageIdentifiers); + LogUtil.v("------------------------------------"); + printPackages(packageIdentifiers); + //End:Just For Debug + } + //如果插件的AssetManager尚未添加webview的包,则补上。 + if (!isAdded(packageIdentifiers, packageInfo.packageName)) { + LogUtil.i("Loaded WebView Package : " + packageInfo.packageName + " version " + packageInfo.versionName + " (code " + packageInfo.versionCode + ")" + packageInfo.applicationInfo.sourceDir); + LogUtil.i("WebView logo " + packageInfo.applicationInfo.logo + ",icon " + packageInfo.applicationInfo.icon + ", labelRes " + packageInfo.applicationInfo.labelRes); + //TODO 由于目前的资源id分组方案是限制宿主范围,插件使用原生范围,因此如果webview也使用了原生的范围,则会和插件冲突 + //为避免webview的资源id和插件的资源id冲突,这里做个判断 + if (packageInfo.applicationInfo.icon != 0 && (packageInfo.applicationInfo.icon >> 24) != 0x7f) { + //Android System WebView + LogUtil.w("add webview assets " + packageInfo.applicationInfo.sourceDir); + hackAssetManager.addAssetPath(packageInfo.applicationInfo.sourceDir); + } else { + //Chrome + LogUtil.w("WebView Assets Not Added " + packageInfo.applicationInfo.sourceDir); + //TODO 既然宿主可以使用和自己相同packageId的webview,可能问题还是出在assets的添加顺序上。 + } + } + } else { + ApplicationInfo chrome = getWebViewPackage(); + if (chrome != null) { + String chromePath = chrome.sourceDir; + LogUtil.i("WebView logo " + chrome.logo + ",icon " + chrome.icon + ", labelRes" + chrome.labelRes + ", path " + chromePath); + if (chrome.icon != 0 && (chrome.icon >> 24) != 0x7f) { + //Android System WebView + LogUtil.w("add webview assets " + chromePath); + HackAssetManager hackAssetManager = new HackAssetManager(pluginAssetManager); + hackAssetManager.addAssetPath(chromePath); + } else { + //Chrome + LogUtil.w("WebView Assets Not Added " + chromePath); + //TODO 既然宿主可以使用和自己相同packageId的webview,可能问题还是出在assets的添加顺序上。 + } + } + } + } + + public static ApplicationInfo getWebViewPackage() { + if (!FairyGlobal.isLocalHtmlEnable()) { + return null; + } + if (Build.VERSION.SDK_INT >= 21) { + try { + Resources hostRes = FairyGlobal.getHostApplication().getResources(); + int packageNameResId = hostRes.getIdentifier("android:string/config_webViewPackageName", "string", "android"); + String chromePackagename = hostRes.getString(packageNameResId); + LogUtil.v("Webview PackageName", chromePackagename); + ApplicationInfo applicationInfo = FairyGlobal.getHostApplication().createPackageContext(chromePackagename, 0).getApplicationInfo(); + return applicationInfo; + } catch (Exception e) { + //ignore + } + } + return null; + } + + private static boolean isAdded(SparseArray packageIdentifiers, String packageName) { + if (packageIdentifiers != null) { + for (int i = 0; i < packageIdentifiers.size(); i++) { + final String name = packageIdentifiers.valueAt(i); + if (packageName.equals(name)) { + return true; + } + } + } + return false; + } + + private static void printPackages(SparseArray packageIdentifiers) { + if (packageIdentifiers != null) { + for (int i = 0; i < packageIdentifiers.size(); i++) { + LogUtil.v("packageIdentifiers", i, packageIdentifiers.valueAt(i)); + } + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptyHashMap.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptyHashMap.java new file mode 100644 index 00000000..5cb5ab90 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptyHashMap.java @@ -0,0 +1,13 @@ +package com.limpoxe.fairy.core.compat; + +import java.util.HashMap; + +public class EmptyHashMap extends HashMap { + + @Override + public V put(K key, V value) { + //不缓存 + return super.put(key, null); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptySimpleArrayMap.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptySimpleArrayMap.java new file mode 100644 index 00000000..bb93d6e3 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/compat/EmptySimpleArrayMap.java @@ -0,0 +1,25 @@ +package com.limpoxe.fairy.core.compat; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.SimpleArrayMap; + +public class EmptySimpleArrayMap extends SimpleArrayMap { + @Override + public V put(K key, V value) { + //put null + return super.put(key, null); + } + + @Nullable + @Override + public V putIfAbsent(K key, V value) { + //put null + return super.putIfAbsent(key, null); + } + + @Override + public void putAll(@NonNull SimpleArrayMap array) { + //do nothing + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotFoundError.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotFoundError.java new file mode 100644 index 00000000..85d307a9 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotFoundError.java @@ -0,0 +1,20 @@ +package com.limpoxe.fairy.core.exception; + +/** + * Created by cailiming on 16/11/18. + */ + +public class PluginNotFoundError extends Error { + + public PluginNotFoundError(String detailMessage) { + super(detailMessage); + } + + public PluginNotFoundError(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PluginNotFoundError(Throwable throwable) { + super(throwable); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotInitError.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotInitError.java new file mode 100644 index 00000000..926d563d --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginNotInitError.java @@ -0,0 +1,20 @@ +package com.limpoxe.fairy.core.exception; + +/** + * Created by cailiming on 16/11/18. + */ + +public class PluginNotInitError extends Error { + + public PluginNotInitError(String detailMessage) { + super(detailMessage); + } + + public PluginNotInitError(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PluginNotInitError(Throwable throwable) { + super(throwable); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginResInitError.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginResInitError.java new file mode 100644 index 00000000..1a963cdc --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/exception/PluginResInitError.java @@ -0,0 +1,20 @@ +package com.limpoxe.fairy.core.exception; + +/** + * Created by cailiming on 16/11/18. + */ + +public class PluginResInitError extends Error { + + public PluginResInitError(String detailMessage) { + super(detailMessage); + } + + public PluginResInitError(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PluginResInitError(Throwable throwable) { + super(throwable); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/loading/WaitForLoadingPluginActivity.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/loading/WaitForLoadingPluginActivity.java new file mode 100644 index 00000000..963f1e13 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/loading/WaitForLoadingPluginActivity.java @@ -0,0 +1,122 @@ +package com.limpoxe.fairy.core.loading; + +import android.app.Activity; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Window; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginIntentResolver; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +/** + * 这个页面要求尽可能的简单 + * Created by cailiming on 16/10/12. + */ + +public class WaitForLoadingPluginActivity extends Activity { + + private PluginDescriptor pluginDescriptor; + private String pluginClassName; + private Handler handler; + private long loadingAt = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + //由于在PluginInstrumentaionWrapper中,忽略了对WaitForLoadingPluginActivity的Context + //的处理,这里的savedInstanceState如果包含插件Fragment信息,会因Classloader没有更换导致 + //FragmentManager尝试自动恢复插件Fragment时出现ClassNotFound异常, + //这里直接将savedInstanceState置空,忽略之 + if (savedInstanceState != null) { + //不能调clear,会触发Bundle.unparcel,可能会导致classNotFound错误 + //savedInstanceState.clear(); + savedInstanceState = null; + } + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 是否需要全屏取决于上个页面是否为全屏, + // 目的是和上个页面保持一致, 否则被透视的页面会发生移动 + // getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + // WindowManager.LayoutParams.FLAG_FULLSCREEN); + + int resId = FairyGlobal.getLoadingResId(); + LogUtil.i("WaitForLoadingPluginActivity ContentView Id = " + resId); + + if (resId != 0) { + setContentView(resId); + } + handler = new Handler(Looper.getMainLooper()); + loadingAt = System.currentTimeMillis(); + + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + savedInstanceState.clear(); + } + super.onRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + LogUtil.i("WaitForLoadingPluginActivity Shown"); + if (pluginDescriptor != null && !PluginManagerHelper.isRunning(pluginDescriptor.getPackageName())) { + AsyncTask asyncTask = new AsyncTask() { + @Override + protected Object doInBackground(Object[] objects) { + PluginLauncher.instance().startPlugin(pluginDescriptor); + return null; + } + + @Override + protected void onPostExecute(Object o) { + long remainTime = (loadingAt + FairyGlobal.getMinLoadingTime()) - System.currentTimeMillis(); + LoadedPlugin loadedPlugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + if (loadedPlugin != null && loadedPlugin.pluginApplication != null) { + LogUtil.i("WaitForLoadingPluginActivity open target"); + LogUtil.e("注意,对首次启动的首屏Activity来说,进入了WaitFor界面后忽略了startActivityForResult"); + + Intent intent = getIntent(); + if (intent.getAction() != null && intent.getAction().contains(PluginIntentResolver.CLASS_SEPARATOR)) { + String[] targetClassName = intent.getAction().split(PluginIntentResolver.CLASS_SEPARATOR); + if (targetClassName.length >1) { + intent.setAction(targetClassName[1]); + } else { + intent.setAction(null); + } + } + intent.setClassName(pluginDescriptor.getPackageName(), pluginClassName); + //重新resoloveIntent,绑定stub + PluginIntentResolver.resolveActivity(intent); + + startActivity(intent); + finish(); + } else { + LogUtil.w("WTF!", pluginDescriptor, loadedPlugin); + finish(); + } + } + }; + asyncTask.execute(); + } else { + LogUtil.w("WTF!", pluginDescriptor); + finish(); + } + } + + public void setTargetPlugin(PluginDescriptor pluginDescriptor, String targetClassName) { + this.pluginDescriptor = pluginDescriptor; + this.pluginClassName = targetClassName; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/localservice/LocalServiceManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/localservice/LocalServiceManager.java new file mode 100644 index 00000000..79e939d6 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/localservice/LocalServiceManager.java @@ -0,0 +1,114 @@ +package com.limpoxe.fairy.core.localservice; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.support.servicemanager.ServiceManager; +import com.limpoxe.support.servicemanager.local.ServicePool; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Created by cailiming on 16/1/1. + */ +@Deprecated +public class LocalServiceManager { + + static boolean isSupport = false; + + static { + try { + Class ServiceManager = Class.forName("com.limpoxe.support.servicemanager.ServiceManager"); + isSupport = ServiceManager != null; + } catch (ClassNotFoundException e) { + LogUtil.e("ServiceManager was disabled"); + } + } + + public static void init() { + if (!isSupport) { + return; + } + ServiceManager.init(FairyGlobal.getHostApplication()); + } + + public static void registerService(PluginDescriptor plugin) { + if (!isSupport) { + return; + } + HashMap localServices = plugin.getFunctions(); + if (localServices != null) { + Iterator> serv = localServices.entrySet().iterator(); + while (serv.hasNext()) { + Map.Entry entry = serv.next(); + LocalServiceManager.registerService(plugin.getPackageName(), entry.getKey(), entry.getValue()); + } + } + } + + public static void registerService(final String pluginId, final String serviceName, final String serviceClass) { + if (!isSupport) { + return; + } + ServiceManager.publishService(serviceName, new ServicePool.ClassProvider() { + @Override + public Object getServiceInstance() { + + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginId); + if (plugin != null) { + try { + return plugin.pluginClassLoader.loadClass(serviceClass.split("\\|")[0]).newInstance(); + } catch (ClassNotFoundException e) { + LogUtil.printException("获取服务失败", e); + } catch (InstantiationException e) { + LogUtil.printException("LocalServiceManager.registerService", e); + } catch (IllegalAccessException e) { + LogUtil.printException("LocalServiceManager.registerService", e); + } + } else { + LogUtil.e("未找到插件", pluginId); + } + return null; + } + + @Override + public String getInterfaceName() { + return serviceClass.split("\\|")[1]; + } + }); + } + + public static Object getService(String name) { + if (!isSupport) { + return null; + } + return ServiceManager.getService(name); + } + + public static void unRegistService(PluginDescriptor plugin) { + if (!isSupport) { + return; + } + HashMap localServices = plugin.getFunctions(); + if (localServices != null) { + Iterator> serv = localServices.entrySet().iterator(); + while (serv.hasNext()) { + Map.Entry entry = serv.next(); + ServiceManager.unPublishService(entry.getKey()); + } + } + } + + public static void unRegistAll() { + if (!isSupport) { + return; + } + ServiceManager.unPublishAllService(); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/multidex/PluginMultiDexHelper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/multidex/PluginMultiDexHelper.java new file mode 100644 index 00000000..cc1454da --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/multidex/PluginMultiDexHelper.java @@ -0,0 +1,511 @@ +package com.limpoxe.fairy.core.multidex; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.ChangedPackages; +import android.content.pm.FeatureInfo; +import android.content.pm.InstrumentationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.VersionedPackage; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; + +import com.limpoxe.fairy.core.FairyGlobal; + +import java.util.List; + +public class PluginMultiDexHelper { + + public static PackageManager fixPackageManagerForMultDexInstaller(final String pluginPackageName, final PackageManager packageManager) { + + return new PackageManager() { + + @Override + public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException { + return null; + } + + //android-O + public PackageInfo getPackageInfo(VersionedPackage versionedPackage, int i) throws NameNotFoundException { + return null; + } + + @Override + public String[] currentToCanonicalPackageNames(String[] names) { + return new String[0]; + } + + @Override + public String[] canonicalToCurrentPackageNames(String[] names) { + return new String[0]; + } + + @Override + public Intent getLaunchIntentForPackage(String packageName) { + return null; + } + + @Override + public Intent getLeanbackLaunchIntentForPackage(String packageName) { + return null; + } + + @Override + public int[] getPackageGids(String packageName) throws NameNotFoundException { + return new int[0]; + } + + //android-N + public int[] getPackageGids(String s, int i) throws NameNotFoundException { + return new int[0]; + } + + //android-N + public int getPackageUid(String s, int i) throws NameNotFoundException { + return 0; + } + + @Override + public PermissionInfo getPermissionInfo(String name, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List queryPermissionsByGroup(String group, int flags) throws NameNotFoundException { + return null; + } + + @Override + public PermissionGroupInfo getPermissionGroupInfo(String name, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List getAllPermissionGroups(int flags) { + return null; + } + + @Override + public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException { + if (packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + packageName = pluginPackageName; + } + return packageManager.getApplicationInfo(packageName, flags); + } + + @Override + public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException { + return null; + } + + @Override + public ActivityInfo getReceiverInfo(ComponentName component, int flags) throws NameNotFoundException { + return null; + } + + @Override + public ServiceInfo getServiceInfo(ComponentName component, int flags) throws NameNotFoundException { + return null; + } + + @Override + public ProviderInfo getProviderInfo(ComponentName component, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List getInstalledPackages(int flags) { + return null; + } + + @Override + public List getPackagesHoldingPermissions(String[] permissions, int flags) { + return null; + } + + @Override + public int checkPermission(String permName, String pkgName) { + return PERMISSION_DENIED; + } + + @Override + public boolean isPermissionRevokedByPolicy(String permName, String pkgName) { + return false; + } + + @Override + public boolean addPermission(PermissionInfo info) { + return false; + } + + @Override + public boolean addPermissionAsync(PermissionInfo info) { + return false; + } + + @Override + public void removePermission(String name) { + + } + + @Override + public int checkSignatures(String pkg1, String pkg2) { + return PackageManager.SIGNATURE_MATCH; + } + + @Override + public int checkSignatures(int uid1, int uid2) { + return PackageManager.SIGNATURE_MATCH; + } + + @Override + public String[] getPackagesForUid(int uid) { + return new String[0]; + } + + @Override + public String getNameForUid(int uid) { + return null; + } + + @Override + public List getInstalledApplications(int flags) { + return null; + } + + //android-O + public boolean isInstantApp() { + return false; + } + + //android-O + public boolean isInstantApp(String s) { + return false; + } + + //android-O + public int getInstantAppCookieMaxBytes() { + return 0; + } + + //android-O + public byte[] getInstantAppCookie() { + return new byte[0]; + } + + //android-O + public void clearInstantAppCookie() { + + } + + //android-O + public void updateInstantAppCookie(byte[] bytes) { + + } + + @Override + public String[] getSystemSharedLibraryNames() { + return new String[0]; + } + + //android-O + public List getSharedLibraries(int i) { + return null; + } + + //android-O + public ChangedPackages getChangedPackages(int i) { + return null; + } + + @Override + public FeatureInfo[] getSystemAvailableFeatures() { + return new FeatureInfo[0]; + } + + @Override + public boolean hasSystemFeature(String name) { + return false; + } + + @Override + public ResolveInfo resolveActivity(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentActivities(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentActivityOptions(ComponentName caller, Intent[] specifics, Intent intent, int flags) { + return null; + } + + @Override + public List queryBroadcastReceivers(Intent intent, int flags) { + return null; + } + + @Override + public ResolveInfo resolveService(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentServices(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentContentProviders(Intent intent, int flags) { + return null; + } + + @Override + public ProviderInfo resolveContentProvider(String name, int flags) { + return null; + } + + @Override + public List queryContentProviders(String processName, int uid, int flags) { + return null; + } + + @Override + public InstrumentationInfo getInstrumentationInfo(ComponentName className, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List queryInstrumentation(String targetPackage, int flags) { + return null; + } + + @Override + public Drawable getDrawable(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public Drawable getActivityIcon(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityIcon(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityBanner(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityBanner(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getDefaultActivityIcon() { + return null; + } + + @Override + public Drawable getApplicationIcon(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationIcon(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getApplicationBanner(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationBanner(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityLogo(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityLogo(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getApplicationLogo(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationLogo(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getUserBadgedIcon(Drawable icon, UserHandle user) { + return null; + } + + @Override + public Drawable getUserBadgedDrawableForDensity(Drawable drawable, UserHandle user, Rect badgeLocation, int badgeDensity) { + return null; + } + + @Override + public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) { + return null; + } + + @Override + public CharSequence getText(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public CharSequence getApplicationLabel(ApplicationInfo info) { + return null; + } + + @Override + public Resources getResourcesForActivity(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException { + return null; + } + + @Override + public Resources getResourcesForApplication(String appPackageName) throws NameNotFoundException { + return null; + } + + @Override + public void verifyPendingInstall(int id, int verificationCode) { + + } + + @Override + public void extendVerificationTimeout(int id, int verificationCodeAtTimeout, long millisecondsToDelay) { + + } + + @Override + public void setInstallerPackageName(String targetPackage, String installerPackageName) { + + } + + @Override + public String getInstallerPackageName(String packageName) { + return null; + } + + @Override + public void addPackageToPreferred(String packageName) { + + } + + @Override + public void removePackageFromPreferred(String packageName) { + + } + + @Override + public List getPreferredPackages(int flags) { + return null; + } + + @Override + public void addPreferredActivity(IntentFilter filter, int match, ComponentName[] set, ComponentName activity) { + + } + + @Override + public void clearPackagePreferredActivities(String packageName) { + + } + + @Override + public int getPreferredActivities(List outFilters, List outActivities, String packageName) { + return 0; + } + + @Override + public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) { + + } + + @Override + public int getComponentEnabledSetting(ComponentName componentName) { + return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + } + + @Override + public void setApplicationEnabledSetting(String packageName, int newState, int flags) { + + } + + @Override + public int getApplicationEnabledSetting(String packageName) { + return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + } + + @Override + public boolean isSafeMode() { + return false; + } + + //android-O + public void setApplicationCategoryHint(String s, int i) { + + } + + @Override + public PackageInstaller getPackageInstaller() { + return null; + } + + //android-O + public boolean canRequestPackageInstalls() { + return false; + } + + //android-N + public boolean hasSystemFeature(String arg1, int agr2) { + return false; + } + }; + } +} diff --git a/PluginCore/src/com/plugin/core/proxy/MethodDelegate.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodDelegate.java similarity index 92% rename from PluginCore/src/com/plugin/core/proxy/MethodDelegate.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodDelegate.java index 752eec37..7d96be07 100644 --- a/PluginCore/src/com/plugin/core/proxy/MethodDelegate.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodDelegate.java @@ -1,4 +1,4 @@ -package com.plugin.core.proxy; +package com.limpoxe.fairy.core.proxy; import java.lang.reflect.Method; diff --git a/PluginCore/src/com/plugin/core/proxy/MethodHandler.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodHandler.java similarity index 82% rename from PluginCore/src/com/plugin/core/proxy/MethodHandler.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodHandler.java index be7ec251..35c81491 100644 --- a/PluginCore/src/com/plugin/core/proxy/MethodHandler.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodHandler.java @@ -1,4 +1,4 @@ -package com.plugin.core.proxy; +package com.limpoxe.fairy.core.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; @@ -6,9 +6,9 @@ public class MethodHandler extends MethodDelegate implements InvocationHandler { - private Object mTarget = null; + private final Object mTarget; - private MethodDelegate mDelegate; + private final MethodDelegate mDelegate; public MethodHandler(Object target, MethodDelegate delegate) { this.mTarget = target; @@ -26,7 +26,7 @@ public Object afterInvoke(Object target, Method method, Object[] args, Object be } @Override - public synchronized Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { + public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { Object before = beforeInvoke(mTarget, method, args); Object invokeResult = null; diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodProxy.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodProxy.java new file mode 100644 index 00000000..0c6adb1c --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/MethodProxy.java @@ -0,0 +1,43 @@ +package com.limpoxe.fairy.core.proxy; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by cailiming on 16/1/15. + */ +public abstract class MethodProxy extends MethodDelegate { + + /** + * 为了省事,这里做成静态map,但是有一定的风险,如果两个系统服务定义了相同的方法名称,可能会导致proxy中命中错误的方法 + * 另外,如果同一个服务定义了同名的重载方法,可能会导致proxy中命中错误的方法 + * 不过目前还未发现这种情况。 + * 否则需要将静态map换成实例map + */ + public static final Map sMethods = new HashMap(5); + + private MethodDelegate findMethodDelegate(String methodName, Object[] args) { + return sMethods.get(methodName); + } + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + String methodName = method.getName(); + MethodDelegate delegate = findMethodDelegate(methodName, args); + if (delegate != null) { + return delegate.beforeInvoke(target, method, args); + } + return super.beforeInvoke(target, method, args); + } + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object before, Object invokeResult) { + String methodName = method.getName(); + MethodDelegate deleate = findMethodDelegate(methodName, args); + if (deleate != null) { + return deleate.afterInvoke(target, method, args, before, invokeResult); + } + return super.afterInvoke(target, method, args, before, invokeResult); + } +} diff --git a/PluginCore/src/com/plugin/core/proxy/ProxyUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/ProxyUtil.java similarity index 84% rename from PluginCore/src/com/plugin/core/proxy/ProxyUtil.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/ProxyUtil.java index 5d370b4f..aac96869 100644 --- a/PluginCore/src/com/plugin/core/proxy/ProxyUtil.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/ProxyUtil.java @@ -1,4 +1,4 @@ -package com.plugin.core.proxy; +package com.limpoxe.fairy.core.proxy; import java.lang.reflect.Proxy; import java.util.ArrayList; @@ -8,6 +8,11 @@ public class ProxyUtil { + public static Object createProxy2(Object target, Object delegate) { + MethodDelegate realDelegate = (MethodDelegate) delegate; + return createProxy(target, realDelegate); + } + public static Object createProxy(Object target, MethodDelegate delegate) { Class clazz = target.getClass(); List> interfaces = getAllInterfaces(clazz); diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/WhiteList.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/WhiteList.java new file mode 100644 index 00000000..21dd6673 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/WhiteList.java @@ -0,0 +1,94 @@ +package com.limpoxe.fairy.core.proxy; + +import java.util.HashMap; +import java.util.HashSet; + +/** + * Created by cailiming on 2017/3/28. + */ + +public class WhiteList { + + public static final HashMap sInterfaceList = new HashMap(); + public static final HashSet sSystemServiceName = new HashSet(); + + static { + //不需要在getService()时被hook的系统服务列在此处,hook越少稳定性越高 + addServiceToIgnoreList("phone"); + addServiceToIgnoreList("iphonesubinfo"); + addServiceToIgnoreList("isub"); + addServiceToIgnoreList("wifi"); + addServiceToIgnoreList("multiwindow"); + addServiceToIgnoreList("assetatlas"); + + // 通常情况下,如果是通过编译命令生成的接口, 类名如下 + // 接口类全名 : descriptor + // 接口服务端侧实现类基类全名 : descriptor.Stub + // 接口客户端侧代理类全名称 : descriptor.Stub.Proxy + // 但是也有特殊情况,不是通过命令生成,而是自行实现的,这种情况就需要做白名单 + // 例如: + // android.content.IContentProvider ---> descriptor + // android.content.ContentProviderNative ---> descriptor.Stub + // android.content.ContentProviderProxy ---> descriptor.Stub.Proxy + // 不过contentprovider这个例子比较特殊, 正好不能hook, 否则会造成递归, 因为在被hook的实现里面,调用的Contentprovider查询插件信息 + + add("android.content.IContentProvider", null);//不需要hook + add("com.android.internal.telephony.ITelephony", null);//不需要hook + add("IMountService", "android.os.storage.IMountService$Stub$Proxy");//命令规则非默认 + add("android.content.IBulkCursor", "android.database.BulkCursorProxy");//命令规则非默认 + + //其他, 都属于使用默认命名规则: + //android.view.accessibility.IAccessibilityInteractionConnectionCallback + //android.view.accessibility.IAccessibilityManager + //android.view.IAssetAtlas + //android.view.IGraphicsStats + //android.view.IWindowManager + //android.view.IWindowSession + //com.android.internal.view.IInputMethodSession + //com.android.internal.view.IInputMethodManager + //com.android.internal.view.IInputMethodClient + //com.android.internal.telephony.ITelephony + //com.android.internal.telephony.ITelephonyRegistry + //com.android.internal.telephony.ISub + //com.android.internal.app.IBatteryStats + //android.app.IUiModeManager + //android.app.IWallpaperManager + //android.bluetooth.IBluetoothManager + //android.content.IBulkCursor + //android.content.IContentService + //android.hardware.input.IInputManager + //android.hardware.usb.IUsbManager + //android.net.wifi.IWifiManager + //android.os.IBatteryPropertiesRegistrar + //android.os.IMessenger + //android.os.IPowerManager + //android.os.IUserManager + //android.security.IKeystoreService + //android.vrsystem.IVRSystemService + //android.webkit.IWebViewUpdateService + //com.huawei.permission.IHoldService + //com.android.internal.app.IAppOpsService + //android.net.IConnectivityManager + } + + public static void add(String descriptor, String implClassName) { + sInterfaceList.put(descriptor, implClassName); + } + + public static String getProxyImplClassName(String descriptor) { + if (sInterfaceList.containsKey(descriptor)) { + return sInterfaceList.get(descriptor); + } else { + //默认命名规则 + return descriptor + "$Stub$Proxy"; + } + } + + public static void addServiceToIgnoreList(String systemServiceName) { + sSystemServiceName.add(systemServiceName); + } + + public static boolean isInIgnoreList(String systemServiceName) { + return sSystemServiceName.contains(systemServiceName); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIActivityManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIActivityManager.java new file mode 100644 index 00000000..acc7a7d8 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIActivityManager.java @@ -0,0 +1,505 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.app.ActivityManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.os.Build; +import android.os.IBinder; +import android.os.Process; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackActivityManager; +import com.limpoxe.fairy.core.android.HackActivityManagerNative; +import com.limpoxe.fairy.core.android.HackActivityThread; +import com.limpoxe.fairy.core.android.HackComponentName; +import com.limpoxe.fairy.core.android.HackContentProviderHolder; +import com.limpoxe.fairy.core.android.HackSingleton; +import com.limpoxe.fairy.core.bridge.PluginShadowService; +import com.limpoxe.fairy.core.bridge.ProviderClientUnsafeProxy; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProvider; +import com.limpoxe.fairy.manager.mapping.PluginStubBinding; +import com.limpoxe.fairy.manager.mapping.StubMappingProcessor; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.PendingIntentHelper; +import com.limpoxe.fairy.util.ProcessUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Created by cailiming on 16/1/15. + */ +public class AndroidAppIActivityManager extends MethodProxy { + + static { + sMethods.put("getRunningAppProcesses", new getRunningAppProcesses()); + sMethods.put("getIntentSender", new getIntentSender()); + sMethods.put("getIntentSenderWithFeature", new getIntentSender()); + sMethods.put("overridePendingTransition", new overridePendingTransition()); + sMethods.put("serviceDoneExecuting", new serviceDoneExecuting()); + sMethods.put("getContentProvider", new getContentProvider()); + sMethods.put("getTasks", new getTasks()); + sMethods.put("getAppTasks", new getAppTasks()); + sMethods.put("getServices", new getServices()); + //暂不需要 + //sMethods.put("broadcastIntent", new broadcastIntent()); + //sMethods.put("startService", new startService()); + //sMethods.put("stopService", new stopService()); + //sMethods.put("bindService", new bindService()); + //sMethods.put("unbindService", new unbindService()); + //sMethods.put("clearApplicationUserData", new clearApplicationUserData()); + sMethods.put("stopServiceToken", new stopServiceToken()); + } + + public static void installProxy() { + LogUtil.d("安装ActivityManagerProxy"); + Object androidAppActivityManagerProxy = HackActivityManagerNative.getDefault(); + Object androidAppIActivityManagerStubProxyProxy = ProxyUtil.createProxy(androidAppActivityManagerProxy, new AndroidAppIActivityManager()); + //O Preview版本暂时不能通过SDK_INT来区分 2017-5-18 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + Object singleton = HackActivityManagerNative.getGDefault(); + //如果是IActivityManager + if (singleton.getClass().isAssignableFrom(androidAppIActivityManagerStubProxyProxy.getClass())) { + HackActivityManagerNative.setGDefault(androidAppIActivityManagerStubProxyProxy); + } else {//否则是包装过的单例 + new HackSingleton(singleton).setInstance(androidAppIActivityManagerStubProxyProxy); + } + } else { + //Android O 没有gDefault这个成员了, 变量被移到了ActivityManager这个类中 + Object singleton = HackActivityManager.getIActivityManagerSingleton(); + if (singleton != null) { + new HackSingleton(singleton).setInstance(androidAppIActivityManagerStubProxyProxy); + } else { + LogUtil.e("WTF!!"); + } + } + LogUtil.d("安装完成"); + } + + //public List getRunningAppProcesses() + public static class getRunningAppProcesses extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + //由于插件运行在插件进程中,这里需要欺骗插件,让插件的中判断进程的逻辑以为当前是在主进程中运行 + //但是这会导致插件框架也无法判断当前的进程了,因此框架中判断插件进程的方法一定要在安装ActivityManager代理之前执行并记住状态 + //同时要保证主进程能正确判断进程。 + //这里不会导致无限递归,因为ProcessUtil.isPluginProcess方法内部有缓存,再安装ActivityManager代理之前已经执行并缓存了 + if (ProcessUtil.isPluginProcess() && FairyGlobal.isFakePluginProcessName()) { + List result = (List)invokeResult; + for (ActivityManager.RunningAppProcessInfo appProcess : result) { + if (appProcess != null && appProcess.pid == Process.myPid()) { + appProcess.processName = FairyGlobal.getHostApplication().getPackageName(); + break; + } + } + } + + return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); + } + } + + public static class getIntentSender extends MethodDelegate { + + public static final int INTENT_SENDER_BROADCAST = 1; + public static final int INTENT_SENDER_ACTIVITY = 2; + public static final int INTENT_SENDER_ACTIVITY_RESULT = 3; + public static final int INTENT_SENDER_SERVICE = 4; + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + int type = (int)args[0]; + args[1] = FairyGlobal.getHostApplication().getPackageName(); + if (type != INTENT_SENDER_ACTIVITY_RESULT) { + for (int i = 0; i < args.length; i++) { + if (args[i] != null && args[i].getClass().isAssignableFrom(Intent[].class)) { + Intent[] intents = (Intent[])args[i]; + if (type == INTENT_SENDER_BROADCAST) { + type = PluginDescriptor.BROADCAST; + } else if (type == INTENT_SENDER_ACTIVITY) { + type = PluginDescriptor.ACTIVITY; + } else if (type == INTENT_SENDER_SERVICE) { + type = PluginDescriptor.SERVICE; + } + for(int j = 0; j < intents.length; j++) { + intents[j] = PendingIntentHelper.resolvePendingIntent(intents[j], type); + } + break; + } + } + } + + return super.beforeInvoke(target, method, args); + } + } + + public static class overridePendingTransition extends MethodDelegate { + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if (ProcessUtil.isPluginProcess()) { + if (!ResourceUtil.isMainResId((Integer) args[2])) { + args[2] = 0; + } + if (!ResourceUtil.isMainResId((Integer) args[3])) { + args[3] = 0; + } + } + return super.beforeInvoke(target, method, args); + } + } + + public static class serviceDoneExecuting extends MethodDelegate { + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if (ProcessUtil.isPluginProcess()) { + if (((Integer)args[1]).equals(HackActivityThread.getSERVICE_DONE_EXECUTING_ANON())) { + for (Object obj: args) { + if (obj instanceof IBinder) { + Map services = HackActivityThread.get().getServices(); + Service service = services.get(obj); + if (service instanceof PluginShadowService) { + PluginShadowService shadowService = (PluginShadowService) service; + if (shadowService.realService != null) { + LogUtil.e("serviceDoneExecuting", "resolve real service for this PluginShadowService"); + services.put((IBinder) obj, shadowService.realService); + } else { + LogUtil.e("serviceDoneExecuting", "unable to create real service for this PluginShadowService"); + } + } + break; + } + } + } else if (((Integer)args[1]).equals(HackActivityThread.getSERVICE_DONE_EXECUTING_START())) { + // do nothing + } else if (((Integer)args[1]).equals(HackActivityThread.getSERVICE_DONE_EXECUTING_STOP())) { + // do nothing + } + } + return super.beforeInvoke(target, method, args); + } + } + + /** + * 这个的目的是为了实现跨进程调用插件中定义的provider + * + * 例:在插件A中定义了一个contentprovider + * 插件A在被wakeup的时候会通过调用PluginInjector.installContentProvider安装这个contentprovider到插件进程 + * 接下来在插件进程中的任意一个插件,都是可以直接通过contentreslover直接调用这个插件 + * + * 但是如果想在非插件进程,也能调用这个插件的contentprovider,则需要借助这个getContentProvider类来实现 + * 过程是: + * 想在非插件进程调用插件contentprovider,必定会在此非插件进程触发getContentProvider方法 + * 进入到下面的逻辑中; + * 如果目标contentprovider是插件中的(通过auth判断),则先返回一个fake的contentprovider给调用者 + * 此时,非插件进程中的调用发起方,获得来一个fake的contentprovider;其实例是:{@link ProviderClientUnsafeProxy} + * + * 接下来,所有对插件contentprovdier的调用,其实都是在调用这个fake contentprovider:{@link ProviderClientUnsafeProxy} + * + * 而{@link ProviderClientUnsafeProxy}会将所有调用,都转发给 {@link com.limpoxe.fairy.core.bridge.PluginShadowProvider},这个provider是定义在插件进程中的 + * + * 接着由这个插件进程中的{@link com.limpoxe.fairy.core.bridge.PluginShadowProvider},将调用再转发给插件定义的contentreslover + * 到这一步为止,其实就是回到了最前面在插件进程中调用插件contentprovider逻辑中去了 + * + * 而在前面提到的"都转发给{@link com.limpoxe.fairy.core.bridge.PluginShadowProvider}",有一种情况是不方便直接转发的,就是call函数 + * call函数丢失了url参数,在{@link com.limpoxe.fairy.core.bridge.PluginShadowProvider}在转发的时候不知道要转给谁, + * 因此额外做了一个约定,是非插件进程的调用发起方,在试图调用插件provider的call方法时, + * 需要同时将url添加到参数extras中去,{@link com.limpoxe.fairy.core.bridge.PluginShadowProvider}在转发的时候再从extras中取出url参数,就知道要转给谁了 + * + */ + public static class getContentProvider extends MethodDelegate { + + /** + * 防止在插件被唤起之前,在插件进程中调用插件的contentprovider,由于插件尚未初始化和安装contentprovider导致的无效url错误 + * @param target + * @param method + * @param args + * @return + */ + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if(ProcessUtil.isPluginProcess()) { + String auth = (String)args[Build.VERSION.SDK_INT <= 28 ? 1 : 2]; + tryWakeupBeforeCallPluginProvider(auth); + } + return super.beforeInvoke(target, method, args); + } + + private void tryWakeupBeforeCallPluginProvider(final String auth) { + if (!PluginManagerProvider.buildUri().getAuthority().equals(auth)) { + boolean found = false; + ArrayList list = PluginManagerHelper.getPlugins(); + for(PluginDescriptor pluginDescriptor : list) { + HashMap map = pluginDescriptor.getProviderInfos(); + if (map != null) { + Iterator iterator = map.values().iterator(); + while(iterator.hasNext()) { + PluginProviderInfo pluginProviderInfo = iterator.next(); + //在插件中找到了匹配的contentprovider + if (auth != null && auth.equals(pluginProviderInfo.getAuthority())) { + found = true; + //先检查插件是否已经初始化 + boolean isrunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isrunning) { + LogUtil.d("getContentProvider", "not running, wakeup", pluginDescriptor.getPackageName()); + PluginManagerHelper.wakeup(pluginDescriptor.getPackageName()); + //TODO 这里时序仍然晚了一步 可能是因为wakeup异步执行的原因 + } + break; + } + } + } + } + } + } + + //ApplicationThread, auth, userId, stable + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if (invokeResult != null) { + return invokeResult; + } + //invokeResult为空表示没有获取到contentprovider,正常情况下会抛出Unknown URI + //这里为了让非插件进程也能调用插件进程的插件ContentProvider,需要在此进程安装一个Proxy进行桥接 + String auth = (String)args[Build.VERSION.SDK_INT <= 28 ? 1 : 2]; + //快速判断,排除不是来自插件的auth + if (PluginManagerProvider.buildUri().getAuthority().equals(auth)) { + return invokeResult; + } + + //非插件进程 + if (!ProcessUtil.isPluginProcess()) { + return tryInstallProxyForCallerProcess(invokeResult, auth); + } else { + return tryReInstallPluginContentProvider(invokeResult, auth); + } + } + + /** + * 为了让非插件进程也能调用插件进程中插件配置的ContentProvider + * @param invokeResult + * @return + */ + private Object tryInstallProxyForCallerProcess(final Object invokeResult, final String auth) { + ProviderInfo[] hostProviders = new ProviderInfo[0]; + try { + hostProviders = FairyGlobal.getHostApplication().getPackageManager() + .getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), + PackageManager.GET_PROVIDERS).providers; + } catch (Exception e) { + e.printStackTrace(); + } + boolean isAlreadyAddByHost = false; + ArrayList list = PluginManagerHelper.getPlugins(); + for(PluginDescriptor pluginDescriptor : list) { + HashMap map = pluginDescriptor.getProviderInfos(); + if (map != null) { + Iterator iterator = map.values().iterator(); + while(iterator.hasNext()) { + PluginProviderInfo pluginProviderInfo = iterator.next(); + + isAlreadyAddByHost = false; + + if (hostProviders != null) { + for(ProviderInfo hostProvider : hostProviders) { + if (hostProvider.authority.equals(pluginProviderInfo.getAuthority())) { + LogUtil.e("此contentProvider已经在宿主中定义,不再安装插件中定义的contentprovider", hostProvider.authority, pluginProviderInfo.getName(), pluginProviderInfo.getName()); + isAlreadyAddByHost = true; + break; + } + } + } + if (isAlreadyAddByHost) { + continue; + } + + //在插件中找到了匹配的contentprovider + if (auth != null && auth.equals(pluginProviderInfo.getAuthority())) { + //先检查插件是否已经初始化 + boolean isrunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isrunning) { + isrunning = PluginManagerHelper.wakeup(pluginDescriptor.getPackageName()); + } + if (!isrunning) { + return invokeResult; + } + + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.applicationInfo = FairyGlobal.getHostApplication().getApplicationInfo(); + providerInfo.authority = auth; + //设置代理Provider + providerInfo.name = ProviderClientUnsafeProxy.class.getName(); + providerInfo.packageName = FairyGlobal.getHostApplication().getPackageName(); + Object holder = HackContentProviderHolder.newInstance(providerInfo); + new HackContentProviderHolder(holder).setLocal(true); + if (holder != null) { + //返回代理Provider + return holder; + } else { + return invokeResult; + } + } + } + } + } + return invokeResult; + } + + private Object tryReInstallPluginContentProvider(final Object invokeResult, final String auth) { + ProviderInfo[] hostProviders = new ProviderInfo[0]; + try { + hostProviders = FairyGlobal.getHostApplication().getPackageManager() + .getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), + PackageManager.GET_PROVIDERS).providers; + } catch (Exception e) { + e.printStackTrace(); + } + boolean isAlreadyAddByHost = false; + ArrayList list = PluginManagerHelper.getPlugins(); + for(PluginDescriptor pluginDescriptor : list) { + HashMap map = pluginDescriptor.getProviderInfos(); + if (map != null) { + Iterator iterator = map.values().iterator(); + while(iterator.hasNext()) { + isAlreadyAddByHost = false; + PluginProviderInfo pluginProviderInfo = iterator.next(); + if (hostProviders != null) { + for(ProviderInfo hostProvider : hostProviders) { + if (hostProvider.authority.equals(pluginProviderInfo.getAuthority())) { + LogUtil.e("此contentProvider已经在宿主中定义,不再安装插件中定义的contentprovider", hostProvider.authority, pluginProviderInfo.getName(), pluginProviderInfo.getName()); + isAlreadyAddByHost = true; + break; + } + } + } + if (isAlreadyAddByHost) { + continue; + } + //在插件中找到了匹配的contentprovider + if (auth != null && auth.equals(pluginProviderInfo.getAuthority())) { + //先检查插件是否已经初始化 + boolean isrunning = PluginManagerHelper.isRunning(pluginDescriptor.getPackageName()); + if (!isrunning) { + isrunning = PluginManagerHelper.wakeup(pluginDescriptor.getPackageName()); + } + if (!isrunning) { + return invokeResult; + } + + LogUtil.w("安装插件中的contentProvider"); + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.name = pluginProviderInfo.getName(); + providerInfo.authority = pluginProviderInfo.getAuthority(); + providerInfo.applicationInfo = new ApplicationInfo(FairyGlobal.getHostApplication().getApplicationInfo()); + providerInfo.applicationInfo.packageName = pluginDescriptor.getPackageName(); + providerInfo.exported = pluginProviderInfo.isExported(); + providerInfo.packageName = FairyGlobal.getHostApplication().getApplicationInfo().packageName; + providerInfo.grantUriPermissions = pluginProviderInfo.isGrantUriPermissions(); + + LogUtil.w("providerInfo packageName ", pluginDescriptor.getPackageName(), providerInfo.packageName, auth); + + Object holder = HackContentProviderHolder.newInstance(providerInfo); + new HackContentProviderHolder(holder).setLocal(true); + if (holder != null) { + return holder; + } else { + return invokeResult; + } + } + } + } + } + return invokeResult; + } + + } + + public static class getTasks extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if (ProcessUtil.isPluginProcess()) { + List list = (List)invokeResult; + if (list != null && list.size() > 0) { + for(ActivityManager.RunningTaskInfo taskInfo : list) { + fixStubName(taskInfo.baseActivity); + fixStubName(taskInfo.topActivity); + } + } + } + return invokeResult; + } + + private void fixStubName(ComponentName componentName) { + if(componentName == null) { + return; + } + if (PluginStubBinding.isStub(componentName.getClassName())) { + //通过stub查询其绑定的插件组件名称,如果是Activity,只支持非Standard模式的 + //因为standard模式是1对多的关系,1个stub对应多个插件Activity,通过stub查绑定关系是是查不出来的,这种情况需要通过lifecycle来记录 + //其他模式的可以通过这种方法查出来 + String realClassName = PluginStubBinding.getBindedPluginClassName(componentName.getClassName(), StubMappingProcessor.TYPE_ACTIVITY); + if (realClassName != null) { + new HackComponentName(componentName).setClassName(realClassName); + } + } + } + } + + //useless + public static class getAppTasks extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if (ProcessUtil.isPluginProcess()) { + LogUtil.d("getAppTasks", invokeResult); + } + return invokeResult; + } + } + + //useless + public static class getServices extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if (ProcessUtil.isPluginProcess()) { + LogUtil.d("getServices", invokeResult); + } + return invokeResult; + } + } + + //in case: calling stopSelf + public static class stopServiceToken extends MethodDelegate { + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if(ProcessUtil.isPluginProcess()) { + //fix packageName + ComponentName old = (ComponentName) args[0]; + ComponentName real = new ComponentName(FairyGlobal.getHostApplication().getPackageName(), old.getClassName()); + args[0] = real; + LogUtil.d("stopServiceToken", old + " -> " + real); + } + return super.beforeInvoke(target, method, args); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppINotificationManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppINotificationManager.java new file mode 100644 index 00000000..483e0e73 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppINotificationManager.java @@ -0,0 +1,245 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.app.Notification; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Icon; +import android.os.Build; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginIntentResolver; +import com.limpoxe.fairy.core.android.HackNotificationManager; +import com.limpoxe.fairy.core.android.HackPendingIntent; +import com.limpoxe.fairy.core.android.HackRemoteViews; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.FileUtil; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.io.File; +import java.lang.reflect.Method; + +/** + * Created by cailiming on 16/1/15. + */ +public class AndroidAppINotificationManager extends MethodProxy { + + static { + sMethods.put("enqueueNotification", new enqueueNotification()); + sMethods.put("enqueueNotificationWithTag", new enqueueNotificationWithTag()); + sMethods.put("enqueueNotificationWithTagPriority", new enqueueNotificationWithTagPriority()); + } + + public static void installProxy() { + LogUtil.d("安装NotificationManagerProxy"); + Object androidAppINotificationStubProxy = HackNotificationManager.getService(); + Object androidAppINotificationStubProxyProxy = ProxyUtil.createProxy(androidAppINotificationStubProxy, new AndroidAppINotificationManager()); + HackNotificationManager.setService(androidAppINotificationStubProxyProxy); + LogUtil.d("安装完成"); + } + + public static class enqueueNotification extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + //修正包名 + args[0] = FairyGlobal.getHostApplication().getPackageName(); + + if (FairyGlobal.isSupportRemoteViews()) { + for(Object obj: args) { + if (obj instanceof Notification) { + resolveRemoteViews((Notification)obj); + break; + } + } + } + + return super.beforeInvoke(target, method, args); + } + } + + public static class enqueueNotificationWithTag extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + //修正包名 + args[0] = FairyGlobal.getHostApplication().getPackageName(); + + if (FairyGlobal.isSupportRemoteViews()) { + for(Object obj: args) { + if (obj instanceof Notification) { + resolveRemoteViews((Notification)obj); + break; + } + } + } + + return super.beforeInvoke(target, method, args); + } + } + + public static class enqueueNotificationWithTagPriority extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + //修正包名 + args[0] = FairyGlobal.getHostApplication().getPackageName(); + + if (FairyGlobal.isSupportRemoteViews()) { + for(Object obj: args) { + if (obj instanceof Notification) { + resolveRemoteViews((Notification)obj); + break; + } + } + } + + return super.beforeInvoke(target, method, args); + } + } + + private static void resolveRemoteViews(Notification notification) { + + String hostPackageName = FairyGlobal.getHostApplication().getPackageName(); + + if (Build.VERSION.SDK_INT >= 23) { + Icon mSmallIcon = (Icon)RefInvoker.getField(notification, Notification.class, "mSmallIcon"); + Icon mLargeIcon = (Icon)RefInvoker.getField(notification, Notification.class, "mLargeIcon"); + if (mSmallIcon != null) { + RefInvoker.setField(mSmallIcon, Icon.class, "mString1", hostPackageName); + } + if (mLargeIcon != null) { + RefInvoker.setField(mLargeIcon, Icon.class, "mString1", hostPackageName); + } + } + + if (Build.VERSION.SDK_INT >= 21) { + + int layoutId = 0; + if (notification.tickerView != null) { + layoutId = new HackRemoteViews(notification.tickerView).getLayoutId(); + } + if (layoutId == 0) { + if (notification.contentView != null) { + layoutId = new HackRemoteViews(notification.contentView).getLayoutId(); + } + } + if (layoutId == 0) { + if (notification.bigContentView != null) { + layoutId = new HackRemoteViews(notification.bigContentView).getLayoutId(); + } + } + if (layoutId == 0) { + if (notification.headsUpContentView != null) { + layoutId = new HackRemoteViews(notification.headsUpContentView).getLayoutId(); + } + } + + if (layoutId == 0) { + return; + } + + //检查资源布局资源Id是否属于宿主 + if (ResourceUtil.isMainResId(layoutId)) { + return; + } + + //检查资源布局资源Id是否属于系统 + if (layoutId >> 24 == 0x1f) { + return; + } + + if ("Xiaomi".equals(Build.MANUFACTURER)) { + LogUtil.e("Xiaomi, not support, caused by MiuiResource"); + if (notification.contentView != null) { + //重置layout,避免crash + new HackRemoteViews(notification.contentView).setLayoutId(android.R.layout.test_list_item); + } + notification.bigContentView = null; + notification.headsUpContentView = null; + notification.tickerView = null; + return; + } + + ApplicationInfo newInfo = new ApplicationInfo(); + String packageName = null; + + if (notification.tickerView != null) { + packageName = notification.tickerView.getPackage(); + new HackRemoteViews(notification.tickerView).setApplicationInfo(newInfo); + } + if (notification.contentView != null) { + if (packageName == null) { + packageName = notification.contentView.getPackage(); + } + new HackRemoteViews(notification.contentView).setApplicationInfo(newInfo); + } + if (notification.bigContentView != null) { + if (packageName == null) { + packageName = notification.bigContentView.getPackage(); + } + new HackRemoteViews(notification.bigContentView).setApplicationInfo(newInfo); + } + if (notification.headsUpContentView != null) { + if (packageName == null) { + packageName = notification.headsUpContentView.getPackage(); + } + new HackRemoteViews(notification.headsUpContentView).setApplicationInfo(newInfo); + } + + ApplicationInfo applicationInfo = FairyGlobal.getHostApplication().getApplicationInfo(); + newInfo.packageName = applicationInfo.packageName; + newInfo.sourceDir = applicationInfo.sourceDir; + newInfo.dataDir = applicationInfo.dataDir; + + if (packageName != null && !packageName.equals(hostPackageName)) { + + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + newInfo.packageName = pluginDescriptor.getPackageName(); + //要确保publicSourceDir这个路径可以被SystemUI应用读取, + newInfo.publicSourceDir = prepareNotificationResourcePath(pluginDescriptor.getInstalledPath(), + FairyGlobal.getNotificationResPath()); + + } else if (packageName != null && packageName.equals(hostPackageName)) { + //如果packageName是宿主,由于前面已经判断出,layoutid不是来自插件,则尝试查找notifications的目标页面,如果目标是插件,则尝试使用此插件作为通知栏的资源来源 + if (notification.contentIntent != null) {//只处理contentIntent,其他不管 + Intent intent = new HackPendingIntent(notification.contentIntent).getIntent(); + if (intent != null && intent.getAction() != null && intent.getAction().contains(PluginIntentResolver.CLASS_SEPARATOR)) { + String className = intent.getAction().split(PluginIntentResolver.CLASS_SEPARATOR)[0]; + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + newInfo.packageName = pluginDescriptor.getPackageName(); + //要确保publicSourceDir这个路径可以被SystemUI应用读取, + newInfo.publicSourceDir = prepareNotificationResourcePath(pluginDescriptor.getInstalledPath(), + FairyGlobal.getNotificationResPath()); + } + } + } + } else if (Build.VERSION.SDK_INT >= 11) { + if (notification.tickerView != null) { + new HackRemoteViews(notification.tickerView).setPackage(hostPackageName); + } + if (notification.contentView != null) { + new HackRemoteViews(notification.contentView).setPackage(hostPackageName); + } + } + } + + private static String prepareNotificationResourcePath(String pluginInstalledPath, String worldReadablePath) { + LogUtil.w("正在为通知栏准备插件资源。。。这里现在暂时是同步复制,注意大文件卡顿!!"); + File worldReadableFile = new File(worldReadablePath); + + if (FileUtil.copyFile(pluginInstalledPath, worldReadableFile.getAbsolutePath())) { + LogUtil.w("通知栏插件资源准备完成,请确保此路径SystemUi有读权限", worldReadableFile.getAbsolutePath()); + return worldReadableFile.getAbsolutePath(); + } else { + LogUtil.e("不应该到这里来,直接返回这个路径SystemUi没有权限读取"); + return pluginInstalledPath; + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIPackageManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIPackageManager.java new file mode 100644 index 00000000..8eeae531 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidAppIPackageManager.java @@ -0,0 +1,508 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginActivityInfo; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginIntentResolver; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.core.android.HackActivityThread; +import com.limpoxe.fairy.core.android.HackApplicationPackageManager; +import com.limpoxe.fairy.core.android.HackParceledListSlice; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.manager.PluginManager; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.manager.PluginManagerProvider; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * Created by cailiming on 16/1/15. + */ +public class AndroidAppIPackageManager extends MethodProxy { + + static { + sMethods.put("getInstalledPackages", new getInstalledPackages()); + sMethods.put("getPackageInfo", new getPackageInfo()); + sMethods.put("getApplicationInfo", new getApplicationInfo()); + sMethods.put("getActivityInfo", new getActivityInfo()); + sMethods.put("getReceiverInfo", new getReceiverInfo()); + sMethods.put("getServiceInfo", new getServiceInfo()); + sMethods.put("getProviderInfo", new getProviderInfo()); + sMethods.put("queryIntentActivities", new queryIntentActivities()); + sMethods.put("queryIntentServices", new queryIntentServices()); + sMethods.put("resolveIntent", new resolveIntent()); + sMethods.put("resolveService", new resolveService()); + sMethods.put("getComponentEnabledSetting", new getComponentEnabledSetting()); + sMethods.put("getXml", new getXml()); + + } + + public static void installProxy(PackageManager manager) { + LogUtil.d("安装PackageManagerProxy"); + Object androidAppIPackageManagerStubProxy = HackActivityThread.getPackageManager(); + Object androidAppIPackageManagerStubProxyProxy = ProxyUtil.createProxy(androidAppIPackageManagerStubProxy, new AndroidAppIPackageManager()); + HackActivityThread.setPackageManager(androidAppIPackageManagerStubProxyProxy); + HackApplicationPackageManager hackApplicationPackageManager = new HackApplicationPackageManager(manager); + hackApplicationPackageManager.setPM(androidAppIPackageManagerStubProxyProxy); + + /** + * + *框架尚未初始化的时候,此方法调用,一定是在ActivityThread.handleBindApplication方法中 + *则此时是正在初始化宿主manifest文件中配置的组件,此时不能执行"优先返回插件的providerInfo"的逻辑 + *否则会出现插件provider在宿主app启动时被意外初始化 + * 因此这个hook需要通过post的方式,将执行时机推迟到application的oncreate之后执行 + } + */ + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + sMethods.put("resolveContentProvider", new resolveContentProvider()); + } + }); + + LogUtil.d("安装完成"); + } + + public static class getPackageInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + String packageName = (String)args[0]; + LogUtil.v("beforeInvoke", method.getName(), packageName); + if (!packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + if (pluginDescriptor != null) { + PackageInfo packageInfo = pluginDescriptor.getPackageInfo(args[1]); + return packageInfo; + } + } + return super.beforeInvoke(target, method, args); + } + } + + public static class getInstalledPackages extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeResult, Object invokeResult) { + LogUtil.v("afterInvoke", method.getName()); + //Mi A1 Android O 的invokeResult可能是null + if (invokeResult != null && Build.VERSION.SDK_INT >= 18) {//android4.3 + ArrayList plugins = PluginManagerHelper.getPlugins(); + if (plugins != null) { + List resultList = (List) new HackParceledListSlice(invokeResult).getList(); + if (resultList != null) { + for(PluginDescriptor pluginDescriptor:plugins) { + PackageInfo packageInfo = pluginDescriptor.getPackageInfo((int) args[0]); + if (packageInfo == null || packageInfo.applicationInfo == null) { + //Begin Just For Debug Trace + if (!TextUtils.isEmpty(pluginDescriptor.getInstalledPath())) { + File file = new File(pluginDescriptor.getInstalledPath()); + LogUtil.e("getPackageArchiveInfo fail", file.exists(), file.canRead(), file.canWrite()); + } else { + LogUtil.e("getPackageArchiveInfo fail, path is empth"); + } + //End + } + resultList.add(packageInfo); + } + } + } + } else { + //TODO android4.3以下对getInstalledPackages函数的hook + LogUtil.e("not support this method getInstalledPackages for api version " + Build.VERSION.SDK_INT); + } + return invokeResult; + } + } + + public static class queryIntentActivities extends MethodDelegate { + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + LogUtil.v("beforeInvoke", method.getName()); + ArrayList classNames = PluginIntentResolver.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); + if (classNames != null && classNames.size() > 0) { + LogUtil.v("Plugin Activity Intent Match"); + ArrayList resolveInfos = new ArrayList(); + for(String className: classNames) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + ResolveInfo info = new ResolveInfo(); + info.activityInfo = getActivityInfo(pluginDescriptor, className); + info.labelRes = 0;//需要时再加上 + resolveInfos.add(info); + } else { + LogUtil.v("PluginDescriptor Not Found"); + } + } + if (resolveInfos.size() > 0) { + invokeResult = appendPluginResolveInfo(invokeResult, resolveInfos); + } + } else { + LogUtil.v("It's not a plugin intent"); + } + return invokeResult; + } + + } + + public static class getApplicationInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + String packageName = (String)args[0]; + LogUtil.v("beforeInvoke", method.getName(), packageName); + if (!packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + if (pluginDescriptor != null) { + return getApplicationInfo(pluginDescriptor); + } + } else { + LogUtil.w("注意:使用了宿主包名:" + packageName); + } + return super.beforeInvoke(target, method, args); + } + } + + public static class getActivityInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + String className = ((ComponentName)args[0]).getClassName(); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + return getActivityInfo(pluginDescriptor, className); + } + return super.beforeInvoke(target, method, args); + } + + } + + public static class getReceiverInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + String className = ((ComponentName)args[0]).getClassName(); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + return getActivityInfo(pluginDescriptor, className); + } + return super.beforeInvoke(target, method, args); + } + + } + + public static class getServiceInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + String className = ((ComponentName)args[0]).getClassName(); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + return getServiceInfo(pluginDescriptor, className); + } + + return super.beforeInvoke(target, method, args); + } + } + + public static class getProviderInfo extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + String className = ((ComponentName)args[0]).getClassName(); + if (!className.equals(PluginManagerProvider.class.getName())) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + PluginProviderInfo info = pluginDescriptor.getProviderInfos().get(className); + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.name = info.getName(); + providerInfo.packageName = getPackageName(pluginDescriptor); + providerInfo.icon = pluginDescriptor.getApplicationIcon(); + providerInfo.metaData = pluginDescriptor.getMetaData(); + providerInfo.enabled = true; + providerInfo.exported = info.isExported(); + providerInfo.applicationInfo = getApplicationInfo(pluginDescriptor); + providerInfo.authority = info.getAuthority(); + return providerInfo; + } + } + return super.beforeInvoke(target, method, args); + } + } + + public static class queryIntentServices extends MethodDelegate { + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + LogUtil.v("beforeInvoke", method.getName()); + ArrayList classNames = PluginIntentResolver.matchPlugin((Intent) args[0], PluginDescriptor.SERVICE); + if (classNames != null && classNames.size() > 0) { + LogUtil.v("Plugin Service Intent Match"); + ArrayList resolveInfos = new ArrayList(); + for(String className: classNames) { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + if (pluginDescriptor != null) { + ResolveInfo info = new ResolveInfo(); + info.serviceInfo = getServiceInfo(pluginDescriptor, className); + resolveInfos.add(info); + } else { + LogUtil.v("PluginDescriptor Not Found"); + } + } + if (resolveInfos.size() > 0) { + invokeResult = appendPluginResolveInfo(invokeResult, resolveInfos); + } + } else { + LogUtil.v("It's not a plugin intent"); + } + return invokeResult; + } + } + + private static Object appendPluginResolveInfo(Object invokeResult, ArrayList resolveInfos) { + LogUtil.v("将插件组件信息插入结果集"); + if (resolveInfos == null || resolveInfos.size() == 0) { + return invokeResult; + } + + if (Build.VERSION.SDK_INT <= 23) { + if (invokeResult == null) { + invokeResult = resolveInfos; + } else { + ((List)invokeResult).addAll(resolveInfos); + } + } else { + // 高于7.0的版本应当返回的类型是 android.content.pm.ParceledListSlice + if (invokeResult == null) { + invokeResult = HackParceledListSlice.newParecledListSlice(resolveInfos); + } else { + List resultList = (List) new HackParceledListSlice(invokeResult).getList(); + resultList.addAll(resolveInfos); + } + } + return invokeResult; + } + + //ResolveInfo resolveIntent(Intent intent, String resolvedType, int flags, int userId); + public static class resolveIntent extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + ArrayList classNames = PluginIntentResolver.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); + if (classNames != null && classNames.size() > 0) { + //TODO 只取第一个,忽略了多组件匹配到同一个Intent的情况 + if (classNames.size() > 1) { + LogUtil.w("只取第一个,忽略了多组件匹配到同一个Intent的情况"); + } + String className = classNames.get(0); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + ResolveInfo info = new ResolveInfo(); + info.activityInfo = getActivityInfo(pluginDescriptor, className); + return info; + } + return super.beforeInvoke(target, method, args); + } + } + + public static class resolveService extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + ArrayList classNames = PluginIntentResolver.matchPlugin((Intent) args[0], PluginDescriptor.SERVICE); + if (classNames != null && classNames.size() > 0) { + //TODO 只取第一个,忽略了多组件匹配到同一个Intent的情况 + if (classNames.size() > 1) { + LogUtil.w("只取第一个,忽略了多组件匹配到同一个Intent的情况"); + } + String className = classNames.get(0); + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); + ResolveInfo info = new ResolveInfo(); + info.serviceInfo = getServiceInfo(pluginDescriptor, className); + return info; + } + return super.beforeInvoke(target, method, args); + } + } + + public static class getComponentEnabledSetting extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + Object arg0 = args[0]; + if (arg0 instanceof ComponentName) { + ComponentName mComponentName = ((ComponentName) args[0]); + + LogUtil.v("beforeInvoke", method.getName(), mComponentName.getPackageName(), mComponentName.getClassName()); + + if ("com.htc.android.htcsetupwizard".equalsIgnoreCase(mComponentName.getPackageName())) { + return PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } + } else { + LogUtil.v("beforeInvoke", method.getName(), arg0); + } + + return super.beforeInvoke(target, method, args); + } + } + + public static class resolveContentProvider extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.d("authorities", args[0]); + ArrayList plugins = PluginManager.getPlugins(); + PluginProviderInfo info = null; + PluginDescriptor pluginDescriptor = null; + if (plugins != null) { + for(PluginDescriptor descriptor:plugins) { + HashMap pluginProviderInfoMap = descriptor.getProviderInfos(); + Iterator> iterator = pluginProviderInfoMap.entrySet().iterator(); + while (iterator.hasNext()) { + HashMap.Entry entry = iterator.next(); + if (args[0].equals(entry.getValue().getAuthority())) { + //如果插件中有重复的配置,先到先得 + LogUtil.d("如果插件中有重复的配置,先到先得 authorities ", args[0]); + info = entry.getValue(); + pluginDescriptor = descriptor; + break; + } + } + if (info != null) { + break; + } + } + } + if (info != null) { + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.name = info.getName(); + providerInfo.packageName = getPackageName(pluginDescriptor); + providerInfo.icon = pluginDescriptor.getApplicationIcon(); + //todo + providerInfo.metaData = pluginDescriptor.getMetaData(); + providerInfo.enabled = true; + providerInfo.exported = info.isExported(); + providerInfo.applicationInfo = getApplicationInfo(pluginDescriptor); + providerInfo.authority = info.getAuthority(); + return providerInfo; + } + LogUtil.e("null authorities", args[0]); + return null; + } + } + + public static class getXml extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if (ProcessUtil.isPluginProcess()) { + String packageName = (String)args[0]; + LoadedPlugin loadedPlugin = PluginLauncher.instance().startPlugin(packageName); + if (loadedPlugin != null) { + return loadedPlugin.pluginResource.getXml((int)args[1]); + } + } + return null; + } + } + + private static ApplicationInfo getApplicationInfo(PluginDescriptor pluginDescriptor) { + ApplicationInfo info = new ApplicationInfo(); + info.packageName = getPackageName(pluginDescriptor); + info.metaData = pluginDescriptor.getMetaData(); + info.name = pluginDescriptor.getApplicationName(); + info.className = pluginDescriptor.getApplicationName(); + info.enabled = true; + info.processName = null;//需要时再添加 + info.sourceDir = pluginDescriptor.getInstalledPath(); + info.dataDir = pluginDescriptor.getRootDir(); + //info.uid == Process.myUid(); + info.publicSourceDir = pluginDescriptor.getInstalledPath(); + info.taskAffinity = null;//需要时再加上 + info.theme = pluginDescriptor.getApplicationTheme(); + info.flags = info.flags | ApplicationInfo.FLAG_HAS_CODE; + info.nativeLibraryDir = pluginDescriptor.getNativeLibDir(); + String targetSdkVersion = pluginDescriptor.getTargetSdkVersion(); + if (!TextUtils.isEmpty(targetSdkVersion)) { + info.targetSdkVersion = Integer.valueOf(targetSdkVersion); + } else { + info.targetSdkVersion = FairyGlobal.getHostApplication().getApplicationInfo().targetSdkVersion; + } + return info; + } + + private static ActivityInfo getActivityInfo(PluginDescriptor pluginDescriptor, String className) { + LogUtil.v("getActivityInfo for plugin ", pluginDescriptor.getPackageName(), className); + + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.name = className; + activityInfo.packageName = getPackageName(pluginDescriptor); + activityInfo.icon = pluginDescriptor.getApplicationIcon(); + activityInfo.metaData = pluginDescriptor.getMetaData(); + activityInfo.enabled = true; + activityInfo.exported = false; + activityInfo.applicationInfo = getApplicationInfo(pluginDescriptor); + //参数太多了,需要时再 + activityInfo.taskAffinity = null;//需要时再加上 + //activityInfo.targetActivity = //需要时再加上 + //activityInfo.softInputMode = //需要时再加上 + //activityInfo.screenOrientation = //需要时再加上 + + if (pluginDescriptor.getType(className) == PluginDescriptor.ACTIVITY) { + PluginActivityInfo detail = pluginDescriptor.getActivityInfos().get(className); + activityInfo.launchMode = Integer.valueOf(detail.getLaunchMode()); + activityInfo.theme = ResourceUtil.parseResId(detail.getTheme()); + if (detail.getUiOptions() != null) { + activityInfo.uiOptions = (int)Long.parseLong(detail.getUiOptions().replace("0x", ""), 16); + } + activityInfo.configChanges = detail.getConfigChanges(); + } + return activityInfo; + } + + private static ServiceInfo getServiceInfo(PluginDescriptor pluginDescriptor, String className) { + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.name = className; + serviceInfo.packageName = getPackageName(pluginDescriptor); + serviceInfo.icon = pluginDescriptor.getApplicationIcon(); + serviceInfo.metaData = pluginDescriptor.getMetaData(); + serviceInfo.enabled = true; + serviceInfo.exported = false; + //加上插件中配置进程名称后缀 + String process = pluginDescriptor.getServiceInfos().get(className); + if (process == null) { + serviceInfo.processName = FairyGlobal.getHostApplication().getPackageName(); + } else if (process.startsWith(":")) { + serviceInfo.processName = FairyGlobal.getHostApplication().getPackageName() + process; + } else { + serviceInfo.processName = process; + } + serviceInfo.applicationInfo = getApplicationInfo(pluginDescriptor); + return serviceInfo; + } + + private static String getPackageName(PluginDescriptor pluginDescriptor) { + //这里要使用插件包名 + return pluginDescriptor.getPackageName(); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsBinderProxyWrapper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsBinderProxyWrapper.java new file mode 100644 index 00000000..1017664e --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsBinderProxyWrapper.java @@ -0,0 +1,126 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import android.text.TextUtils; + +import com.limpoxe.fairy.core.PluginLoader; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.core.proxy.WhiteList; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.FileDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public class AndroidOsBinderProxyWrapper implements IBinder { + + IBinder mReal; + + public AndroidOsBinderProxyWrapper(IBinder real) { + mReal = real; + } + + @Override + public String getInterfaceDescriptor() throws RemoteException { + return mReal.getInterfaceDescriptor(); + } + + @Override + public boolean pingBinder() { + return mReal.pingBinder(); + } + + @Override + public boolean isBinderAlive() { + return mReal.isBinderAlive(); + } + + @Override + public IInterface queryLocalInterface(String descriptor) { + IInterface invokeResult = mReal.queryLocalInterface(descriptor); + if (invokeResult == null) {//为空表示不是服务侧 + invokeResult = hookQueryLocalInterface(descriptor, this); + } + return invokeResult; + } + + @Override + public void dump(FileDescriptor fd, String[] args) + throws RemoteException { + mReal.dump(fd, args); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + @Override + public void dumpAsync(FileDescriptor fd, String[] args) + throws RemoteException { + mReal.dumpAsync(fd, args); + } + + @Override + public boolean transact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + return mReal.transact(code, data, reply, flags); + } + + @Override + public void linkToDeath(DeathRecipient recipient, int flags) + throws RemoteException { + mReal.linkToDeath(recipient, flags); + } + + @Override + public boolean unlinkToDeath(DeathRecipient recipient, int flags) { + return mReal.unlinkToDeath(recipient, flags); + } + + static IInterface hookQueryLocalInterface(final String descriptor, IBinder binder) { + try { + LogUtil.i("Hook服务 : " + descriptor, binder.getClass().getName()); + + // 仍然可能会有一些其他服务hook不到, 比如PackageManager和ActivityManager, + // 是因为这些服务的binder在queryLocalInterface方法被hook之前, 已经被系统获取到了并缓存到全局静态变量中 + // 后面再取获取这些服务的时候, 直接返回的是这些缓存, 不会调用queryLocalInterface + // 所以AndroidOsServiceManager应该尽可能早地执行installProxy, 以免错过hook时机 + + String className = WhiteList.getProxyImplClassName(descriptor); + if (TextUtils.isEmpty(className)) { + return null; + } + + Class stubProxy = Class.forName(className, true, PluginLoader.class.getClassLoader()); + Constructor constructor = stubProxy.getDeclaredConstructor(IBinder.class); + constructor.setAccessible(true); + IInterface proxy = (IInterface)constructor.newInstance(binder); + SystemApiDelegate binderProxyDelegate = new SystemApiDelegate(descriptor); + + //借此方法可以一次代理掉所有服务的remote, 而不必每个服务加一个hook + proxy = (IInterface)ProxyUtil.createProxy2(proxy, binderProxyDelegate); + + return proxy; + } catch (ClassNotFoundException e) { + LogUtil.printException("hookQueryLocalInterface", e); + } catch (NoSuchMethodException e) { + LogUtil.printException("hookQueryLocalInterface", e); + } catch (IllegalAccessException e) { + LogUtil.printException("hookQueryLocalInterface", e); + } catch (InstantiationException e) { + LogUtil.printException("hookQueryLocalInterface", e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if(cause instanceof RuntimeException) { + throw (RuntimeException)cause; + } else if(cause instanceof Error) { + throw (Error)cause; + } else { + throw new RuntimeException("hookQueryLocalInterface", cause); + } + } + return null; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsIBinder.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsIBinder.java new file mode 100644 index 00000000..1129a1d7 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsIBinder.java @@ -0,0 +1,41 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.os.IBinder; + +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Method; + +/** + * Created by cailiming on 16/1/15. + */ +public class AndroidOsIBinder extends MethodProxy { + + static { + sMethods.put("queryLocalInterface", new queryLocalInterface()); + } + + public static IBinder installProxy(String serviceName, IBinder invokeResult) { + LogUtil.d("安装AndroidOsIBinderProxy For " + serviceName); + IBinder result = (IBinder)invokeResult; + IBinder resultProxy = (IBinder)ProxyUtil.createProxy(result, new AndroidOsIBinder()); + LogUtil.d("安装完成"); + return resultProxy; + } + + public static class queryLocalInterface extends MethodDelegate { + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if (invokeResult == null) {//为空表示不是服务侧 + invokeResult = AndroidOsBinderProxyWrapper.hookQueryLocalInterface((String)args[0], (IBinder)target); + if (invokeResult != null) { + return invokeResult; + } + } + return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsServiceManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsServiceManager.java new file mode 100644 index 00000000..ffa5789d --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidOsServiceManager.java @@ -0,0 +1,89 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import static com.limpoxe.fairy.core.proxy.ProxyUtil.createProxy; + +import android.content.Context; +import android.os.Build; +import android.os.IBinder; +import android.view.ViewConfiguration; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackServiceManager; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.WhiteList; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Map; + +/** + * Created by cailiming on 16/9/15. + */ +public class AndroidOsServiceManager extends MethodProxy { + + private static HashSet sCacheKeySet; + private static Map sCache; + + static { + sMethods.put("getService", new getService()); + } + + public static void installProxy() { + LogUtil.d("安装IServiceManagerProxy"); + + //for android 7.0 + + if (Build.VERSION.SDK_INT > 23) { + //触发初始化WindowGlobal中的静态成员变量,即触发WindowManagerGlobal.getWindowManagerService()函数被调用 + //避免7.+的系统中对window服务代理, + //7.+的系统代理window服务会被SELinux拒绝导致陷入死循环 + ViewConfiguration.get(FairyGlobal.getHostApplication()); + FairyGlobal.getHostApplication().getSystemService(Context.KEYGUARD_SERVICE); + //上面两行代码都是为了触发初始化 + } + + Object androidOsServiceManagerProxy = HackServiceManager.getIServiceManager(); + Object androidOsServiceManagerProxyProxy = createProxy(androidOsServiceManagerProxy, new AndroidOsServiceManager()); + HackServiceManager.setServiceManager(androidOsServiceManagerProxyProxy); + + //干掉缓存 + sCache = HackServiceManager.getCache(); + sCacheKeySet = new HashSet(); + sCacheKeySet.addAll(sCache.keySet()); + IBinder windowService = sCache.get(Context.WINDOW_SERVICE); + sCache.clear(); + if (windowService != null) { + sCache.put(Context.WINDOW_SERVICE, windowService); + } + LogUtil.d("安装完成"); + } + + public static class getService extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + LogUtil.i("ServiceManager.getService", args[0], invokeResult != null); + if (ProcessUtil.isPluginProcess() && invokeResult != null && !WhiteList.isInIgnoreList((String)args[0])) { + //优先使用wrapper,其次才是动态代理. + if (invokeResult.getClass().getName().equals("android.os.BinderProxy")) { + return new AndroidOsBinderProxyWrapper((IBinder)invokeResult); + } + IBinder binder = AndroidOsIBinder.installProxy((String)args[0], (IBinder) invokeResult); + //0 = "package" //7.0 + //1 = "window" //7.0 + //2 = "alarm" //7.0 + if (sCacheKeySet.contains(args[0])) { + LogUtil.i("补回安装时干掉的缓存", args[0]); + //TODO 在这里可以hook window service manager + //AndroidViewWindowManager.installProxy() + sCache.put((String) args[0], binder); + } + return binder; + } + return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewIWindowSession.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewIWindowSession.java new file mode 100644 index 00000000..78d3c3f4 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewIWindowSession.java @@ -0,0 +1,57 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.view.WindowManager; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Method; + +/** + * Created by cailiming on 16/1/15. + * not used + */ +public class AndroidViewIWindowSession extends MethodDelegate { + + public static Object installProxy(Object invokeResult) { + LogUtil.d("安装AndroidViewIWindowSessionProxy"); + Object iWindowSessionProxy = ProxyUtil.createProxy(invokeResult, new AndroidViewIWindowSession()); + LogUtil.d("安装完成"); + return iWindowSessionProxy; + } + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + if (args != null) { + fixPackageName(method.getName(), args); + } + return super.beforeInvoke(target, method, args); + } + + private void fixPackageName(String methodName, Object[] args) { + for (Object object : args) { + if (object != null && object instanceof WindowManager.LayoutParams) { + + WindowManager.LayoutParams params = ((WindowManager.LayoutParams)object); + + if (params.packageName != null && !params.packageName.equals(FairyGlobal.getHostApplication().getPackageName())) { + + //尝试读取插件, 注意, 这个方法调用会触发ContentProvider调用 + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(params.packageName); + if(pluginDescriptor != null) { + LogUtil.v("修正System api", methodName, params.packageName); + //这里修正packageName会引起弹PopupWindow时发生WindowManager异常, + //TODO 此处暂不修正,似乎无需修正,原因待查 + //params.packageName = PluginLoader.getHostApplication().getPackageName(); + } + } + } + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewWindowManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewWindowManager.java new file mode 100644 index 00000000..8106c5ea --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidViewWindowManager.java @@ -0,0 +1,44 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.view.WindowManager; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Method; + +/** + * Created by cailiming on 16/1/15. + * not used + */ +public class AndroidViewWindowManager extends MethodDelegate { + + public static WindowManager installProxy(Object invokeResult) { + LogUtil.d("安装AndroidViewWindowManagerProxy"); + WindowManager windowManager = (WindowManager)ProxyUtil.createProxy(invokeResult, new AndroidViewWindowManager()); + LogUtil.d("安装完成"); + return windowManager; + } + + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + if (args != null) { + fixPackageName(method.getName(), args); + } + return super.beforeInvoke(target, method, args); + } + + private void fixPackageName(String methodName, Object[] args) { + if (methodName.equals("addView") || methodName.equals("updateViewLayout")) { + for (Object object : args) { + if (object instanceof WindowManager.LayoutParams) { + LogUtil.v("修正WindowManager", methodName, "方法参数中的packageName", ((WindowManager.LayoutParams)object).packageName); + ((WindowManager.LayoutParams)object).packageName = FairyGlobal.getHostApplication().getPackageName(); + } + } + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWebkitWebViewFactoryProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWebkitWebViewFactoryProvider.java new file mode 100644 index 00000000..08162360 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWebkitWebViewFactoryProvider.java @@ -0,0 +1,209 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.content.Context; +import android.os.Build; +import android.webkit.WebView; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackWebViewFactory; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.util.FakeUtil; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Created by cailiming on 16/1/28. + */ +public class AndroidWebkitWebViewFactoryProvider extends MethodProxy { + + static { + sMethods.put("createWebView", new createWebView()); + } + + public static void installProxy() { + if (!FairyGlobal.isLocalHtmlEnable()) { + return; + } + //Debug.waitForDebugger(); + if (Build.VERSION.SDK_INT >= 19) { + LogUtil.d("安装WebViewFactoryProviderProxy"); + //在4.4及以上,这里的WebViewFactoryProvider的实际类型是 + // com.android.webview.chromium.WebViewChromiumFactoryProvider implements WebViewFactoryProvider + Object webViewFactoryProvider = null; + try { + webViewFactoryProvider = HackWebViewFactory.getProvider(); + } catch (Exception e) { + e.printStackTrace(); + LogUtil.printException("HackWebViewFactory getProvider", e); + } + if (webViewFactoryProvider != null) { + Object webViewFactoryProviderProxy = ProxyUtil.createProxy(webViewFactoryProvider, new AndroidWebkitWebViewFactoryProvider()); + HackWebViewFactory.setProviderInstance(webViewFactoryProviderProxy); + + WebView wb = new WebView(FairyGlobal.getHostApplication()); + wb.loadUrl("");//触发webview渲染引擎初始化 + + } else { + //如果取不到值,原因可能是不同版本差异 + } + LogUtil.d("安装完成"); + } + } + + public static class createWebView extends MethodDelegate { + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, final Object invokeResult) { + //这里invokeResult的实际类型是 + // com.android.webview.chromium.WebViewChromium implements WebViewProvider + //所以这里可以再次进行Proxy + if (FairyGlobal.isLocalHtmlEnable()) { + final WebView webView = (WebView) args[0]; + fixWebViewAsset(webView.getContext()); + } + return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); +// return ProxyUtil.createProxy(invokeResult, new MethodDelegate() { +// +// @Override +// public Object beforeInvoke(Object target, Method method, Object[] args) { +// fixWebViewAsset(webView.getContext()); +// return super.beforeInvoke(target, method, args); +// } +// +// }); + } + } + + /** + * 这个方法是解决如下问题: + * 目前插件进程是多个插件共享的, 而Webview的全局Context是进程唯一的。 + * 要让哪个插件能加载插件自己的Assets目录下的本地HTML, 就的将Webview的全局Context设置为哪个插件的AppContext + * + * 但是当有多个插件在自己的Assets目录下的存在本地HTML文件时, + * Webview的全局Context无论设置为哪个插件的AppContext, + * 都会导致另外一个插件的Asest下的HTML文件加载不出来。 + * + * 因此每次切换Activity的时候都尝试将Webview的全局Context切换到当前Activity所在的AppContext + * + * @param pluginActivity + */ + public static void switchWebViewContext(Context pluginActivity) { + if (!FairyGlobal.isLocalHtmlEnable()) { + return; + } + LogUtil.d("尝试切换WebView Context, 不同的WebView内核, 实现方式可能不同, 本方法基于Chrome的WebView实现"); + try { + /** + * webviewProvider获取过程: + * new WebView() + ->WebViewFactory.getProvider().createWebView(this, new PrivateAccess()).init() + ->loadChromiumProvider + -> PathClassLoader("/system/framework/webviewchromium.jar") + .forName("com.android.webviewchromium.WebViewChromiumFactoryProvider") + + -> BootLoader.forName(android.webkit.WebViewClassic$Factory) + + ->new WebViewClassic.Factory() + */ + WebView wb = new WebView(pluginActivity); + wb.loadUrl("");//触发下面的fixWebViewAsset方法 + } catch (NullPointerException e) { + LogUtil.printException("AndroidWebkitWebViewFactoryProvider.switchWebViewContext", e); + LogUtil.e("插件Application对象尚未初始化会触发NPE,如果是异步初始化插件,应等待异步初始化完成再进入插件"); + } catch (Exception e) { + LogUtil.printException("AndroidWebkitWebViewFactoryProvider.switchWebViewContext", e); + //参看com.android.webview.chromium.WebViewDelegateFactory.Api21CompatibilityDelegate.getPackageId方法和addWebViewAssetPath方法 + LogUtil.e("插件进程的webview渲染引擎不是通过宿主的resource初始化时,会出现package not found错误"); + } + } + + private static void fixWebViewAsset(Context context) { + context = FakeUtil.fakeContext(context); + try { + ClassLoader cl = null; + if (sContextUtils == null) { + if (sContentMain == null) { + Object provider = null; + try { + provider = HackWebViewFactory.getProvider(); + } catch (Exception e) { + e.printStackTrace(); + LogUtil.printException("HackWebViewFactory getProvider", e); + } + if (provider != null) { + cl = provider.getClass().getClassLoader(); + + try { + sContentMain = Class.forName("org.chromium.content.app.ContentMain", true, cl); + } catch (ClassNotFoundException e) { + } + + if (sContentMain == null) { + try { + sContentMain = Class.forName("com.android.org.chromium.content.app.ContentMain", true, cl); + } catch (ClassNotFoundException e) { + } + } + + if (sContentMain == null) { + //try com.android.webview.chromium.WebViewChromium.init()? + throw new ClassNotFoundException(String.format("Can not found class %s or %s in classloader %s", "org.chromium.content.app.ContentMain", "com.android.org.chromium.content.app.ContentMain", cl)); + } + } + } + if (sContentMain != null) { + Class[] paramTypes = new Class[]{Context.class}; + try { + Method method = sContentMain.getDeclaredMethod("initApplicationContext", paramTypes); + if (method != null) { + if (!method.isAccessible()) { + method.setAccessible(true); + } + try { + method.invoke(null, new Object[]{context}); + LogUtil.d("触发了切换WebView Context"); + } catch (IllegalAccessException e) { + LogUtil.printException("WebView.initApplicationContext", e); + } catch (IllegalArgumentException e) { + LogUtil.printException("WebView.initApplicationContext", e); + } catch (InvocationTargetException e) { + LogUtil.printException("WebView.initApplicationContext", e); + } + } + } catch (NoSuchMethodException ex) { + try{ + // 不同的Chrome版本, 初始化的方法不同 + // for Chrome version 52 + sContextUtils = Class.forName("org.chromium.base.ContextUtils", true, cl); + } catch (ClassNotFoundException e) { + } + if (sContextUtils != null) { + RefInvoker.setField(null, sContextUtils, "sApplicationContext", null); + RefInvoker.invokeMethod(null, sContextUtils, "initApplicationContext", new Class[]{Context.class}, new Object[]{context}); + LogUtil.d("触发了切换WebView Context"); + } + } + } + } else { + RefInvoker.setField(null, sContextUtils, "sApplicationContext", null); + RefInvoker.invokeMethod(null, sContextUtils, "initApplicationContext", new Class[]{Context.class}, new Object[]{context}); + // 不同的Chrome版本, 初始化的方法不同 + // for Chrome version 52.0.2743.98 + RefInvoker.invokeMethod(null, sContextUtils, "initApplicationContextForNative", (Class[])null, (Object[])null); + LogUtil.d("触发了切换WebView Context"); + } + + } catch (Exception e) { + LogUtil.printException("createWebview", e); + } + } + + private static Class sContentMain; + private static Class sContextUtils; + +} \ No newline at end of file diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWidgetToast.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWidgetToast.java new file mode 100644 index 00000000..e913a0f8 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/AndroidWidgetToast.java @@ -0,0 +1,78 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.content.Context; +import android.os.Build; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackToast; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.core.proxy.ProxyUtil; +import com.limpoxe.fairy.util.FakeUtil; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import java.lang.reflect.Method; + +/** + * Created by cailiming on 21/4/24. + */ +public class AndroidWidgetToast extends MethodProxy { + + static { + if (Build.VERSION.SDK_INT >= 30) { + sMethods.put("enqueueToast", new enqueueToast()); + sMethods.put("cancelToast", new cancelToast()); + sMethods.put("finishToken", new finishToken()); + } + } + + public static void installProxy() { + if (Build.VERSION.SDK_INT >= 30) { + LogUtil.d("安装AndroidWidgetToastProxy"); + Object androidAppINotificationStubProxy = HackToast.getService(); + Object androidAppINotificationStubProxyProxy = ProxyUtil.createProxy(androidAppINotificationStubProxy, new AndroidWidgetToast()); + HackToast.setService(androidAppINotificationStubProxyProxy); + LogUtil.d("安装完成"); + } + } + + public static class enqueueToast extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName()); + Object tn = args[2]; + //TN + RefInvoker.setField(tn, tn.getClass(), "mPackageName", FairyGlobal.getHostApplication().getPackageName()); + Object mPresenter = RefInvoker.getField(tn, tn.getClass(), "mPresenter"); + if (mPresenter != null) { + //ToastPresenter + RefInvoker.setField(mPresenter, mPresenter.getClass(), "mPackageName", FairyGlobal.getHostApplication().getPackageName()); + Context mContext = (Context)RefInvoker.getField(mPresenter, mPresenter.getClass(), "mContext"); + if (mContext != null) { + RefInvoker.setField(mPresenter, mPresenter.getClass(), "mContext", FakeUtil.fakeContext(mContext)); + } + } + return super.beforeInvoke(target, method, args); + } + } + + public static class cancelToast extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName(), args[0]); + args[0] = FairyGlobal.getHostApplication().getPackageName(); + return super.beforeInvoke(target, method, args); + } + } + + public static class finishToken extends MethodDelegate { + @Override + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", method.getName(), args[0]); + args[0] = FairyGlobal.getHostApplication().getPackageName(); + return super.beforeInvoke(target, method, args); + } + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/SystemApiDelegate.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/SystemApiDelegate.java new file mode 100644 index 00000000..b96f27a6 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/proxy/systemservice/SystemApiDelegate.java @@ -0,0 +1,124 @@ +package com.limpoxe.fairy.core.proxy.systemservice; + +import android.content.ComponentName; +import android.net.Uri; +import android.os.Build; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackComponentName; +import com.limpoxe.fairy.core.proxy.MethodDelegate; +import com.limpoxe.fairy.core.proxy.MethodProxy; +import com.limpoxe.fairy.manager.PluginManager; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +/** + * 所有被hook的系统服务的所有api方法调用, 都会先进入这里 + * Created by cailiming on 16/7/14.* + */ +public class SystemApiDelegate extends MethodDelegate { + + private final String descriptor; + + public SystemApiDelegate(String descriptor) { + this.descriptor = descriptor; + } + + public Object beforeInvoke(Object target, Method method, Object[] args) { + LogUtil.v("beforeInvoke", descriptor, method.getName()); + + //这里做此判定是为了把一些特定的接口方法仍然交给特定的MethodProxy去处理,不在此做统一处理 + //这些"特定的MethodProxy"主要是一些查询类接口 + //另外, 这里单独判断checkPackage是因为AppOpsService的checkPackage方法会进入这里, 而if里面的replacePackageName方法里 + // 面会触发一次ContentProvider调用, ContentProvider调用又会触发AppOpsService的checkPackage方法, + // AppOpsService的checkPackage方法被触发后又回进入这里, 造成递归异常,因此这里单独屏蔽掉checkPackage方法 + if (!MethodProxy.sMethods.containsKey(method.getName()) && !"checkPackage".equals(method.getName())) { + fixPackageName(method.getName(), args); + } + + if("android.content.IContentService".equals(descriptor)) { + if ("notifyChange".equals(method.getName())) { + if (Build.VERSION.SDK_INT >= 26) { + //TODO FIXME TODO 应该还有更好做法,以后再研究,此方法notifyChange本身用的不多 + //8.0及以上,如果notifyChange的对象是插件中定义的Authority时,直接屏蔽此方法。 + ArrayList plugins = PluginManager.getPlugins(); + if (plugins != null) { + for(PluginDescriptor descriptor:plugins) { + HashMap pluginProviderInfoMap = descriptor.getProviderInfos(); + Iterator> iterator = pluginProviderInfoMap.entrySet().iterator(); + while (iterator.hasNext()) { + HashMap.Entry entry = iterator.next(); + if (((Uri)args[0]).getAuthority().equals(entry.getValue().getAuthority())) { + LogUtil.e("uri", ((Uri)args[0]).toString(), "8.0及以上,notifyChange的对象Uri,直接屏蔽此方法"); + return new Object(); + } + } + } + } + } + } + } + + return null; + } + + @Override + public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, Object invokeResult) { + if ("android.view.IWindowManager".equals(descriptor)) { + LogUtil.v("afterInvoke", descriptor, method.getName()); + if ("openSession".equals(method.getName())) { + if (invokeResult != null) { + Object windowSession = AndroidViewIWindowSession.installProxy(invokeResult); + if (windowSession != null) { + return windowSession; + } + } + } + } + return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); + } + + /** + * 由于插件的Context.getPackageName返回了插件自己的包名 + * 这里需要在调用binder接口前将参数还原为宿主包名 + * @param args + */ + private void fixPackageName(String methodName, Object[] args) { + + //由android.media.session.MediaSessionManager.addOnActiveSessionsChangedListener触发 + if (methodName.equals("addSessionsListener")) { + if (args.length > 2 && args[1] instanceof ComponentName) { + LogUtil.v("修正System Api", descriptor, methodName, "的参数为宿主包名"); + new HackComponentName(args[1]).setPackageName(FairyGlobal.getHostApplication().getPackageName()); + return; + } + } + + if(args != null && args.length>0) { + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof String && ((String)args[i]).contains(".")) { + // 包含.号, 基本可以判定是packageName + if (!args[i].equals(FairyGlobal.getHostApplication().getPackageName())) { + //尝试读取插件, 注意, 这个方法调用会触发ContentProvider调用 + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId((String)args[i]); + if(pluginDescriptor != null) { + LogUtil.v("修正System Api", descriptor, methodName, "的参数为宿主包名", args[i]); + // 参数传的是插件包名, 修正为宿主包名 + args[i] = FairyGlobal.getHostApplication().getPackageName(); + // 这里或许需要break,提高效率, + // 因为一个接口的参数里面出现两个packageName的可能性较小 + // break; + } + } + } + } + } + } +} diff --git a/PluginCore/src/com/plugin/core/viewfactory/PluginFactoryCompat.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryCompat.java similarity index 66% rename from PluginCore/src/com/plugin/core/viewfactory/PluginFactoryCompat.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryCompat.java index 37612464..d5a8ed40 100644 --- a/PluginCore/src/com/plugin/core/viewfactory/PluginFactoryCompat.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryCompat.java @@ -1,4 +1,4 @@ -package com.plugin.core.viewfactory; +package com.limpoxe.fairy.core.viewfactory; import android.annotation.TargetApi; import android.content.Context; @@ -21,8 +21,7 @@ public class PluginFactoryCompat { private static Field sLayoutInflaterFactory2Field; private static boolean sCheckedField; - @TargetApi(11) - static class FactoryWrapper implements LayoutInflater.Factory, LayoutInflater.Factory2 { + static class FactoryWrapper implements LayoutInflater.Factory { final PluginFactoryInterface mDelegateFactory; FactoryWrapper(PluginFactoryInterface delegateFactory) { @@ -33,6 +32,14 @@ static class FactoryWrapper implements LayoutInflater.Factory, LayoutInflater.Fa public View onCreateView(String name, Context context, AttributeSet attrs) { return mDelegateFactory.onCreateView(null, name, context, attrs); } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + static class FactoryWrapper2 extends FactoryWrapper implements LayoutInflater.Factory2 { + + FactoryWrapper2(PluginFactoryInterface delegateFactory) { + super(delegateFactory); + } @Override public View onCreateView(View parent, String name, Context context, @@ -41,23 +48,29 @@ public View onCreateView(View parent, String name, Context context, } } - @TargetApi(11) static void setFactory(LayoutInflater inflater, PluginFactoryInterface factory) { - final LayoutInflater.Factory2 factory2 = factory != null - ? new FactoryWrapper(factory) : null; - inflater.setFactory2(factory2); + if (Build.VERSION.SDK_INT >=11) { + final LayoutInflater.Factory2 factory2 = factory != null + ? new FactoryWrapper2(factory) : null; + inflater.setFactory2(factory2); - if (Build.VERSION.SDK_INT < 21) { - final LayoutInflater.Factory f = inflater.getFactory(); - if (f instanceof LayoutInflater.Factory2) { - // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21). - // We will now try and force set the merged factory to mFactory2 - forceSetFactory2(inflater, (LayoutInflater.Factory2) f); - } else { - // Else, we will force set the original wrapped Factory2 - forceSetFactory2(inflater, factory2); + if (Build.VERSION.SDK_INT < 21) { + final LayoutInflater.Factory f = inflater.getFactory(); + if (f instanceof LayoutInflater.Factory2) { + // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21). + // We will now try and force set the merged factory to mFactory2 + forceSetFactory2(inflater, (LayoutInflater.Factory2) f); + } else { + // Else, we will force set the original wrapped Factory2 + forceSetFactory2(inflater, factory2); + } } + } else { + final LayoutInflater.Factory factory1 = factory != null + ? new FactoryWrapper(factory) : null; + inflater.setFactory(factory1); } + } /** diff --git a/PluginCore/src/com/plugin/core/viewfactory/PluginFactoryInterface.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryInterface.java similarity index 95% rename from PluginCore/src/com/plugin/core/viewfactory/PluginFactoryInterface.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryInterface.java index 140f6a99..10fe71ec 100644 --- a/PluginCore/src/com/plugin/core/viewfactory/PluginFactoryInterface.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginFactoryInterface.java @@ -1,4 +1,4 @@ -package com.plugin.core.viewfactory; +package com.limpoxe.fairy.core.viewfactory; import android.content.Context; import android.util.AttributeSet; diff --git a/PluginCore/src/com/plugin/core/viewfactory/PluginViewFactory.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewFactory.java similarity index 61% rename from PluginCore/src/com/plugin/core/viewfactory/PluginViewFactory.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewFactory.java index eb1133bd..08e33a21 100644 --- a/PluginCore/src/com/plugin/core/viewfactory/PluginViewFactory.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewFactory.java @@ -1,6 +1,5 @@ -package com.plugin.core.viewfactory; +package com.limpoxe.fairy.core.viewfactory; -import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.os.Build; @@ -9,7 +8,8 @@ import android.view.View; import android.view.Window; -import com.plugin.util.LogUtil; +import com.limpoxe.fairy.core.compat.CompatForFragmentClassCache; +import com.limpoxe.fairy.util.LogUtil; /** * Created by Administrator on 2015/12/13. @@ -33,6 +33,7 @@ public PluginViewFactory(Activity context, Window window, LayoutInflater.Factory } public void installViewFactory() { + LogUtil.d("安装PluginViewFactory"); LayoutInflater layoutInflater = mContext.getLayoutInflater(); if (layoutInflater.getFactory() == null) { PluginFactoryCompat.setFactory(layoutInflater, this); @@ -40,11 +41,42 @@ public void installViewFactory() { LogUtil.d("The Activity's LayoutInflater already has a Factory installed" + " so we can not install plugin's"); } + LogUtil.d("安装PluginViewFactory完成"); } @Override public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + if ("fragment".equals(name)) { + + String pluginId = attrs.getAttributeValue(null, "context"); + String fname = attrs.getAttributeValue(null, "class"); + if (fname == null) { + int count = attrs.getAttributeCount(); + for(int i = 0; i < count; i++) { + if ("name".equals(attrs.getAttributeName(i))) { + fname = attrs.getAttributeValue(i); + break; + } + } + } + + if (pluginId != null) { + Context fragmentContext = createContext(context, pluginId); + if (fragmentContext != null) { + if (mOriginalWindowCallback instanceof LayoutInflater.Factory) { + + CompatForFragmentClassCache.forceCache(fragmentContext, fname); + + View view = ((LayoutInflater.Factory) mOriginalWindowCallback).onCreateView(name, fragmentContext, attrs); + if (view != null) { + return view; + } + } + } + } + } + // First let the Activity's Factory try and inflate the view final View view = callActivityOnCreateView(parent, name, context, attrs); if (view != null) { @@ -55,7 +87,6 @@ public final View onCreateView(View parent, String name, return createView(parent, name, context, attrs); } - @TargetApi(11) private View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = null; if (mOriginalWindowCallback instanceof LayoutInflater.Factory) { @@ -67,9 +98,11 @@ private View callActivityOnCreateView(View parent, String name, Context context, return view; } - if (mOriginalWindowCallback instanceof LayoutInflater.Factory2) { - return ((LayoutInflater.Factory2) mOriginalWindowCallback) - .onCreateView(parent, name, context, attrs); + if(Build.VERSION.SDK_INT >= 11) { + if (mOriginalWindowCallback instanceof LayoutInflater.Factory2) { + return ((LayoutInflater.Factory2) mOriginalWindowCallback) + .onCreateView(parent, name, context, attrs); + } } return null; @@ -91,4 +124,11 @@ private View createView(View parent, final String name, Context context, return mPluginViewInflater.createView(parent, name, context, attrs, inheritContext, isPre21); } + + private Context createContext(Context Context, String pluginId) { + if (mPluginViewInflater == null) { + mPluginViewInflater = new PluginViewInflater(mContext, mViewfactory); + } + return mPluginViewInflater.createContext(Context, pluginId); + } } diff --git a/PluginCore/src/com/plugin/core/viewfactory/PluginViewInflater.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewInflater.java similarity index 69% rename from PluginCore/src/com/plugin/core/viewfactory/PluginViewInflater.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewInflater.java index 9e0c978d..5947117c 100644 --- a/PluginCore/src/com/plugin/core/viewfactory/PluginViewInflater.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/core/viewfactory/PluginViewInflater.java @@ -1,11 +1,19 @@ -package com.plugin.core.viewfactory; +package com.limpoxe.fairy.core.viewfactory; import android.content.Context; +import android.content.ContextWrapper; import android.util.AttributeSet; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.PluginContextTheme; +import com.limpoxe.fairy.core.PluginCreator; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.manager.PluginManagerHelper; +import com.limpoxe.fairy.util.LogUtil; import java.lang.reflect.Constructor; import java.util.HashMap; @@ -18,10 +26,10 @@ */ public class PluginViewInflater { - static final Class[] sConstructorSignature = new Class[] { + final Class[] sConstructorSignature = new Class[] { Context.class, AttributeSet.class}; - private static final Map> sConstructorMap = new HashMap<>(); + private final Map> sConstructorMap = new HashMap<>(); private final LayoutInflater.Factory mViewfactory; private final Context mContext; @@ -130,4 +138,32 @@ public static Context themifyContext(Context context, AttributeSet attrs, return context; } + static Context createContext(Context Context, String pluginId) { + try { + PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); + if (pluginDescriptor != null) { + //插件可能尚未初始化,确保使用前已经初始化 + LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor); + if (plugin != null) { + Context baseContext = Context; + if (!(baseContext instanceof PluginContextTheme)) { + baseContext = ((ContextWrapper)baseContext).getBaseContext(); + } + if (baseContext instanceof PluginContextTheme) { + baseContext = ((PluginContextTheme) baseContext).getBaseContext(); + } + Context pluginViewContext = PluginCreator.createNewPluginComponentContext(plugin.pluginContext, baseContext, pluginDescriptor.getApplicationTheme()); + return pluginViewContext; + } else { + LogUtil.e("插件启动失败 " + pluginId); + } + } else { + LogUtil.e("未找到插件" + pluginId + ",请确认是否已安装"); + } + } catch (Exception e) { + return null; + } + return null; + } + } diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/InstallResult.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/InstallResult.java new file mode 100644 index 00000000..cb95cc49 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/InstallResult.java @@ -0,0 +1,33 @@ +package com.limpoxe.fairy.manager; + +/** + * Created by cailiming on 16/6/20. + */ +public class InstallResult { + + private int mResult; + private String mPackageName; + private String mVersion; + + public InstallResult(int result) { + this.mResult = result; + } + + public InstallResult(int result, String packageName, String version) { + this.mResult = result; + this.mPackageName = packageName; + this.mVersion = version; + } + + public int getResult() { + return mResult; + } + + public String getPackageName() { + return mPackageName; + } + + public String getVersion() { + return mVersion; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginActivityMonitor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginActivityMonitor.java new file mode 100644 index 00000000..2ae43842 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginActivityMonitor.java @@ -0,0 +1,61 @@ +package com.limpoxe.fairy.manager; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.limpoxe.fairy.core.PluginContextTheme; +import com.limpoxe.fairy.core.RealPluginClassLoader; +import com.limpoxe.fairy.util.LogUtil; + +import java.util.HashMap; + +public class PluginActivityMonitor { + + public static final String ACTION_STOP_PLUGIN = ".fairy.action.ACTION_STOP_PLUGIN"; + + private HashMap receivers = new HashMap(); + + public void onActivityCreate(final Activity activity) { + if (!activity.isChild()) { + if (activity.getClass().getClassLoader() instanceof RealPluginClassLoader) { + String pluginPackageName = ((PluginContextTheme)activity.getApplication().getBaseContext()).getPluginDescriptor().getPackageName(); + BroadcastReceiver br = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LogUtil.w("onReceive", intent.getAction(), "activity.finish()"); + activity.finish(); + } + }; + receivers.put(activity, br); + + LogUtil.v("registerReceiver", pluginPackageName + ACTION_STOP_PLUGIN); + activity.registerReceiver(br, new IntentFilter(pluginPackageName + ACTION_STOP_PLUGIN)); + } + } + } + + public void onActivityResume(Activity activity) { + if (!activity.isChild()) { + + } + } + + public void onActivityPause(Activity activity) { + if (!activity.isChild()) { + + } + } + + public void onActivityDestory(Activity activity) { + if (!activity.isChild()) { + if (activity.getClass().getClassLoader() instanceof RealPluginClassLoader) { + BroadcastReceiver br = receivers.remove(activity); + LogUtil.v("unregisterReceiver", br.getClass().getName()); + activity.unregisterReceiver(br); + } + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallback.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallback.java new file mode 100644 index 00000000..82173554 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallback.java @@ -0,0 +1,27 @@ +package com.limpoxe.fairy.manager; + +/** + *use PluginStatusChangeListener instead + */ +@Deprecated +public interface PluginCallback { + + public static final String ACTION_PLUGIN_CHANGED = "com.limpoxe.fairy.action.PLUGIN_CHANGED"; + + public static final String EXTRA_TYPE = "type"; + public static final String EXTRA_ID = "id"; + public static final String EXTRA_VERSION = "version"; + public static final String EXTRA_RESULT_CODE = "code"; + public static final String EXTRA_SRC = "src"; + + public static final String TYPE_INSTALL = "install"; + public static final String TYPE_REMOVE = "remove"; + public static final String TYPE_START = "start"; + public static final String TYPE_STOP = "stop"; + + void onInstall(int result, String packageName, String version, String src); + void onRemove(String packageName, int code); + + void onStart(String packageName); + void onStop(String packageName); +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallbackImpl.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallbackImpl.java new file mode 100644 index 00000000..9ac88875 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginCallbackImpl.java @@ -0,0 +1,54 @@ +package com.limpoxe.fairy.manager; + +import android.content.Intent; + +import com.limpoxe.fairy.core.FairyGlobal; + +/** + * Created by cailiming on 2015/9/13. + */ +public class PluginCallbackImpl implements PluginStatusChangeListener { + + @Override + public void onInstall(int result, String packageName, String version, String src) { + Intent intent = new Intent(ACTION_PLUGIN_CHANGED); + intent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + intent.putExtra(EXTRA_TYPE, TYPE_INSTALL); + intent.putExtra(EXTRA_ID, packageName); + intent.putExtra(EXTRA_VERSION, version); + intent.putExtra(EXTRA_RESULT_CODE, result); + intent.putExtra(EXTRA_SRC, src); + FairyGlobal.getHostApplication().sendBroadcast(intent); + } + + @Override + public void onRemove(String packageName, int code) { + Intent intent = new Intent(ACTION_PLUGIN_CHANGED); + intent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + intent.putExtra(EXTRA_TYPE, TYPE_REMOVE); + intent.putExtra(EXTRA_ID, packageName); + intent.putExtra(EXTRA_RESULT_CODE, code); + FairyGlobal.getHostApplication().sendBroadcast(intent); + } + + //暂未使用,有需要再加 + @Override + public void onStart(String packageName) { + Intent intent = new Intent(ACTION_PLUGIN_CHANGED); + intent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + intent.putExtra(EXTRA_TYPE, TYPE_START); + intent.putExtra(EXTRA_ID, packageName); + FairyGlobal.getHostApplication().sendBroadcast(intent); + } + + //暂未使用,有需要再加 + @Override + public void onStop(String packageName) { + Intent intent = new Intent(ACTION_PLUGIN_CHANGED); + intent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + intent.putExtra(EXTRA_TYPE, TYPE_STOP); + intent.putExtra(EXTRA_ID, packageName); + FairyGlobal.getHostApplication().sendBroadcast(intent); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManager.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManager.java new file mode 100644 index 00000000..eca0a413 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManager.java @@ -0,0 +1,9 @@ +package com.limpoxe.fairy.manager; + +/** + * Created by cailiming on 17/11/28. + * + */ +public class PluginManager extends PluginManagerHelper { + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerHelper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerHelper.java new file mode 100644 index 00000000..52eb9313 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerHelper.java @@ -0,0 +1,193 @@ +package com.limpoxe.fairy.manager; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginFilter; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ProcessUtil; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * Created by cailiming on 16/3/11. + * use PluginManager instead + */ +@Deprecated +public class PluginManagerHelper { + + public static final int SUCCESS = 0; + public static final int SRC_FILE_NOT_FOUND = 1; + public static final int COPY_FILE_FAIL = 2; + public static final int SIGNATURES_INVALIDATE = 3; + public static final int VERIFY_SIGNATURES_FAIL = 4; + public static final int PARSE_MANIFEST_FAIL = 5; + public static final int FAIL_BECAUSE_SAME_VER_HAS_LOADED = 6; + public static final int MIN_API_NOT_SUPPORTED = 8; + public static final int INSTALL_FAIL = 7; + public static final int HOST_VERSION_NOT_SUPPORT_CURRENT_PLUGIN = 9; + + public static final int REMOVE_SUCCESS = 0; + public static final int REMOVE_FAIL_PLUGIN_NOT_EXIST = 21; + public static final int REMOVE_FAIL = 27; + + public static PluginDescriptor getPluginDescriptorByClassName(String clazzName) { + return PluginManagerProviderClient.queryByClass(clazzName); + } + + /** + * 尽量减少调用此方法,特别是在插件比较多时,调用此方法会在进程间传递大量数据, + * 一则影响性能,二则数据量可能会超出binder的数据传输上线而导致binder崩溃 + * @return + */ + @SuppressWarnings("unchecked") + public static ArrayList getPlugins() { + return PluginManagerProviderClient.queryAll(); + } + + public static PluginDescriptor getPluginDescriptorByPluginId(String pluginId) { + + if (!PluginFilter.maybePlugin(pluginId)) { + // 之所以有这判断, 是因为可能BinderProxyDelegate + // 或者AndroidAppIPackageManager + // 或者PluginBaseContextWrapper.createPackageContext + // 中拦截了由系统发起的查询操作, 被拦截之后转到了这里 + // 所以在这做个快速判断. + LogUtil.d("默认策略判定" + pluginId + "不是插件包名"); + return null; + } + + return PluginManagerProviderClient.queryById(pluginId); + } + + public static PluginDescriptor getPluginDescriptorByFragmentId(String clazzId) { + return PluginManagerProviderClient.queryByFragment(clazzId); + } + + public static int installPlugin(String srcFile) { + return PluginManagerProviderClient.install(srcFile); + } + + public static boolean isInstalled(String pluginId) { + PluginDescriptor pluginDescriptor = PluginManagerProviderClient.queryById(pluginId); + return pluginDescriptor != null; + } + + public static boolean isInstalled(String pluginId, String pluginVersion) { + PluginDescriptor pluginDescriptor = PluginManagerProviderClient.queryById(pluginId); + if (pluginDescriptor != null) { + LogUtil.v("isInstalled", pluginId, pluginDescriptor.getVersion(), pluginVersion); + return pluginDescriptor.getVersion().equals(pluginVersion); + } + return false; + } + + public static boolean isRunning(String pluginId) { + return PluginManagerProviderClient.isRunning(pluginId); + } + + public static boolean wakeup(String pluginid) { + return PluginManagerProviderClient.wakeup(pluginid); + } + + public static int remove(String pluginId) { + return PluginManagerProviderClient.remove(pluginId); + } + + public static void stop(String pluginId) { + PluginManagerProviderClient.stop(pluginId); + } + + /** + * 清除列表并不能清除已经加载到内存当中的class,因为class一旦加载后后无法卸载 + */ + public static void removeAll() { + PluginManagerProviderClient.removeAll(); + } + + /** + * 强行重启插件进程,杀掉所有运行中的插件 + */ + public static void rebootPluginProcess() { + if (!ProcessUtil.isPluginProcess()) {//只在非插件进程调用才能做到重启,自己杀自己无法重启 + PluginManagerProviderClient.rebootPluginProcess(); + } + } + + /** + * 此功能仅限开发测试时使用: + * 为了在插件开发期间,方便插件的安装和卸载,监听系统广播。 + * 当收到插件的安装和卸载的系统广播时,自动将插件安装到宿主中,或自动从宿主中卸载 + * 其中,安装时由于框架默认限制了相同的版本好不重复安装,因此需要配合{@link FairyGlobal#isInstallationWithSameVersion()}使用 + * @param pluginPackageRegex + */ + @Deprecated + public static void autoInstallPackage(final String[] pluginPackageRegex) { + if (!FairyGlobal.isInited()) { + return; + } + if (pluginPackageRegex == null || pluginPackageRegex.length == 0) { + return; + } + try { + //先把p当作非正则查询一次 + for (String p : pluginPackageRegex) { + ApplicationInfo applicationInfo = FairyGlobal.getHostApplication().getPackageManager() + .getApplicationInfo(p, PackageManager.GET_META_DATA); + if (applicationInfo != null) { + LogUtil.d("发现已经安装到系统的插件包,触发安装插件", p, applicationInfo.sourceDir); + installPlugin(applicationInfo.sourceDir); + } + } + } catch (Exception e) { + } + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + //intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + intentFilter.addDataScheme("package"); + FairyGlobal.getHostApplication().registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { + String pkgName = intent.getData().getSchemeSpecificPart(); + for (String p : pluginPackageRegex) { + try { + if (Pattern.matches(p, pkgName)) { + ApplicationInfo applicationInfo = FairyGlobal.getHostApplication().getPackageManager() + .getApplicationInfo(pkgName, PackageManager.GET_META_DATA); + if (applicationInfo != null) { + LogUtil.d("收到系统广播,触发安装插件", pkgName, applicationInfo.sourceDir); + installPlugin(applicationInfo.sourceDir); + } + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { + String pkgName = intent.getData().getSchemeSpecificPart(); + for (String p : pluginPackageRegex) { + try { + if (Pattern.matches(p, pkgName)) { + LogUtil.d("收到系统广播,触发卸载插件", pkgName); + PluginManagerHelper.remove(pkgName); + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + }, intentFilter); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProvider.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProvider.java new file mode 100644 index 00000000..a2523ec9 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProvider.java @@ -0,0 +1,311 @@ +package com.limpoxe.fairy.manager; + +import android.annotation.TargetApi; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.util.Log; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.manager.mapping.PluginStubBinding; +import com.limpoxe.fairy.manager.mapping.StubExact; +import com.limpoxe.fairy.manager.mapping.StubMappingProcessor; +import com.limpoxe.fairy.util.LogUtil; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by cailiming on 16/3/11. + * + * 利用ContentProvider实现同步跨进程调用 + * + * 注意: ContentProvider 的方法可能不是在主线程中执行的。 + */ +public class PluginManagerProvider extends ContentProvider { + + private static Uri CONTENT_URI; + + static final String ACTION_INSTALL = "install"; + static final String INSTALL_RESULT = "install_result"; + + static final String ACTION_REMOVE = "remove"; + static final String REMOVE_RESULT = "remove_result"; + + static final String ACTION_REMOVE_ALL = "remove_all"; + static final String REMOVE_ALL_RESULT = "remove_all_result"; + + static final String ACTION_STOP = "stop_plugin"; + static final String STOP_RESULT = "stop_plugin_result"; + + static final String ACTION_QUERY_BY_ID = "query_by_id"; + static final String QUERY_BY_ID_RESULT = "query_by_id_result"; + + static final String ACTION_QUERY_BY_CLASS_NAME = "query_by_class_name"; + static final String QUERY_BY_CLASS_NAME_RESULT = "query_by_class_name_result"; + + static final String ACTION_QUERY_BY_FRAGMENT_ID = "query_by_fragment_id"; + static final String QUERY_BY_FRAGMENT_ID_RESULT = "query_by_fragment_id_result"; + + static final String ACTION_QUERY_ALL = "query_all"; + static final String QUERY_ALL_RESULT = "query_all_result"; + + static final String ACTION_BIND_ACTIVITY = "bind_activity"; + static final String BIND_ACTIVITY_RESULT = "bind_activity_result"; + + static final String ACTION_UNBIND_ACTIVITY = "unbind_activity"; + static final String UNBIND_ACTIVITY_RESULT = "unbind_activity_result"; + + static final String ACTION_BIND_SERVICE = "bind_service"; + static final String BIND_SERVICE_RESULT = "bind_service_result"; + + static final String ACTION_GET_BINDED_SERVICE = "get_binded_service"; + static final String GET_BINDED_SERVICE_RESULT = "get_binded_service_result"; + + static final String ACTION_UNBIND_SERVICE = "unbind_service"; + static final String UNBIND_SERVICE_RESULT = "unbind_service_result"; + + static final String ACTION_BIND_RECEIVER = "bind_receiver"; + static final String BIND_RECEIVER_RESULT = "bind_receiver_result"; + + static final String ACTION_IS_EXACT = "is_exact"; + static final String IS_EXACT_RESULT = "is_exact_result"; + + static final String ACTION_IS_STUB = "is_stub"; + static final String IS_STUB_RESULT = "is_stub_result"; + + static final String ACTION_IS_PLUGIN_RUNNING = "is_plugin_running"; + static final String IS_PLUGIN_RUNNING_RESULT = "is_plugin_running_result"; + + static final String ACTION_WAKEUP_PLUGIN = "wakeup_plugin"; + static final String WAKEUP_PLUGIN_RESULT = "wakeup_plugin_result"; + + static final String ACTION_DUMP_SERVICE_INFO = "dump_service_info"; + static final String DUMP_SERVICE_INFO_RESULT = "dump_service_info_result"; + + static final String ACTION_REBOOT_PLUGIN_PROCESS = "reboot_plugin_process"; + + + private PluginManagerService managerService; + private PluginStatusChangeListener changeListener; + private Handler mainHandler; + + public static Uri buildUri() { + if (CONTENT_URI == null) { + CONTENT_URI = Uri.parse("content://"+ FairyGlobal.getHostApplication().getPackageName() + ".manager" + "/call"); + } + return CONTENT_URI; + } + + public PluginManagerProvider() { + Log.e("PluginManagerProvider", "create instance"); + } + + @Override + public boolean onCreate() { + + Log.d("PluginManagerProvider", "onCreate, Thread id " + Thread.currentThread().getId() + " name " + Thread.currentThread().getName() + " pid " + Process.myPid()); + + mainHandler = new Handler(Looper.getMainLooper()); + managerService = new PluginManagerService(); + changeListener = new PluginCallbackImpl(); + managerService.loadEnabledPlugins(); + + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (Binder.getCallingUid() != Process.myUid()) { + throw new UnsupportedOperationException(); + } + + LogUtil.d("跨进程调用统计", + "Thread id", Thread.currentThread().getId(), + "name", Thread.currentThread().getName(), + "method", method, + "arg", arg); + + return dispathToManager(method, arg, extras); + } + + /** + * 在跨进程的调用的情况下,provider的方法在binder的线程中被调用, + * 这个方法可能存在多线程问题 + * 但是直接在dispathToManager这个方法上加同步可能存在死锁风险 + * 因此在managerService等具体的非查询方法上都加了同步处理 + */ + private Bundle dispathToManager(String method, String arg, Bundle extras) { + + Bundle bundle = new Bundle(); + + if (ACTION_INSTALL.equals(method)) { + + InstallResult result = managerService.installPlugin(arg); + bundle.putInt(INSTALL_RESULT, result.getResult()); + + changeListener.onInstall(result.getResult(), result.getPackageName(), result.getVersion(), arg); + + return bundle; + + } else if (ACTION_REMOVE.equals(method)) { + + int code = managerService.remove(arg); + bundle.putInt(REMOVE_RESULT, code); + + changeListener.onRemove(arg, code); + + return bundle; + + } else if (ACTION_REMOVE_ALL.equals(method)) { + + boolean success = managerService.removeAll(); + bundle.putBoolean(REMOVE_ALL_RESULT, success); + + return bundle; + + } else if (ACTION_QUERY_BY_ID.equals(method)) { + + PluginDescriptor pluginDescriptor = managerService.getPluginDescriptorByPluginId(arg); + bundle.putSerializable(QUERY_BY_ID_RESULT, pluginDescriptor); + + return bundle; + + } else if (ACTION_QUERY_BY_CLASS_NAME.equals(method)) { + + PluginDescriptor pluginDescriptor = managerService.getPluginDescriptorByClassName(arg); + bundle.putSerializable(QUERY_BY_CLASS_NAME_RESULT, pluginDescriptor); + + return bundle; + + } else if (ACTION_QUERY_BY_FRAGMENT_ID.equals(method)) { + + PluginDescriptor pluginDescriptor = managerService.getPluginDescriptorByFragmenetId(arg); + bundle.putSerializable(QUERY_BY_FRAGMENT_ID_RESULT, pluginDescriptor); + + return bundle; + + } else if (ACTION_QUERY_ALL.equals(method)) { + + Collection pluginDescriptorList = managerService.getPlugins(); + ArrayList result = new ArrayList(pluginDescriptorList.size()); + result.addAll(pluginDescriptorList); + bundle.putSerializable(QUERY_ALL_RESULT, result); + + return bundle; + + } else if (ACTION_BIND_ACTIVITY.equals(method)) { + + bundle.putString(BIND_ACTIVITY_RESULT, PluginStubBinding.bindStub(arg, extras.getString("packageName"), StubMappingProcessor.TYPE_ACTIVITY)); + + return bundle; + + } else if (ACTION_UNBIND_ACTIVITY.equals(method)) { + + PluginStubBinding.unBind(arg, extras.getString("className"), StubMappingProcessor.TYPE_ACTIVITY); + + } else if (ACTION_BIND_SERVICE.equals(method)) { + bundle.putString(BIND_SERVICE_RESULT, PluginStubBinding.bindStub(arg, null, StubMappingProcessor.TYPE_SERVICE)); + + return bundle; + + } else if (ACTION_GET_BINDED_SERVICE.equals(method)) { + bundle.putString(GET_BINDED_SERVICE_RESULT, PluginStubBinding.getBindedPluginClassName(arg, StubMappingProcessor.TYPE_SERVICE)); + + return bundle; + + } else if (ACTION_UNBIND_SERVICE.equals(method)) { + + PluginStubBinding.unBind(null, arg, StubMappingProcessor.TYPE_SERVICE); + + } else if (ACTION_BIND_RECEIVER.equals(method)) { + bundle.putString(BIND_RECEIVER_RESULT, PluginStubBinding.bindStub(arg, null, StubMappingProcessor.TYPE_RECEIVER)); + + return bundle; + + } else if (ACTION_IS_EXACT.equals(method)) { + bundle.putBoolean(IS_EXACT_RESULT, StubExact.isExact(arg, extras.getInt("type"))); + + return bundle; + + } else if (ACTION_IS_STUB.equals(method)) { + bundle.putBoolean(IS_STUB_RESULT, PluginStubBinding.isStub(arg)); + + return bundle; + + } else if (ACTION_DUMP_SERVICE_INFO.equals(method)) { + bundle.putString(DUMP_SERVICE_INFO_RESULT, "TODO: not implement yet"); + + return bundle; + } else if (ACTION_IS_PLUGIN_RUNNING.equals(method)) { + bundle.putBoolean(IS_PLUGIN_RUNNING_RESULT, PluginLauncher.instance().isRunning(arg)); + + return bundle; + } else if (ACTION_WAKEUP_PLUGIN.equals(method)) { + LoadedPlugin loadedPlugin = PluginLauncher.instance().startPlugin(arg); + bundle.putBoolean(WAKEUP_PLUGIN_RESULT, loadedPlugin!=null); + + return bundle; + } else if (ACTION_STOP.equals(method)) { + PluginDescriptor pluginDescriptor = managerService.getPluginDescriptorByPluginId(arg); + PluginLauncher.instance().stopPlugin(arg, pluginDescriptor); + bundle.putBoolean(STOP_RESULT, true); + + return bundle; + } else if (ACTION_REBOOT_PLUGIN_PROCESS.equals(method)) { + mainHandler.post(new Runnable() { + @Override + public void run() { + ArrayList list = PluginManagerHelper.getPlugins(); + for(PluginDescriptor descriptor: list) { + PluginManagerHelper.stop(descriptor.getPackageName()); + } + //杀进程不能在binder线程执行,否则会导致调用方和被调用方都被杀掉 + LogUtil.w("killProcess,exit"); + Process.killProcess(Process.myPid()); + System.exit(10); + } + }); + return null; + } + + return null; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProviderClient.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProviderClient.java new file mode 100644 index 00000000..a8eccef4 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerProviderClient.java @@ -0,0 +1,216 @@ +package com.limpoxe.fairy.manager; + +import android.os.Bundle; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.compat.CompatForContentProvider; + +import java.util.ArrayList; + +/** + * Created by cailiming on 17/1/25. + * + */ +public class PluginManagerProviderClient { + + @SuppressWarnings("unchecked") + public static ArrayList queryAll() { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_QUERY_ALL, null, null); + + ArrayList list = null; + if (bundle != null) { + list = (ArrayList)bundle.getSerializable(PluginManagerProvider.QUERY_ALL_RESULT); + } + //防止NPE + if (list == null) { + list = new ArrayList(); + } + return list; + } + + public static PluginDescriptor queryById(String pluginId) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_QUERY_BY_ID, pluginId, null); + if (bundle != null) { + return (PluginDescriptor) bundle.getSerializable(PluginManagerProvider.QUERY_BY_ID_RESULT); + } + return null; + } + + public static PluginDescriptor queryByClass(String clazzName) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_QUERY_BY_CLASS_NAME, clazzName, null); + if (bundle != null) { + return (PluginDescriptor)bundle.getSerializable(PluginManagerProvider.QUERY_BY_CLASS_NAME_RESULT); + } + return null; + } + + public static PluginDescriptor queryByFragment(String clazzId) { + + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_QUERY_BY_FRAGMENT_ID, clazzId, null); + if (bundle != null) { + return (PluginDescriptor)bundle.getSerializable(PluginManagerProvider.QUERY_BY_FRAGMENT_ID_RESULT); + } + return null; + } + + public static int install(String srcFile) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_INSTALL, srcFile, null); + + int result = PluginManagerHelper.INSTALL_FAIL;//install-Fail + if (bundle != null) { + result = bundle.getInt(PluginManagerProvider.INSTALL_RESULT); + } + return result; + } + + public static int remove(String pluginId) { + Bundle result = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_REMOVE, pluginId, null); + if (result != null) { + return result.getInt(PluginManagerProvider.REMOVE_RESULT, PluginManagerHelper.REMOVE_FAIL); + } + return PluginManagerHelper.REMOVE_FAIL; + } + + public static boolean removeAll() { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_REMOVE_ALL, null, null); + if (bundle != null) { + return bundle.getBoolean(PluginManagerProvider.REMOVE_ALL_RESULT); + } + return false; + } + + public static String bindStubReceiver(String className) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_BIND_RECEIVER, className, null); + if (bundle != null) { + return bundle.getString(PluginManagerProvider.BIND_RECEIVER_RESULT); + } + return null; + } + + public static String bindStubActivity(String pluginActivityClassName, int launchMode, String packageName, String themeId, String orientation) { + Bundle arg = new Bundle(); + arg.putInt("launchMode", launchMode); + arg.putString("packageName", packageName); + arg.putString("themeId", themeId); + if (orientation != null) { + arg.putInt("orientation", Integer.valueOf(orientation)); + } + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_BIND_ACTIVITY, + pluginActivityClassName, arg); + if (bundle != null) { + return bundle.getString(PluginManagerProvider.BIND_ACTIVITY_RESULT); + } + return null; + } + + public static boolean isExact(String name, int type) { + Bundle arg = new Bundle(); + arg.putInt("type", type); + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_IS_EXACT, + name, arg); + if (bundle != null) { + return bundle.getBoolean(PluginManagerProvider.IS_EXACT_RESULT); + } + return false; + } + + public static void unBindLaunchModeStubActivity(String activityName, String className) { + Bundle arg = new Bundle(); + arg.putString("className", className); + CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_UNBIND_ACTIVITY, + activityName, arg); + } + + public static String getBindedPluginServiceName(String stubServiceName) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_GET_BINDED_SERVICE, + stubServiceName, null); + if (bundle != null) { + return bundle.getString(PluginManagerProvider.GET_BINDED_SERVICE_RESULT); + } + return null; + } + + public static String bindStubService(String pluginServiceClassName) { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_BIND_SERVICE, + pluginServiceClassName, null); + if (bundle != null) { + return bundle.getString(PluginManagerProvider.BIND_SERVICE_RESULT); + } + return null; + } + + public static void unBindStubService(String pluginServiceName) { + CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_UNBIND_SERVICE, + pluginServiceName, null); + } + + public static boolean isStub(String className) { + //这里如果约定stub组件的名字以特定词开头可以省去provider调用,减少跨进程,提高效率 + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_IS_STUB, + className, null); + if (bundle != null) { + return bundle.getBoolean(PluginManagerProvider.IS_STUB_RESULT); + } + return false; + } + + public static String dumpServiceInfo() { + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_DUMP_SERVICE_INFO, + null, null); + if (bundle != null) { + return bundle.getString(PluginManagerProvider.DUMP_SERVICE_INFO_RESULT); + } + return null; + } + + public static boolean isRunning(String pluginId) { + //这里如果约定stub组件的名字以特定词开头可以省去provider调用,减少跨进程,提高效率 + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_IS_PLUGIN_RUNNING, + pluginId, null); + if (bundle != null) { + return bundle.getBoolean(PluginManagerProvider.IS_PLUGIN_RUNNING_RESULT); + } + return false; + } + + public static boolean wakeup(String pluginId) { + //这里如果约定stub组件的名字以特定词开头可以省去provider调用,减少跨进程,提高效率 + Bundle bundle = CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_WAKEUP_PLUGIN, + pluginId, null); + if (bundle != null) { + return bundle.getBoolean(PluginManagerProvider.WAKEUP_PLUGIN_RESULT); + } + return false; + } + + public static void stop(String pluginId) { + CompatForContentProvider.call(PluginManagerProvider.buildUri(), + PluginManagerProvider.ACTION_STOP, + pluginId, null); + } + + public static void rebootPluginProcess() { + //杀掉插件进程 + CompatForContentProvider.call(PluginManagerProvider.buildUri(), PluginManagerProvider.ACTION_REBOOT_PLUGIN_PROCESS, null, null); + //唤起插件进程 + CompatForContentProvider.call(PluginManagerProvider.buildUri(), null, null, null); + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerService.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerService.java new file mode 100644 index 00000000..3b46bc52 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManagerService.java @@ -0,0 +1,498 @@ +package com.limpoxe.fairy.manager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Base64; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginCreator; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.core.localservice.LocalServiceManager; +import com.limpoxe.fairy.util.FileUtil; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.PackageVerifyer; +import com.limpoxe.fairy.util.ProcessUtil; +import com.limpoxe.fairy.util.RefInvoker; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +class PluginManagerService { + private static final String SP_NAME = "plugins.installed"; + private static final String ENABLED_KEY = "plugins.list"; + + private Object mLock = new Object(); + private final Hashtable mEnabledPlugins = new Hashtable(); + + PluginManagerService() { + if (FairyGlobal.isInited()) {//防止集成了插件框架但是没有调用init导致app起不来 + if (!ProcessUtil.isPluginProcess()) { + throw new IllegalAccessError("本类仅在插件进程使用"); + } + } else { + LogUtil.e("插件框架未初始化!"); + LogUtil.printStackTrace(); + } + } + + private static String genTmpPath(File srcFile) { + return FairyGlobal.getHostApplication().getCacheDir().getAbsolutePath() + File.separator + System.currentTimeMillis() + "_" + srcFile.getName(); + } + + @SuppressWarnings("unchecked") + void loadEnabledPlugins() { + synchronized (mLock) { + if (mEnabledPlugins.size() == 0) { + long t1 = System.currentTimeMillis(); + Hashtable installedPlugin = readPlugins(ENABLED_KEY); + if (installedPlugin != null) { + mEnabledPlugins.putAll(installedPlugin); + } + long t2 = System.currentTimeMillis(); + LogUtil.i("加载所有插件列表, 耗时 : " + (t2 - t1)); + } + } + } + + private boolean updateEnabledPlugins(PluginDescriptor pluginDescriptor) { + mEnabledPlugins.put(pluginDescriptor.getPackageName(), pluginDescriptor); + boolean isSaveSuccess = writePlugins(ENABLED_KEY, mEnabledPlugins); + if (!isSaveSuccess) { + mEnabledPlugins.remove(pluginDescriptor.getPackageName()); + } + return isSaveSuccess; + } + + boolean removeAll() { + synchronized (mLock) { + LogUtil.w("卸载所有插件"); + Iterator> itr = mEnabledPlugins.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = itr.next(); + PluginLauncher.instance().stopPlugin(entry.getKey(), entry.getValue()); + } + + mEnabledPlugins.clear(); + boolean isSuccess = writePlugins(ENABLED_KEY, mEnabledPlugins); + + File rootDir = new File(PluginDescriptor.getFairyDir()); + LogUtil.w("删除文件夹", rootDir.getAbsolutePath()); + FileUtil.deleteAll(rootDir); + LogUtil.w("删除完成"); + + return isSuccess; + } + } + + int remove(String pluginId) { + synchronized (mLock) { + LogUtil.w("卸载插件", pluginId); + PluginDescriptor old = mEnabledPlugins.get(pluginId); + if (old != null) { + PluginLauncher.instance().stopPlugin(pluginId, old); + mEnabledPlugins.remove(pluginId); + writePlugins(ENABLED_KEY, mEnabledPlugins); + + File dir = new File(old.getVersionedRootDir()); + LogUtil.w("删除插件目录", pluginId, dir.getAbsolutePath()); + boolean deleteSuccess = FileUtil.deleteAll(dir); + LogUtil.e("删除完成"); + + if (deleteSuccess) { + return PluginManagerHelper.REMOVE_SUCCESS; + } else { + LogUtil.e("remove:REMOVE_FAIL", pluginId); + return PluginManagerHelper.REMOVE_FAIL; + } + } else { + LogUtil.e("remove:REMOVE_FAIL_PLUGIN_NOT_EXIST", pluginId); + return PluginManagerHelper.REMOVE_FAIL_PLUGIN_NOT_EXIST; + } + } + } + + Collection getPlugins() { + return mEnabledPlugins.values(); + } + + /** + * for Fragment + * + * @param clazzId + * @return + */ + PluginDescriptor getPluginDescriptorByFragmenetId(String clazzId) { + Iterator itr = mEnabledPlugins.values().iterator(); + while (itr.hasNext()) { + PluginDescriptor descriptor = itr.next(); + if (descriptor.containsFragment(clazzId)) { + return descriptor; + } + } + return null; + } + + PluginDescriptor getPluginDescriptorByPluginId(String pluginId) { + PluginDescriptor pluginDescriptor = mEnabledPlugins.get(pluginId); + if (pluginDescriptor != null && pluginDescriptor.isEnabled()) { + return pluginDescriptor; + } + return null; + } + + PluginDescriptor getPluginDescriptorByClassName(String clazzName) { + Iterator itr = mEnabledPlugins.values().iterator(); + while (itr.hasNext()) { + PluginDescriptor descriptor = itr.next(); + if (descriptor.containsName(clazzName)) { + return descriptor; + } + } + return null; + } + + /** + * 安装一个插件 + * + * @param srcPluginFile + * @return + */ + InstallResult installPlugin(String srcPluginFile) { + synchronized (mLock) { + LogUtil.w("开始安装插件", srcPluginFile); + long startAt = System.currentTimeMillis(); + if (TextUtils.isEmpty(srcPluginFile) || !FileUtil.checkPathSafe(srcPluginFile)) { + LogUtil.e("fail::SRC_FILE_NOT_FOUND", srcPluginFile); + return new InstallResult(PluginManagerHelper.SRC_FILE_NOT_FOUND); + } + File srcFile = new File(srcPluginFile); + if (!srcFile.exists() || !srcFile.isFile()) { + LogUtil.e("fail::SRC_FILE_NOT_FOUND", srcPluginFile); + return new InstallResult(PluginManagerHelper.SRC_FILE_NOT_FOUND); + } + try { + // 解析相对路径,得到真实绝对路径 + srcPluginFile = srcFile.getCanonicalPath(); + } catch (IOException e) { + LogUtil.printException("PluginManagerService.installPlugin", e); + LogUtil.e("fail::getCanonicalPath", srcPluginFile); + return new InstallResult(PluginManagerHelper.INSTALL_FAIL); + } + + // 先将apk复制到宿主程序私有目录,防止在安装过程中文件被篡改 + if (!srcPluginFile.startsWith(FairyGlobal.getHostApplication().getCacheDir().getAbsolutePath())) { + String tempFilePath = genTmpPath(srcFile); + LogUtil.w("先将apk复制到宿主程序私有目录", tempFilePath); + if (!FileUtil.copyFile(srcPluginFile, tempFilePath)) { + new File(tempFilePath).delete(); + LogUtil.e("fail::COPY_FILE_FAIL", srcPluginFile, tempFilePath); + return new InstallResult(PluginManagerHelper.COPY_FILE_FAIL); + } + srcPluginFile = tempFilePath; + } + + // 解析Manifest,获得插件详情 + LogUtil.w("解析插件Manifest", srcPluginFile); + final PluginDescriptor pluginDescriptor = PluginManifestParser.parseManifest(srcPluginFile); + if (pluginDescriptor == null || TextUtils.isEmpty(pluginDescriptor.getPackageName())) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::PARSE_MANIFEST_FAIL", srcPluginFile); + return new InstallResult(PluginManagerHelper.PARSE_MANIFEST_FAIL); + } + pluginDescriptor.setFileSize(new File(srcPluginFile).length()); + + LogUtil.w("插件信息", pluginDescriptor.getPackageName(), pluginDescriptor.getVersion(), pluginDescriptor.isStandalone(), pluginDescriptor.getAutoStart()); + if (pluginDescriptor.getPackageName().indexOf(File.separatorChar) >= 0 || pluginDescriptor.getVersion().indexOf(File.separatorChar) >= 0) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::PARSE_MANIFEST_FAIL", srcPluginFile); + return new InstallResult(PluginManagerHelper.PARSE_MANIFEST_FAIL); + } + + // 检查插件适用系统版本 + LogUtil.w("检查插件适用系统版本", pluginDescriptor.getMinSdkVersion(), Build.VERSION.SDK_INT); + if (pluginDescriptor.getMinSdkVersion() != null && Build.VERSION.SDK_INT < Integer.valueOf(pluginDescriptor.getMinSdkVersion())) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::MIN_API_NOT_SUPPORTED", pluginDescriptor.getPackageName(), "系统:" + Build.VERSION.SDK_INT, "插件:" + pluginDescriptor.getMinSdkVersion()); + return new InstallResult(PluginManagerHelper.MIN_API_NOT_SUPPORTED, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + + // 验证插件APK签名,如果被篡改过,将获取不到证书 + // 之所以把验证签名步骤在放在验证适用系统版本之后, + // 是因为不同的minSdkVersion在签名时使用的sha算法长度不同, + // 也即高版本的minSdkVersion的插件,即使签名没有被篡改过,在低版本的系统中仍然会校验失败 + // 所以先校验minSdkVersion,再校验签名 + LogUtil.w("读取插件APK签名"); + Signature[] pluginSignatures = FairyGlobal.getHostApplication().getPackageManager().getPackageArchiveInfo(srcPluginFile, PackageManager.GET_SIGNATURES).signatures; + if (pluginSignatures == null) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::SIGNATURES_INVALIDATE", srcPluginFile); + return new InstallResult(PluginManagerHelper.SIGNATURES_INVALIDATE); + } + + // 可选步骤,验证插件APK证书是否和宿主程序证书相同。 + // 证书中存放的是公钥和算法信息,而公钥和私钥是1对1的 + // 公钥相同意味着是同一个作者发布的程序 + LogUtil.w("检查插件和宿主签名(调用FairyGlobal.setNeedVerifyPlugin()可关闭检查)"); + boolean debuggable = (0 != (FairyGlobal.getHostApplication().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)); + if (FairyGlobal.isNeedVerifyPlugin() && !debuggable) { + Signature[] mainSignatures = null; + try { + PackageInfo pkgInfo = FairyGlobal.getHostApplication().getPackageManager().getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), PackageManager.GET_SIGNATURES); + mainSignatures = pkgInfo.signatures; + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("PluginManagerService.installPlugin", e); + } + if (!PackageVerifyer.isSignaturesSame(mainSignatures, pluginSignatures)) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::VERIFY_SIGNATURES_FAIL", srcPluginFile); + return new InstallResult(PluginManagerHelper.VERIFY_SIGNATURES_FAIL); + } + } + + // 检查当前宿主版本是否匹配此非独立插件需要的版本 + LogUtil.w("检查插件和宿主是否兼容"); + if (!PackageVerifyer.isCompatibleWithHost(pluginDescriptor)) { + //不满足要求,不可安装此插件 + new File(srcPluginFile).delete(); + LogUtil.e("fail::HOST_VERSION_NOT_SUPPORT_CURRENT_PLUGIN", pluginDescriptor.getPackageName()); + return new InstallResult(PluginManagerHelper.HOST_VERSION_NOT_SUPPORT_CURRENT_PLUGIN, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + + boolean isOldRunning = false; + boolean isSameVersion = false; + PluginDescriptor oldPluginDescriptor = getPluginDescriptorByPluginId(pluginDescriptor.getPackageName()); + if (oldPluginDescriptor != null) { + // 检查现有插件版本和要更新的插件版本是否相同 + LogUtil.w("检查插件版本是否有变化(调用FairyGlobal.setInstallationWithSameVersion()可关闭坚持)"); + isOldRunning = PluginLauncher.instance().isRunning(oldPluginDescriptor.getPackageName()); + isSameVersion = oldPluginDescriptor.getVersion().equals(pluginDescriptor.getVersion()); + //版本号无变化,不需要安装 + if (!FairyGlobal.isInstallationWithSameVersion() && isSameVersion) { + new File(srcPluginFile).delete(); + LogUtil.e("fail::SAME_VERSION", oldPluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + return new InstallResult(PluginManagerHelper.FAIL_BECAUSE_SAME_VER_HAS_LOADED, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + } + + final String destApkPath = pluginDescriptor.getInstalledPath(); + LogUtil.w("复制插件到插件目录", destApkPath); + boolean isCopySuccess = FileUtil.copyFile(srcPluginFile, destApkPath); + new File(srcPluginFile).delete(); + srcPluginFile = null; + if (!isCopySuccess) { + LogUtil.e("fail::COPY_FILE_FAIL", destApkPath); + return new InstallResult(PluginManagerHelper.COPY_FILE_FAIL, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + + pluginDescriptor.setInstallationTime(System.currentTimeMillis()); + PackageInfo packageInfo = pluginDescriptor.getPackageInfo(PackageManager.GET_GIDS); + if (packageInfo != null) { + LogUtil.v("设置theme、logo、icon", pluginDescriptor.getInstalledPath()); + pluginDescriptor.setApplicationTheme(packageInfo.applicationInfo.theme); + pluginDescriptor.setApplicationIcon(packageInfo.applicationInfo.icon); + pluginDescriptor.setApplicationLogo(packageInfo.applicationInfo.logo); + } + + // 先解压so到临时目录,再从临时目录复制到插件so目录。 在构造插件Dexclassloader的时候,会使用这个so目录作为参数 + File tempUnzipDir = new File(pluginDescriptor.getVersionedRootDir(), "temp"); + Set soList = FileUtil.unZipSo(pluginDescriptor.getInstalledPath(), tempUnzipDir); + if (soList != null) {//TODO soList插件中所有so的名字列表,如果插件中不同cpu架构下的so个数不相等可能会复制不匹配的so + ArrayList abiList = getSupportedAbis(); + LogUtil.w("复制so", pluginDescriptor.getNativeLibDir()); + for (String soName : soList) { + FileUtil.copySo2(tempUnzipDir, soName, pluginDescriptor.getNativeLibDir(), abiList); + } + //删掉临时文件 + LogUtil.v("删除so的临时解压目录", tempUnzipDir); + FileUtil.deleteAll(tempUnzipDir); + LogUtil.v("删除完成"); + } + + //try { + //ArrayList multiDexFiles = PluginMultiDexExtractor.performExtractions(new File(destApkPath), new File(apkParent, "secondDexes")); + //pluginDescriptor.setMuliDexList(multiDexFiles); + //} catch (IOException e) { + // e.printStackTrace(); + //} + + File dalvikCacheDir = new File(pluginDescriptor.getDalvikCacheDir()); + LogUtil.v("删除DEXOPT缓存目录", dalvikCacheDir.getAbsolutePath()); + FileUtil.deleteAll(dalvikCacheDir); + LogUtil.v("删除完成", dalvikCacheDir.getAbsolutePath()); + + LogUtil.v("触发DEXOPT...", pluginDescriptor.getInstalledPath()); + //ActivityThread.getPackageManager().performDexOptIfNeeded() + ClassLoader cl = PluginCreator.createPluginClassLoader( + pluginDescriptor.getPackageName(), + pluginDescriptor.getInstalledPath(), + pluginDescriptor.getDalvikCacheDir(), + pluginDescriptor.getNativeLibDir(), + pluginDescriptor.isStandalone(), + null, + null); + try { + cl.loadClass(Object.class.getName()); + cl = null; + } catch (ClassNotFoundException e) { + LogUtil.printException("PluginManagerService.installPlugin", e); + } + LogUtil.v("DEXOPT完毕"); + //打印一下目录结构 + if (debuggable) { + FileUtil.printAll(new File(FairyGlobal.getHostApplication().getApplicationInfo().dataDir)); + } + + //这个步骤真正完成插件更替 + LogUtil.v("开始插件更替", pluginDescriptor.getPackageName()); + //如果版本相同,不能删除。因为版本相同时新版本和旧版本的安装目录是同一个 + if (!isSameVersion) { + remove(pluginDescriptor.getPackageName()); + } + boolean isInstallSuccess = updateEnabledPlugins(pluginDescriptor); + LogUtil.v("结束插件更替", pluginDescriptor.getPackageName(), isInstallSuccess); + if (!isInstallSuccess) { + LogUtil.e("fail::INSTALL_FAIL", pluginDescriptor.getPackageName()); + return new InstallResult(PluginManagerHelper.INSTALL_FAIL, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + LogUtil.w("安装" + pluginDescriptor.getPackageName() + "成功,耗时(ms) : " + (System.currentTimeMillis() - startAt)); + + LogUtil.v("注册插件内定义的localService"); + LocalServiceManager.registerService(pluginDescriptor); + LogUtil.v("注册完成"); + + //如果是自启动插件,或者安装时旧版本插件正处于运行中,则新版本安装完成后立即启动 + if (pluginDescriptor.getAutoStart() || isOldRunning) { + postWakeup(pluginDescriptor); + } + return new InstallResult(PluginManagerHelper.SUCCESS, pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); + } + } + + private void postWakeup(final PluginDescriptor pluginDescriptor) { + LogUtil.w("唤醒插件", pluginDescriptor.getPackageName()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + boolean succ = PluginManagerHelper.wakeup(pluginDescriptor.getPackageName()); + LogUtil.w("立即唤醒" + (succ?"成功":"失败"), pluginDescriptor.getPackageName()); + } + }); + } + + private static ArrayList getSupportedAbis() { + + ArrayList abiList = new ArrayList<>(); + + String defaultAbi = (String) RefInvoker.getField(FairyGlobal.getHostApplication().getApplicationInfo(), ApplicationInfo.class, "primaryCpuAbi"); + abiList.add(defaultAbi); + + if (Build.VERSION.SDK_INT >= 21) { + String[] abis = Build.SUPPORTED_ABIS; + if (abis != null) { + for (String abi: abis) { + abiList.add(abi); + } + } + } else { + abiList.add(Build.CPU_ABI); + abiList.add(Build.CPU_ABI2); + abiList.add("armeabi"); + } + return abiList; + } + + private static SharedPreferences getSharedPreference() { + SharedPreferences sp = FairyGlobal.getHostApplication().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + return sp; + } + + private boolean writePlugins(String key, Hashtable plugins) { + + ObjectOutputStream objectOutputStream = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(plugins); + objectOutputStream.flush(); + + byte[] data = byteArrayOutputStream.toByteArray(); + String list = Base64.encodeToString(data, Base64.DEFAULT); + + getSharedPreference().edit().putString(key, list).commit(); + return true; + } catch (Exception e) { + LogUtil.printException("PluginManagerService.savePlugins", e); + } finally { + if (objectOutputStream != null) { + try { + objectOutputStream.close(); + } catch (IOException e) { + LogUtil.printException("PluginManagerService.savePlugins", e); + } + } + if (byteArrayOutputStream != null) { + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + LogUtil.printException("PluginManagerService.savePlugins", e); + } + } + } + return false; + } + + @SuppressWarnings("unchecked") + private Hashtable readPlugins(String key) { + String list = getSharedPreference().getString(key, ""); + Serializable object = null; + if (!TextUtils.isEmpty(list)) { + ByteArrayInputStream byteArrayInputStream = null; + ObjectInputStream objectInputStream = null; + try { + byteArrayInputStream = new ByteArrayInputStream(Base64.decode(list, Base64.DEFAULT)); + objectInputStream = new ObjectInputStream(byteArrayInputStream); + object = (Serializable) objectInputStream.readObject(); + } catch (Exception e) { + LogUtil.printException("PluginManagerService.readPlugins", e); + } finally { + if (objectInputStream != null) { + try { + objectInputStream.close(); + } catch (IOException e) { + LogUtil.printException("PluginManagerService.readPlugins", e); + } + } + if (byteArrayInputStream != null) { + try { + byteArrayInputStream.close(); + } catch (IOException e) { + LogUtil.printException("PluginManagerService.readPlugins", e); + } + } + } + } + + return (Hashtable) object; + } + +} \ No newline at end of file diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManifestParser.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManifestParser.java new file mode 100644 index 00000000..f705f575 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginManifestParser.java @@ -0,0 +1,399 @@ +package com.limpoxe.fairy.manager; + +import android.app.Application; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.AssetManager; +import android.content.res.XmlResourceParser; +import android.text.TextUtils; + +import com.limpoxe.fairy.content.PluginActivityInfo; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.content.PluginIntentFilter; +import com.limpoxe.fairy.content.PluginProviderInfo; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.android.HackAssetManager; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +public class PluginManifestParser { + public static String PREVIOUS = "unsafe.proxy."; + + public static PluginDescriptor parseManifest(String pluginPath) { + try { + + AssetManager assetManager = AssetManager.class.newInstance(); + new HackAssetManager(assetManager).addAssetPath(pluginPath); + XmlResourceParser parser = assetManager.openXmlResourceParser("AndroidManifest.xml"); + + int eventType = parser.getEventType(); + String namespaceAndroid = null; + String packageName = null; + + ArrayList dependencies = null; + + PluginDescriptor desciptor = new PluginDescriptor(); + do { + switch (eventType) { + case XmlPullParser.START_DOCUMENT: { + break; + } + case XmlPullParser.START_TAG: { + String tag = parser.getName(); + if ("manifest".equals(tag)) { + + namespaceAndroid = parser.getAttributeNamespace(0); + if (TextUtils.isEmpty(namespaceAndroid)) { + namespaceAndroid = "http://schemas.android.com/apk/res/android"; + } + + packageName = parser.getAttributeValue(null, "package"); + String useHostPackageName = parser.getAttributeValue(null, "useHostPackageName"); + String versionCode = parser.getAttributeValue(namespaceAndroid, "versionCode"); + String versionName = parser.getAttributeValue(namespaceAndroid, "versionName"); + String platformBuildVersionCode = parser.getAttributeValue(null, "platformBuildVersionCode"); + String platformBuildVersionName = parser.getAttributeValue(null, "platformBuildVersionName"); + + //用这个字段来标记apk是独立apk,还是需要依赖主程序的class和resource + //当这个值等于宿主程序packageName时,则认为这个插件是需要依赖宿主的class和resource的 + String hostApplicationId = parser.getAttributeValue(null, "hostApplicationId"); + if (hostApplicationId == null) { + hostApplicationId = parser.getAttributeValue(namespaceAndroid, "sharedUserId"); + } + + //这个字段用来标记非独立插件以来的宿主版本号,即此当前插件仅可运行在此版本的宿主中 + //独立插件忽略此项 + String requiredHostVersionName = parser.getAttributeValue(null, "requiredHostVersionName"); + String requiredHostVersionCode = parser.getAttributeValue(null, "requiredHostVersionCode"); + + String autoStart = parser.getAttributeValue(null, "autoStart"); + + desciptor.setPackageName(packageName); + desciptor.setVersionName(versionName); + desciptor.setVersionCode(versionCode); + desciptor.setPlatformBuildVersionCode(platformBuildVersionCode); + desciptor.setPlatformBuildVersionName(platformBuildVersionName); + + desciptor.setStandalone(hostApplicationId == null || !FairyGlobal.getHostApplication().getPackageName().equals(hostApplicationId)); + if (!desciptor.isStandalone() && !TextUtils.isEmpty(requiredHostVersionName)) { + desciptor.setRequiredHostVersionName(requiredHostVersionName); + } + + desciptor.setUseHostPackageName("true".equals(useHostPackageName)); + desciptor.setAutoStart("true".equals(autoStart)); + + LogUtil.v(packageName, versionCode, versionName, hostApplicationId, FairyGlobal.getHostApplication().getPackageName()); + } else if ("uses-sdk".equals(tag)) { + + String minSdkVersion = parser.getAttributeValue(namespaceAndroid, "minSdkVersion"); + String targetSdkVersion = parser.getAttributeValue(namespaceAndroid, "targetSdkVersion"); + + desciptor.setMinSdkVersion(minSdkVersion); + desciptor.setTargetSdkVersion(targetSdkVersion); + + } else if ("meta-data".equals(tag)) { + + String type = parser.getAttributeValue(namespaceAndroid, "tag"); + String name = parser.getAttributeValue(namespaceAndroid, "name"); + String value = parser.getAttributeValue(namespaceAndroid, "value"); + + if ("exported-fragment".equals(type)) { + + value = getName(value, packageName); + if (name != null) { + + HashMap fragments = desciptor.getFragments(); + if (fragments == null) { + fragments = new HashMap(); + desciptor.setfragments(fragments); + } + fragments.put(name, value); + LogUtil.v(name, value); + + } + + } else if ("exported-service".equals(type)) { + String label = parser.getAttributeValue(namespaceAndroid, "label"); + + value = getName(value, packageName); + if (label != null) { + value = value + "|" + label; + } + if (name != null) { + + HashMap functions = desciptor.getFunctions(); + if (functions == null) { + functions = new HashMap(); + desciptor.setFunctions(functions); + } + functions.put(name, value); + LogUtil.v(name, value); + + } + + } else { + if (name != null) { + String resource = parser.getAttributeValue(namespaceAndroid, "resource"); + if (value != null && value.startsWith("@")) { + //等插件初始化的时候再处理这类meta信息 + desciptor.getMetaDataTobeInflate().put(name, ResourceUtil.covent2Hex(value)); + } else if (value != null) { + //meta-data支持的数据类型:Boolean、Float、Integer、String + if ("true".equals(value) || "false".equals(value)) { + desciptor.getMetaDataObject().put(name, Boolean.parseBoolean(value)); + } else if (value.contains(".")) { + try { + desciptor.getMetaDataObject().put(name, Float.parseFloat(value)); + } catch (Exception e) { + desciptor.getMetaDataObject().put(name, ResourceUtil.covent2Hex(value)); + } + } else { + try { + desciptor.getMetaDataObject().put(name, Integer.parseInt(value)); + } catch (Exception e) { + desciptor.getMetaDataObject().put(name, ResourceUtil.covent2Hex(value)); + } + } + } else if (resource != null && resource.startsWith("@")) { + desciptor.getMetaDataResource().put(name, ResourceUtil.parseResId(ResourceUtil.covent2Hex(resource))); + } + + LogUtil.v("meta-data", name, value, resource); + } + } + + } else if ("uses-library".equals(tag)) { + + String name = parser.getAttributeValue(namespaceAndroid, "name"); + if (name.startsWith("com.google") || name.startsWith("com.sec.android") || name.startsWith("com.here.android")) { + LogUtil.d("uses-library ignore", name); + } else { + if (dependencies == null) { + dependencies = new ArrayList(); + } + dependencies.add(name); + } + + } else if ("application".equals(tag)) { + + String applicationName = parser.getAttributeValue(namespaceAndroid, "name"); + if (applicationName == null) { + applicationName = Application.class.getName(); + } + applicationName = getName(applicationName, packageName); + desciptor.setApplicationName(applicationName); + + desciptor.setDescription(ResourceUtil.covent2Hex(parser.getAttributeValue(namespaceAndroid, "label"))); + + //这里不解析主题,后面会通过packageManager查询 + + LogUtil.v("applicationName", applicationName, " Description ", desciptor.getDescription()); + + } else if ("activity".equals(tag)) { + + String windowSoftInputMode = parser.getAttributeValue(namespaceAndroid, "windowSoftInputMode");//strin + String hardwareAccelerated = parser.getAttributeValue(namespaceAndroid, "hardwareAccelerated");//int string + String launchMode = parser.getAttributeValue(namespaceAndroid, "launchMode");//string + String screenOrientation = parser.getAttributeValue(namespaceAndroid, "screenOrientation");//string + String theme = parser.getAttributeValue(namespaceAndroid, "theme");//int + String immersive = parser.getAttributeValue(namespaceAndroid, "immersive");//int string + String uiOptions = parser.getAttributeValue(namespaceAndroid, "uiOptions");//int string + String configChanges = parser.getAttributeValue(namespaceAndroid, "configChanges");//int string + String useHostPackageName = parser.getAttributeValue(null, "useHostPackageName"); + + HashMap> map = desciptor.getActivitys(); + if (map == null) { + map = new HashMap>(); + desciptor.setActivitys(map); + } + String name = addIntentFilter(map, packageName, namespaceAndroid, parser, "activity"); + + HashMap infos = desciptor.getActivityInfos(); + if (infos == null) { + infos = new HashMap(); + desciptor.setActivityInfos(infos); + } + + PluginActivityInfo pluginActivityInfo = infos.get(name); + if (pluginActivityInfo == null) { + pluginActivityInfo = new PluginActivityInfo(); + infos.put(name, pluginActivityInfo); + } + pluginActivityInfo.setHardwareAccelerated(ResourceUtil.covent2Hex(hardwareAccelerated)); + pluginActivityInfo.setImmersive(ResourceUtil.covent2Hex(immersive)); + if (launchMode == null) { + launchMode = String.valueOf(ActivityInfo.LAUNCH_MULTIPLE); + } + pluginActivityInfo.setLaunchMode(launchMode); + pluginActivityInfo.setName(name); + pluginActivityInfo.setScreenOrientation(screenOrientation); + pluginActivityInfo.setTheme(ResourceUtil.covent2Hex(theme)); + pluginActivityInfo.setWindowSoftInputMode(windowSoftInputMode); + pluginActivityInfo.setUiOptions(uiOptions); + if (configChanges != null) { + pluginActivityInfo.setConfigChanges((int)Long.parseLong(configChanges.replace("0x", ""), 16)); + } + pluginActivityInfo.setUseHostPackageName("true".equals(useHostPackageName)); + + } else if ("receiver".equals(tag)) { + + HashMap> map = desciptor.getReceivers(); + if (map == null) { + map = new HashMap>(); + desciptor.setReceivers(map); + } + addIntentFilter(map, packageName, namespaceAndroid, parser, "receiver"); + + } else if ("service".equals(tag)) { + + String process = parser.getAttributeValue(namespaceAndroid, "process"); + + HashMap> map = desciptor.getServices(); + if (map == null) { + map = new HashMap>(); + desciptor.setServices(map); + } + String name = addIntentFilter(map, packageName, namespaceAndroid, parser, "service"); + + if (process != null) { + HashMap infos = desciptor.getServiceInfos(); + if (infos == null) { + infos = new HashMap(); + desciptor.setServiceInfos(infos); + } + infos.put(name, process); + } + + } else if ("provider".equals(tag)) { + + String name = parser.getAttributeValue(namespaceAndroid, "name"); + name = getName(name, packageName); + String author = parser.getAttributeValue(namespaceAndroid, "authorities"); + ProviderInfo[] hostProviders = new ProviderInfo[0]; + try { + hostProviders = FairyGlobal.getHostApplication().getPackageManager() + .getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), + PackageManager.GET_PROVIDERS).providers; + } catch (Exception e) { + e.printStackTrace(); + } + if (hostProviders != null) { + for(ProviderInfo hostProvider : hostProviders) { + if (hostProvider.authority.equals(author)) { + LogUtil.e("此contentProvider已经在宿主中定义,表示需要桥接, 追加一个标记", hostProvider.authority, name); + author = PREVIOUS + author; + break; + } + } + } + String exported = parser.getAttributeValue(namespaceAndroid, "exported"); + String grantUriPermissions = parser.getAttributeValue(namespaceAndroid, "grantUriPermissions"); + HashMap providers = desciptor.getProviderInfos(); + if (providers == null) { + providers = new HashMap(); + desciptor.setProviderInfos(providers); + } + + PluginProviderInfo info = new PluginProviderInfo(); + info.setName(name); + info.setExported("true".equals(exported)); + info.setAuthority(author); + info.setGrantUriPermissions("true".equals(grantUriPermissions)); + providers.put(name, info); + + LogUtil.d(name, info.isGrantUriPermissions(), grantUriPermissions, author, exported); + } + break; + } + case XmlPullParser.END_TAG: { + break; + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + desciptor.setEnabled(true); + + //有可能没有配置application节点,这里需要检查一下application + if (desciptor.getApplicationName() == null) { + desciptor.setApplicationName(Application.class.getName()); + } + + if (dependencies != null) { + desciptor.setDependencies((String[])dependencies.toArray(new String[0])); + } + + return desciptor; + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } + + return null; + } + + private static String addIntentFilter(HashMap> map, String packageName, String namespace, + XmlResourceParser parser, String endTagName) throws XmlPullParserException, IOException { + int eventType = parser.getEventType(); + String activityName = parser.getAttributeValue(namespace, "name"); + activityName = getName(activityName, packageName); + + ArrayList filters = map.get(activityName); + if (filters == null) { + filters = new ArrayList(); + map.put(activityName, filters); + } + + PluginIntentFilter intentFilter = new PluginIntentFilter(); + do { + switch (eventType) { + case XmlPullParser.START_TAG: { + String tag = parser.getName(); + if ("intent-filter".equals(tag)) { + intentFilter = new PluginIntentFilter(); + filters.add(intentFilter); + } else { + intentFilter.readFromXml(tag, namespace, parser); + } + } + } + eventType = parser.next(); + } while (!endTagName.equals(parser.getName()));//再次到达,表示一个标签结束了 + + return activityName; + } + + private static String getName(String nameOrig, String pkgName) { + if (nameOrig == null) { + return null; + } + StringBuilder sb = null; + if (nameOrig.startsWith(".")) { + sb = new StringBuilder(); + sb.append(pkgName); + sb.append(nameOrig); + } else if (!nameOrig.contains(".")) { + sb = new StringBuilder(); + sb.append(pkgName); + sb.append('.'); + sb.append(nameOrig); + } else { + return nameOrig; + } + return sb.toString(); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginStatusChangeListener.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginStatusChangeListener.java new file mode 100644 index 00000000..f469e1e2 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/PluginStatusChangeListener.java @@ -0,0 +1,8 @@ +package com.limpoxe.fairy.manager; + +/** + * Created by cailiming on 17/11/28. + */ +public interface PluginStatusChangeListener extends PluginCallback { + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/PluginStubBinding.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/PluginStubBinding.java new file mode 100644 index 00000000..752dc031 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/PluginStubBinding.java @@ -0,0 +1,83 @@ +package com.limpoxe.fairy.manager.mapping; + +import android.text.TextUtils; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.manager.PluginManagerHelper; + +import java.util.ArrayList; + +/** + * 插件组件动态绑定到宿主的虚拟stub组件 + * + * 本类只能在插件进程使用 + */ +public class PluginStubBinding { + + public static String buildDefaultAction() { + return FairyGlobal.getHostApplication().getPackageName() + ".STUB_DEFAULT"; + } + + public static synchronized String bindStub(String pluginClassName, String packageName, int type) { + ArrayList list = FairyGlobal.getStubMappingProcessors(); + if(list != null) { + for(int i = list.size() - 1; i >= 0; i--) { + StubMappingProcessor processor = list.get(i); + if (processor.getType() == type) { + PluginDescriptor pluginDescriptor = null; + if (!TextUtils.isEmpty(packageName)) { + pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); + } + String stubClass = processor.bindStub(pluginDescriptor, pluginClassName); + if (!TextUtils.isEmpty(stubClass)) { + return stubClass; + } + } + } + } + return null; + } + + public static synchronized void unBind(String stubClassName, String pluginClassName, int type) { + ArrayList list = FairyGlobal.getStubMappingProcessors(); + if(list != null) { + for(int i = list.size() - 1; i >= 0; i--) { + StubMappingProcessor processor = list.get(i); + if (processor.getType() == type) { + processor.unBindStub(stubClassName, pluginClassName); + } + } + } + } + + public static synchronized String getBindedPluginClassName(String stubClassName, int type) { + ArrayList list = FairyGlobal.getStubMappingProcessors(); + if(list != null) { + for(int i = list.size() - 1; i >= 0; i--) { + StubMappingProcessor processor = list.get(i); + if (processor.getType() == type) { + String bindedClass = processor.getBindedPluginClassName(stubClassName); + if (!TextUtils.isEmpty(bindedClass)) { + return bindedClass; + } + } + } + } + return null; + } + + public static boolean isStub(String className) { + ArrayList list = FairyGlobal.getStubMappingProcessors(); + if(list != null) { + for(int i = list.size() - 1; i >= 0; i--) { + StubMappingProcessor processor = list.get(i); + if (processor.isStub(className)) { + return true; + } + } + } + return StubExact.isExact(className, PluginDescriptor.UNKOWN); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityExactMappingProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityExactMappingProcessor.java new file mode 100644 index 00000000..c1fd9a83 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityExactMappingProcessor.java @@ -0,0 +1,54 @@ +package com.limpoxe.fairy.manager.mapping; + +import android.util.Pair; + +import com.limpoxe.fairy.content.PluginDescriptor; + +public abstract class StubActivityExactMappingProcessor implements StubMappingProcessor { + + private Pair pair; + + @Override + final public int getType() { + return TYPE_ACTIVITY; + } + + @Override + final public String bindStub(PluginDescriptor pluginDescriptor, String pluginComponentClassName) { + if (pluginComponentClassName.equals(getPluginActivityName())) { + pair = new Pair<>(getStubActivityName(), pluginComponentClassName); + return pair.first; + } + return null; + } + + @Override + final public void unBindStub(String stubClassName, String pluginStubClass) { + if (pair != null && pair.first.equals(stubClassName) && pair.second.equals(pluginStubClass)) { + pair = null; + } + } + + @Override + final public boolean isStub(String stubClassName) { + return stubClassName.equals(getStubActivityName()); + } + + @Override + final public String getBindedPluginClassName(String stubClassName) { + if (pair != null && pair.first.equals(stubClassName)) { + return pair.second; + } + return null; + } + + /** + * @return 在宿主manifest中配置的stub + */ + public abstract String getStubActivityName(); + + /** + * @return 插件中的组件名称 + */ + public abstract String getPluginActivityName(); +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityMappingProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityMappingProcessor.java new file mode 100644 index 00000000..18509263 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubActivityMappingProcessor.java @@ -0,0 +1,325 @@ +package com.limpoxe.fairy.manager.mapping; + +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static com.limpoxe.fairy.manager.mapping.PluginStubBinding.buildDefaultAction; + +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; + +import com.limpoxe.fairy.content.LoadedPlugin; +import com.limpoxe.fairy.content.PluginActivityInfo; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.PluginLauncher; +import com.limpoxe.fairy.util.LogUtil; +import com.limpoxe.fairy.util.ResourceUtil; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class StubActivityMappingProcessor implements StubMappingProcessor { + + /** + * key:stub Activity Name + * value:plugin Activity Name + */ + private static HashMap> singleTaskActivityMapping = new HashMap>(); + private static HashMap> singleTopActivityMapping = new HashMap>(); + private static HashMap> singleInstanceActivityMapping = new HashMap>(); + private static HashMap> standardActivityMapping = new HashMap>(); + private static HashMap> standardActivityTranslucentMapping = new HashMap>(); + + private static String standardLandspaceActivity = null; + + private static boolean isPoolInited = false; + + private static int sResId = -1; + + @Override + public String bindStub(PluginDescriptor pluginDescriptor, String pluginActivityClassName) { + if (StubExact.isExact(pluginActivityClassName, PluginDescriptor.ACTIVITY)) { + return pluginActivityClassName; + } + initStubPool(); + + PluginActivityInfo info = pluginDescriptor.getActivityInfos().get(pluginActivityClassName); + + HashMap> bindingMapping = null; + int launchMode = (int)Long.parseLong(info.getLaunchMode()); + + if (launchMode == ActivityInfo.LAUNCH_MULTIPLE) { + + //如果是横屏 + if (info.getScreenOrientation() != null && Integer.parseInt(info.getScreenOrientation()) == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { + return standardLandspaceActivity; + } + + if (info.getTheme() != null) { + LoadedPlugin loadedPlugin = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()); + if (loadedPlugin != null) { + try { + if (sResId == -1) { + Class r = Class.forName("com.android.internal.R$attr"); + Field f = r.getDeclaredField("windowIsTranslucent"); + f.setAccessible(true); + sResId = (int)f.get(null); + } + int styleId = ResourceUtil.parseResId(info.getTheme()); + if (styleId != 0) { + //maybe need cache + //根据目标Activity的主题id构造一个主题对象, + //并尝试从此主题中取出用于配置透明的属性:windowIsTranslucent + //如果取到了,说明目标Activity是使用的透明主题 + //则返回透明主题的stubActivity + Resources.Theme theme = loadedPlugin.pluginResource.newTheme(); + Resources.Theme baseTheme = ((ContextWrapper)loadedPlugin.pluginContext).getBaseContext().getTheme(); + if (baseTheme != null) { + theme.setTo(baseTheme); + } + theme.applyStyle(styleId, true); + TypedArray a = theme.obtainStyledAttributes(null, new int[]{sResId}, 0, 0); + if (a.hasValue(0)) { + bindingMapping = standardActivityTranslucentMapping; + } + } + } catch (ClassNotFoundException e) { + LogUtil.printException("StubActivityMappingProcessor.bindStub", e); + } catch (IllegalAccessException e) { + LogUtil.printException("StubActivityMappingProcessor.bindStub", e); + } catch (NoSuchFieldException e) { + LogUtil.printException("StubActivityMappingProcessor.bindStub", e); + } + } else { + LogUtil.e("插件尚未运行,无法获取pluginResource对象", pluginDescriptor.getPackageName()); + } + } + + if (bindingMapping == null) { + bindingMapping = standardActivityMapping; + } + + } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) { + + bindingMapping = singleTaskActivityMapping; + + } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) { + + bindingMapping = singleTopActivityMapping; + + } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) { + + bindingMapping = singleInstanceActivityMapping; + + } + + if (bindingMapping != null) { + + Iterator>> itr = bindingMapping.entrySet().iterator(); + String idleStubActivityName = null; + + while (itr.hasNext()) { + Map.Entry> entry = itr.next(); + List list = entry.getValue(); + if (list == null || list.size() == 0) { + if (idleStubActivityName == null) { + idleStubActivityName = entry.getKey(); + //这里找到空闲的stubactivity以后,还需继续遍历,用来检查是否pluginActivityClassName已经绑定过了 + } + } else { + if (list.get(0).equals(pluginActivityClassName)) { + //已绑定过,直接返回 + return entry.getKey(); + } + } + } + + //没有绑定到StubActivity,而且还有空余的stubActivity,进行绑定 + if (idleStubActivityName != null) { + List list = bindingMapping.get(idleStubActivityName); + if (list == null) { + list = new ArrayList(); + bindingMapping.put(idleStubActivityName, list); + } + list.add(pluginActivityClassName); + return idleStubActivityName; + } else { + //stub 不够用了 + LogUtil.e("stub不够用了,需要继续扩展stub占坑"); + } + + } + + //todo 或许需要一个default更保险一点? + return ""; + } + + @Override + public void unBindStub(String stubActivityName, String pluginActivityName) { + initStubPool(); + + LogUtil.v("unBindLaunchModeStubActivity", stubActivityName, pluginActivityName); + + if (reduce(singleTaskActivityMapping.get(stubActivityName), pluginActivityName)) { + return; + } + + if (reduce(singleInstanceActivityMapping.get(stubActivityName), pluginActivityName)) { + return; + } + + if (reduce(standardActivityMapping.get(stubActivityName), pluginActivityName)) { + return; + } + + if (reduce(standardActivityTranslucentMapping.get(stubActivityName), pluginActivityName)) { + return; + } + + if (reduce(singleTopActivityMapping.get(stubActivityName), pluginActivityName)) { + return; + } + } + + private boolean reduce(List pluginActivityList, String pluginActivityName) { + if (pluginActivityList != null && pluginActivityList.size() > 0 && pluginActivityList.get(0).equals(pluginActivityName)) { + LogUtil.v("unBindLaunchModeStubActivity", pluginActivityName); + pluginActivityList.remove(pluginActivityName); + return true; + } + return false; + } + + @Override + public boolean isStub(String className) { + initStubPool(); + + return standardActivityMapping.containsKey(className) + || className.equals(standardLandspaceActivity) + || standardActivityTranslucentMapping.containsKey(className) + || singleTaskActivityMapping.containsKey(className) + || singleTopActivityMapping.containsKey(className) + || singleInstanceActivityMapping.containsKey(className); + } + + @Override + public String getBindedPluginClassName(String stubClassName) { + + //桥接的,不需要反查 + if(StubExact.isExact(stubClassName, PluginDescriptor.UNKOWN)) { + return stubClassName; + } + + if (stubClassName.equals(standardLandspaceActivity)) { + //1对多的,没法反查 + return stubClassName; + } + + String target = searchMapping(standardActivityMapping, stubClassName); + if (target == null) { + target = searchMapping(standardActivityTranslucentMapping, stubClassName); + } + if (target == null) { + target = searchMapping(singleTaskActivityMapping, stubClassName); + } + if (target == null) { + target = searchMapping(singleTopActivityMapping, stubClassName); + } + if (target == null) { + target = searchMapping(singleInstanceActivityMapping, stubClassName); + } + if (target == null) { + target = stubClassName; + } + return target; + } + + private String searchMapping(HashMap> bindingMapping, String stubClassName) { + if (bindingMapping != null) { + Iterator>> itr = bindingMapping.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry> entry = itr.next(); + String stubName = entry.getKey(); + if (stubName.equals(stubClassName)) { + List list = entry.getValue(); + if (list != null && list.size() > 0) { + return list.get(0); + } else { + LogUtil.e("searchMapping fail, stubClassName : " + stubClassName + " not bind anything"); + return null; + } + } + } + } + LogUtil.e("searchMapping fail, bindingMapping is NULL"); + return null; + } + + @Override + public int getType() { + return StubMappingProcessor.TYPE_ACTIVITY; + } + + private static void loadStubActivity() { + Intent launchModeIntent = new Intent(); + launchModeIntent.setAction(buildDefaultAction()); + launchModeIntent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + + List list = FairyGlobal.getHostApplication().getPackageManager().queryIntentActivities(launchModeIntent, PackageManager.MATCH_DEFAULT_ONLY); + + if (list != null && list.size() >0) { + for (ResolveInfo resolveInfo: + list) { + if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) { + + singleTaskActivityMapping.put(resolveInfo.activityInfo.name, null); + + } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) { + + singleTopActivityMapping.put(resolveInfo.activityInfo.name, null); + + } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) { + + singleInstanceActivityMapping.put(resolveInfo.activityInfo.name, null); + + } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_MULTIPLE) { + + if (resolveInfo.activityInfo.theme == android.R.style.Theme_Translucent) { + + standardActivityTranslucentMapping.put(resolveInfo.activityInfo.name, null); + + } else if (resolveInfo.activityInfo.screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { + + standardLandspaceActivity = resolveInfo.activityInfo.name; + + } else { + + standardActivityMapping.put(resolveInfo.activityInfo.name, null); + + } + } + + } + } + + } + + private static void initStubPool() { + + if (isPoolInited) { + return; + } + + loadStubActivity(); + + isPoolInited = true; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubExact.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubExact.java new file mode 100644 index 00000000..689626d7 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubExact.java @@ -0,0 +1,79 @@ +package com.limpoxe.fairy.manager.mapping; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import com.limpoxe.fairy.core.FairyGlobal; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class StubExact { + + private static boolean isPoolInited = false; + private static Set mExcatStubSet; + + private static String buildExactAction() { + return FairyGlobal.getHostApplication().getPackageName() + ".STUB_EXACT"; + } + + private static void initStubPool() { + if (isPoolInited) { + return; + } + loadStubExactly(); + isPoolInited = true; + } + + private static void loadStubExactly() { + Intent exactStub = new Intent(); + exactStub.setAction(buildExactAction()); + exactStub.setPackage(FairyGlobal.getHostApplication().getPackageName()); + + //精确匹配的activity + List resolveInfos = FairyGlobal.getHostApplication().getPackageManager().queryIntentActivities(exactStub, PackageManager.MATCH_DEFAULT_ONLY); + + if (resolveInfos != null && resolveInfos.size() > 0) { + if (mExcatStubSet == null) { + mExcatStubSet = new HashSet(); + } + for(ResolveInfo info:resolveInfos) { + mExcatStubSet.add(info.activityInfo.name); + } + } + + //精确匹配的service + resolveInfos = FairyGlobal.getHostApplication().getPackageManager().queryIntentServices(exactStub, PackageManager.MATCH_DEFAULT_ONLY); + + if (resolveInfos != null && resolveInfos.size() > 0) { + if (mExcatStubSet == null) { + mExcatStubSet = new HashSet(); + } + for(ResolveInfo info:resolveInfos) { + mExcatStubSet.add(info.serviceInfo.name); + } + } + + //精确匹配的receiver + resolveInfos = FairyGlobal.getHostApplication().getPackageManager().queryBroadcastReceivers(exactStub, PackageManager.MATCH_DEFAULT_ONLY); + + if (resolveInfos != null && resolveInfos.size() > 0) { + if (mExcatStubSet == null) { + mExcatStubSet = new HashSet(); + } + for(ResolveInfo info:resolveInfos) { + mExcatStubSet.add(info.activityInfo.name); + } + } + } + + public static boolean isExact(String name, int type) { + initStubPool(); + if (mExcatStubSet != null && mExcatStubSet.size() > 0) { + return mExcatStubSet.contains(name); + } + return false; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubMappingProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubMappingProcessor.java new file mode 100644 index 00000000..40a92215 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubMappingProcessor.java @@ -0,0 +1,50 @@ +package com.limpoxe.fairy.manager.mapping; + +import com.limpoxe.fairy.content.PluginDescriptor; + +/** + * 插件组件动态绑定到宿主的虚拟stub组件的映射器 + * Manifest中节点下的各种组合比较多,而框架内置的stub有限,若有不同于框架内置的stub需要添加, + * 则可以通过注册StubMappingProcessor来添加自定义的处理器 + */ +public interface StubMappingProcessor { + + public static final int TYPE_ACTIVITY = 1; + public static final int TYPE_RECEIVER = 2; + public static final int TYPE_SERVICE = 3; + + /** + * 组件类型,表面这个处理器可以处理哪些类型的组件的绑定工作 + * @return + */ + int getType(); + + /** + * + * @param pluginDescriptor + * @param pluginComponentClassName 插件组件名 + * @return 返回插件组件绑定到的宿主stub组件名 + */ + String bindStub(PluginDescriptor pluginDescriptor, String pluginComponentClassName); + + /** + * 解除绑定,如果被绑定的StubClass不能同时被多个插件Class同时绑定, + * 则需要实现此接口,用于解绑,回收StubClass否则可以忽略 + */ + void unBindStub(String stubClassName, String pluginStubClass); + + /** + * 判断这个组件是否是一个stub组件 + * @param stubClassName + * @return + */ + boolean isStub(String stubClassName); + + /** + * 反查这个stub和哪个插件组件绑定了 + * @param stubClassName + * @return 插件组件名 + */ + String getBindedPluginClassName(String stubClassName); + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubReceiverMappingProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubReceiverMappingProcessor.java new file mode 100644 index 00000000..7f7655f9 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubReceiverMappingProcessor.java @@ -0,0 +1,81 @@ +package com.limpoxe.fairy.manager.mapping; + +import static com.limpoxe.fairy.manager.mapping.PluginStubBinding.buildDefaultAction; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; + +import java.util.List; + +public class StubReceiverMappingProcessor implements StubMappingProcessor { + + private static boolean isPoolInited = false; + private static String receiver = null; + + @Override + public String bindStub(PluginDescriptor pluginDescriptor, String pluginReceiverClassName) { + + if (pluginReceiverClassName != null) { + if (StubExact.isExact(pluginReceiverClassName, PluginDescriptor.BROADCAST)) { + return pluginReceiverClassName; + } + } + + initStubPool(); + + return receiver; + } + + @Override + public void unBindStub(String stubClassName, String pluginStubClass) { + //not need + } + + @Override + public boolean isStub(String stubClassName) { + initStubPool(); + + return stubClassName.equals(receiver); + } + + @Override + public String getBindedPluginClassName(String stubClassName) { + //not need + return null; + } + + @Override + public int getType() { + return TYPE_RECEIVER; + } + + private static void loadStubReceiver() { + Intent exactStub = new Intent(); + exactStub.setAction(buildDefaultAction()); + exactStub.setPackage(FairyGlobal.getHostApplication().getPackageName()); + + List resolveInfos = FairyGlobal.getHostApplication().getPackageManager().queryBroadcastReceivers(exactStub, PackageManager.MATCH_DEFAULT_ONLY); + + if (resolveInfos != null && resolveInfos.size() >0) { + //框架只内置了1个receiver,所以这里直接就get(0)了 + receiver = resolveInfos.get(0).activityInfo.name; + } + + } + + private static void initStubPool() { + + if (isPoolInited) { + return; + } + + loadStubReceiver(); + + isPoolInited = true; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubServiceMappingProcessor.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubServiceMappingProcessor.java new file mode 100644 index 00000000..d9ae0e13 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/manager/mapping/StubServiceMappingProcessor.java @@ -0,0 +1,247 @@ +package com.limpoxe.fairy.manager.mapping; + +import static com.limpoxe.fairy.manager.mapping.PluginStubBinding.buildDefaultAction; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; +import android.util.Base64; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.util.LogUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class StubServiceMappingProcessor implements StubMappingProcessor { + /** + * key:stub Service Name + * value:plugin Service Name + */ + private static HashMap serviceMapping = new HashMap(); + private static boolean isPoolInited = false; + + @Override + public String bindStub(PluginDescriptor pluginDescriptor, String pluginServiceClassName) { + if (StubExact.isExact(pluginServiceClassName, PluginDescriptor.SERVICE)) { + return pluginServiceClassName; + } + + initStubPool(); + + Iterator> itr = serviceMapping.entrySet().iterator(); + + String idleStubServiceName = null; + + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + if (entry.getValue() == null) { + if (idleStubServiceName == null) { + idleStubServiceName = entry.getKey(); + //这里找到空闲的idleStubServiceName以后,还需继续遍历,用来检查是否pluginServiceClassName已经绑定过了 + } + } else if (pluginServiceClassName.equals(entry.getValue())) { + //已经绑定过,直接返回 + LogUtil.v("已经绑定过", entry.getKey(), pluginServiceClassName); + return entry.getKey(); + } + } + + //没有绑定到StubService,而且还有空余的StubService,进行绑定 + if (idleStubServiceName != null) { + LogUtil.v("添加绑定", idleStubServiceName, pluginServiceClassName); + serviceMapping.put(idleStubServiceName, pluginServiceClassName); + //对serviceMapping持久化是因为如果service处于运行状态时app发生了crash,系统会自动恢复之前的service,此时插件映射信息查不到的话会再次crash + save(serviceMapping); + return idleStubServiceName; + } + + //绑定失败 + return null; + } + + @Override + public void unBindStub(String stubClassName, String pluginStubClass) { + initStubPool(); + + Iterator> itr = serviceMapping.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + if (pluginStubClass.equals(entry.getValue())) { + //如果存在绑定关系,解绑 + LogUtil.v("回收绑定", entry.getKey(), entry.getValue()); + serviceMapping.put(entry.getKey(), null); + save(serviceMapping); + break; + } + } + } + + @Override + public boolean isStub(String stubClassName) { + initStubPool(); + return serviceMapping.containsKey(stubClassName); + } + + @Override + public String getBindedPluginClassName(String stubServiceName) { + if (StubExact.isExact(stubServiceName, PluginDescriptor.SERVICE)) { + return stubServiceName; + } + + initStubPool(); + + Iterator> itr = serviceMapping.entrySet().iterator(); + + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + + if (entry.getKey().equals(stubServiceName)) { + return entry.getValue(); + } + } + + //没有找到,尝试重磁盘恢复 + HashMap mapping = restore(); + if (mapping != null) { + itr = mapping.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + + if (entry.getKey().equals(stubServiceName)) { + serviceMapping.put(stubServiceName, entry.getValue()); + save(serviceMapping); + return entry.getValue(); + } + } + } + + return null; + } + + @Override + public int getType() { + return TYPE_SERVICE; + } + + private static synchronized void loadStubService() { + Intent launchModeIntent = new Intent(); + launchModeIntent.setAction(buildDefaultAction()); + launchModeIntent.setPackage(FairyGlobal.getHostApplication().getPackageName()); + + List list = FairyGlobal.getHostApplication().getPackageManager().queryIntentServices(launchModeIntent, PackageManager.MATCH_DEFAULT_ONLY); + + if (list != null && list.size() >0) { + for (ResolveInfo resolveInfo: + list) { + serviceMapping.put(resolveInfo.serviceInfo.name, null); + } + HashMap mapping = restore(); + if (mapping != null) { + serviceMapping.putAll(mapping); + } + //只有service需要固化 + save(serviceMapping); + } + } + + private static boolean save(HashMap mapping) { + + ObjectOutputStream objectOutputStream = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(mapping); + objectOutputStream.flush(); + + byte[] data = byteArrayOutputStream.toByteArray(); + String list = Base64.encodeToString(data, Base64.DEFAULT); + + FairyGlobal.getHostApplication() + .getSharedPreferences("plugins.serviceMapping", Context.MODE_PRIVATE) + .edit().putString("plugins.serviceMapping.map", list).commit(); + + return true; + } catch (Exception e) { + LogUtil.printException("StubServiceMappingProcessor.save", e); + } finally { + if (objectOutputStream != null) { + try { + objectOutputStream.close(); + } catch (IOException e) { + LogUtil.printException("StubServiceMappingProcessor.save", e); + } + } + if (byteArrayOutputStream != null) { + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + LogUtil.printException("StubServiceMappingProcessor.save", e); + } + } + } + return false; + } + + private static HashMap restore() { + String list = FairyGlobal.getHostApplication() + .getSharedPreferences("plugins.serviceMapping", Context.MODE_PRIVATE) + .getString("plugins.serviceMapping.map", ""); + Serializable object = null; + if (!TextUtils.isEmpty(list)) { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + Base64.decode(list, Base64.DEFAULT)); + ObjectInputStream objectInputStream = null; + try { + objectInputStream = new ObjectInputStream(byteArrayInputStream); + object = (Serializable) objectInputStream.readObject(); + } catch (Exception e) { + LogUtil.printException("StubServiceMappingProcessor.restore", e); + } finally { + if (objectInputStream != null) { + try { + objectInputStream.close(); + } catch (IOException e) { + LogUtil.printException("StubServiceMappingProcessor.restore", e); + } + } + if (byteArrayInputStream != null) { + try { + byteArrayInputStream.close(); + } catch (IOException e) { + LogUtil.printException("StubServiceMappingProcessor.restore", e); + } + } + } + } + if (object != null) { + + HashMap mapping = (HashMap) object; + return mapping; + } + return null; + } + + private static void initStubPool() { + + if (isPoolInited) { + return; + } + + loadStubService(); + + isPoolInited = true; + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FakeUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FakeUtil.java new file mode 100644 index 00000000..3c64de4b --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FakeUtil.java @@ -0,0 +1,665 @@ +package com.limpoxe.fairy.util; + +import android.app.Activity; +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.ChangedPackages; +import android.content.pm.FeatureInfo; +import android.content.pm.InstrumentationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.VersionedPackage; +import android.content.res.Resources; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.core.RealPluginClassLoader; +import com.limpoxe.fairy.core.android.HackActivity; + +import java.lang.reflect.Field; +import java.util.List; + +public class FakeUtil { + + /** + * 由于插件的getPackageName返回的是插件包名 + * 实际应用中一些第三方库可能需要使用宿主包名, 此时可以通过此方法 + * 对插件的Context的包名进行修正 + * @param context + * @return + */ + public static Context fakeContext(Context context) { + if (!context.getPackageName().equals(FairyGlobal.getHostApplication().getPackageName())) { + context = new ContextWrapper(context) { + @Override + public String getPackageName() { + return FairyGlobal.getHostApplication().getPackageName(); + } + }; + } + return context; + } + + /** + * 需要fakeContext并不是因为插件取不到自己的meta-data, + * 而是因为针对需要appkey的sdk插件,需要同时正确的取到下面3个值, + * 1、packageName + * 2、meta-data + * 3、signatures + * + * 默认情况是: + * 1、当packageName是宿主的时候,meta-data和signatures都会取到宿主的值 + * 2、当packageName是插件的时候,meta-data和signatures都会取到插件的值 + * + * 然而针对需要appkey的sdk插件来说,一般情况是这样: + * 1、packageName需要是宿主,因为appkey是注册的宿主的appkey + * 2、meta-data需要是插件,因为meta-data是放在插件的manifeat文件中 + * 3、signatures需要是宿主,因为appkey是注册的宿主的signatures + * 因此需要fackeContext来实现这种组合 + * + * 但是,如果注册appkey的时候,就直接使用了插件的packageName、meta-data、signatures + * 则无需使用fakeContext,直接使用插件的Application即可。 + * + */ + public static Context fakeApplication(final Application application) { + Application fakeForSdk = new Application() { + + @Override + public String getPackageName() { + Context context = getBaseContext(); + while (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + //返回宿主packageName + return context.getPackageName(); + } + + @Override + public PackageManager getPackageManager() { + final PackageManager pm = super.getPackageManager(); + + return new PackageManager() { + + @Override + public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException { + PackageInfo packageInfo = pm.getPackageInfo(packageName, flags); + if ((flags & PackageManager.GET_SIGNATURES) != 0) { + //返回宿主签名 + packageInfo.signatures = pm.getPackageInfo(getPackageName(), flags).signatures; + } + return packageInfo; + } + + //Android-O + public PackageInfo getPackageInfo(VersionedPackage versionedPackage, int i) throws NameNotFoundException { + return null; + } + + @Override + public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException { + ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, flags); + if ((flags & PackageManager.GET_META_DATA) != 0 ) { + Context context = getBaseContext(); + while (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + //返回宿主meta + applicationInfo.metaData = pm.getApplicationInfo(getPackageName(), flags).metaData; + } + return applicationInfo; + } + + @Override + public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException { + return pm.getActivityInfo(component, flags); + } + + @Override + public ActivityInfo getReceiverInfo(ComponentName component, int flags) throws NameNotFoundException { + return pm.getReceiverInfo(component, flags); + } + + @Override + public ServiceInfo getServiceInfo(ComponentName component, int flags) throws NameNotFoundException { + return pm.getServiceInfo(component, flags); + } + + @Override + public ProviderInfo getProviderInfo(ComponentName component, int flags) throws NameNotFoundException { + return pm.getProviderInfo(component, flags); + } + + @Override + public List getInstalledPackages(int flags) { + return pm.getInstalledPackages(flags); + } + + @Override + public int checkSignatures(String pkg1, String pkg2) { + return pm.checkSignatures(pkg1, pkg2); + } + + @Override + public int checkSignatures(int uid1, int uid2) { + return pm.checkSignatures(uid1, uid2); + } + + @Override + public List queryBroadcastReceivers(Intent intent, int flags) { + return pm.queryBroadcastReceivers(intent, flags); + } + + //methods belows is not need, remain empty impl + @Override + public String[] currentToCanonicalPackageNames(String[] names) { + return new String[0]; + } + + @Override + public String[] canonicalToCurrentPackageNames(String[] names) { + return new String[0]; + } + + @Override + public Intent getLaunchIntentForPackage(String packageName) { + return null; + } + + @Override + public Intent getLeanbackLaunchIntentForPackage(String packageName) { + return null; + } + + @Override + public int[] getPackageGids(String packageName) throws NameNotFoundException { + return new int[0]; + } + + //@Override //android-N + public int[] getPackageGids(String packageName, int flags) throws NameNotFoundException { + return new int[0]; + } + + @Override + public PermissionInfo getPermissionInfo(String name, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List queryPermissionsByGroup(String group, int flags) throws NameNotFoundException { + return null; + } + + @Override + public PermissionGroupInfo getPermissionGroupInfo(String name, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List getAllPermissionGroups(int flags) { + return null; + } + + @Override + public List getPackagesHoldingPermissions(String[] permissions, int flags) { + return null; + } + + @Override + public int checkPermission(String permName, String pkgName) { + return PackageManager.PERMISSION_GRANTED; + } + + @Override + public boolean isPermissionRevokedByPolicy(String permName, String pkgName) { + return false; + } + + @Override + public boolean addPermission(PermissionInfo info) { + return false; + } + + @Override + public boolean addPermissionAsync(PermissionInfo info) { + return false; + } + + @Override + public void removePermission(String name) { + + } + + @Override + public String[] getPackagesForUid(int uid) { + return new String[0]; + } + + @Override + public String getNameForUid(int uid) { + return null; + } + + @Override + public List getInstalledApplications(int flags) { + return null; + } + + //Android-O + public boolean isInstantApp() { + return false; + } + + //Android-O + public boolean isInstantApp(String s) { + return false; + } + + //Android-O + public int getInstantAppCookieMaxBytes() { + return 0; + } + + //Android-O + public byte[] getInstantAppCookie() { + return new byte[0]; + } + + //Android-O + public void clearInstantAppCookie() { + + } + + //Android-O + public void updateInstantAppCookie(byte[] bytes) { + + } + + @Override + public String[] getSystemSharedLibraryNames() { + return new String[0]; + } + + //Android-O + public List getSharedLibraries(int i) { + return null; + } + + //Android-O + public ChangedPackages getChangedPackages(int i) { + return null; + } + + @Override + public FeatureInfo[] getSystemAvailableFeatures() { + return new FeatureInfo[0]; + } + + @Override + public boolean hasSystemFeature(String name) { + return false; + } + + @Override + public ResolveInfo resolveActivity(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentActivities(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentActivityOptions(ComponentName caller, Intent[] specifics, Intent intent, int flags) { + return null; + } + + @Override + public ResolveInfo resolveService(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentServices(Intent intent, int flags) { + return null; + } + + @Override + public List queryIntentContentProviders(Intent intent, int flags) { + return null; + } + + @Override + public ProviderInfo resolveContentProvider(String name, int flags) { + return null; + } + + @Override + public List queryContentProviders(String processName, int uid, int flags) { + return null; + } + + @Override + public InstrumentationInfo getInstrumentationInfo(ComponentName className, int flags) throws NameNotFoundException { + return null; + } + + @Override + public List queryInstrumentation(String targetPackage, int flags) { + return null; + } + + @Override + public Drawable getDrawable(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public Drawable getActivityIcon(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityIcon(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityBanner(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityBanner(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getDefaultActivityIcon() { + return null; + } + + @Override + public Drawable getApplicationIcon(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationIcon(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getApplicationBanner(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationBanner(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityLogo(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getActivityLogo(Intent intent) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getApplicationLogo(ApplicationInfo info) { + return null; + } + + @Override + public Drawable getApplicationLogo(String packageName) throws NameNotFoundException { + return null; + } + + @Override + public Drawable getUserBadgedIcon(Drawable icon, UserHandle user) { + return null; + } + + @Override + public Drawable getUserBadgedDrawableForDensity(Drawable drawable, UserHandle user, Rect badgeLocation, int badgeDensity) { + return null; + } + + @Override + public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) { + return null; + } + + @Override + public CharSequence getText(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public android.content.res.XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) { + return null; + } + + @Override + public CharSequence getApplicationLabel(ApplicationInfo info) { + return null; + } + + @Override + public Resources getResourcesForActivity(ComponentName activityName) throws NameNotFoundException { + return null; + } + + @Override + public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException { + return null; + } + + @Override + public Resources getResourcesForApplication(String appPackageName) throws NameNotFoundException { + return null; + } + + @Override + public void verifyPendingInstall(int id, int verificationCode) { + + } + + @Override + public void extendVerificationTimeout(int id, int verificationCodeAtTimeout, long millisecondsToDelay) { + + } + + @Override + public void setInstallerPackageName(String targetPackage, String installerPackageName) { + + } + + @Override + public String getInstallerPackageName(String packageName) { + return null; + } + + @Override + public void addPackageToPreferred(String packageName) { + + } + + @Override + public void removePackageFromPreferred(String packageName) { + + } + + @Override + public List getPreferredPackages(int flags) { + return null; + } + + @Override + public void addPreferredActivity(IntentFilter filter, int match, ComponentName[] set, ComponentName activity) { + + } + + @Override + public void clearPackagePreferredActivities(String packageName) { + + } + + @Override + public int getPreferredActivities(List outFilters, List outActivities, String packageName) { + return 0; + } + + @Override + public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) { + + } + + @Override + public int getComponentEnabledSetting(ComponentName componentName) { + return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + } + + @Override + public void setApplicationEnabledSetting(String packageName, int newState, int flags) { + + } + + @Override + public int getApplicationEnabledSetting(String packageName) { + return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + } + + @Override + public boolean isSafeMode() { + return false; + } + + //Android-O + public void setApplicationCategoryHint(String s, int i) { + + } + + @Override + public PackageInstaller getPackageInstaller() { + return null; + } + + //Android-O + public boolean canRequestPackageInstalls() { + return false; + } + + //@Override //android-N + public boolean hasSystemFeature(String name,int flag) { + return false; + } + + //@Override //android-N + public int getPackageUid(String name,int flag) { + return 0; + } + }; + } + }; + try { + Field base = ContextWrapper.class.getDeclaredField("mBase"); + base.setAccessible(true); + Context c = (Context)base.get(application); + base.set(fakeForSdk, c); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + LogUtil.printException("FakeUtil.fakeApplication", e); + } catch (IllegalAccessException e) { + e.printStackTrace(); + LogUtil.printException("FakeUtil.fakeApplication", e); + } + + return fakeForSdk; + } + + /** + * @param pluginContext 参数为插件的context,例如插件activity或者插件Application + * @return + */ + public static String getHostPackageName(ContextWrapper pluginContext) { + Context context = pluginContext; + while (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + //到这里context的实际类型应当是ContextImpl类,可以返回宿主packageName + return context.getPackageName(); + } + + public static Context fakeWindowContext(final Activity pluginActivity) { + return new ContextWrapper(FairyGlobal.getHostApplication()) { + @Override + public Object getSystemService(String name) { + if (WINDOW_SERVICE.equals(name)) { + return pluginActivity.getSystemService(name); + } + return super.getSystemService(name); + } + }; + } + + public static Activity fakeActivityForUMengSdk(Activity activity) { + //getHostApplication(); + //getApplicationContext(); + //getPackageName(); + //getLocalClassName(); + final String className = activity.getClass().getSimpleName(); + Activity fakeActivity = new Activity() { + @Override + public Context getApplicationContext() { + return FairyGlobal.getHostApplication().getApplicationContext(); + } + + @Override + public String getPackageName() { + return FairyGlobal.getHostApplication().getPackageName(); + } + + public String getLocalClassName() { + return className; + } + }; + new HackActivity(fakeActivity).setApplication(FairyGlobal.getHostApplication()); + return fakeActivity; + } + + public static Context fakeMultiDexContext(Application application) { + return new ContextWrapper(application) { + @Override + public ClassLoader getClassLoader() { + ClassLoader classLoader = super.getClassLoader(); + if (!(classLoader instanceof RealPluginClassLoader)) { + if(classLoader.getParent() instanceof RealPluginClassLoader) { + classLoader = classLoader.getParent(); + } + } + return classLoader; + } + }; + } +} diff --git a/PluginCore/src/com/plugin/util/FileUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FileUtil.java similarity index 57% rename from PluginCore/src/com/plugin/util/FileUtil.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/util/FileUtil.java index 816e4f7c..43e42d37 100644 --- a/PluginCore/src/com/plugin/util/FileUtil.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FileUtil.java @@ -1,9 +1,13 @@ -package com.plugin.util; +package com.limpoxe.fairy.util; +import android.Manifest; +import android.content.pm.PackageManager; import android.os.Build; +import android.os.Environment; +import android.os.Looper; import android.widget.Toast; -import com.plugin.core.PluginLoader; +import com.limpoxe.fairy.core.FairyGlobal; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -15,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; @@ -31,19 +36,30 @@ public static boolean copyFile(String source, String dest) { try { return copyFile(new FileInputStream(new File(source)), dest); } catch (FileNotFoundException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.copyFile", e); } return false; } public static boolean copyFile(final InputStream inputStream, String dest) { - LogUtil.d("copyFile to " + dest); + LogUtil.v("copyFile to " + dest); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (dest.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { + int permissionState = FairyGlobal.getHostApplication().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (permissionState != PackageManager.PERMISSION_GRANTED) { + //6.0的系统即使申请了读写sdcard的权限,仍然可以在设置中关闭, 则需要requestPermissons + LogUtil.e("6.0以上的系统, targetSDK>=23时, sdcard读写默认为未授权,需requestPermissons或者在设置中开启", dest); + return false; + } + } + } FileOutputStream oputStream = null; try { File destFile = new File(dest); - destFile.getParentFile().mkdirs(); - destFile.createNewFile(); - + File parentDir = destFile.getParentFile(); + if (!parentDir.isDirectory() || !parentDir.exists()) { + destFile.getParentFile().mkdirs(); + } oputStream = new FileOutputStream(destFile); byte[] bb = new byte[48 * 1024]; int len = 0; @@ -53,78 +69,84 @@ public static boolean copyFile(final InputStream inputStream, String dest) { oputStream.flush(); return true; } catch (Exception e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.copyFile", e); } finally { if (oputStream != null) { try { oputStream.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.copyFile", e); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.copyFile", e); } } } return false; } - public static boolean copySo(File sourceDir, String so, String dest) { + public static boolean copySo2(File sourceDir, String soName, String dest, ArrayList supportedAbis) { + boolean isSuccess = false; try { - - boolean isSuccess = false; - - if (Build.VERSION.SDK_INT >= 21) { - String[] abis = Build.SUPPORTED_ABIS; - if (abis != null) { - for (String abi: abis) { - LogUtil.d("try supported abi:", abi); - String name = "lib" + File.separator + abi + File.separator + so; - File sourceFile = new File(sourceDir, name); - if (sourceFile.exists()) { - isSuccess = copyFile(sourceFile.getAbsolutePath(), dest + File.separator + "lib" + File.separator + so); - //api21 64位系统的目录可能有些不同 - //copyFile(sourceFile.getAbsolutePath(), dest + File.separator + name); - break; - } + for(String abi : supportedAbis) { + if (abi != null) { + LogUtil.d("try supported abi:", abi); + File sourceSoFile = new File(sourceDir, "lib" + File.separator + abi + File.separator + soName); + if (sourceSoFile.exists()) { + LogUtil.d("安装 " + sourceSoFile + " 到 " + dest + File.separator + soName); + isSuccess = copyFile(sourceSoFile.getAbsolutePath(), dest + File.separator + soName); + break; } } - } else { - LogUtil.d("supported api:", Build.CPU_ABI, Build.CPU_ABI2); + } - String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so; - File sourceFile = new File(sourceDir, name); + if (!isSuccess) { + LogUtil.e("安装 " + soName + " 失败: NO_MATCHING_ABIS"); + if (DEBUG && Thread.currentThread() == Looper.getMainLooper().getThread()) { + Toast.makeText(FairyGlobal.getHostApplication(), "安装 " + soName + " 失败: NO_MATCHING_ABIS", Toast.LENGTH_LONG).show(); + } + } + } catch(Exception e) { + LogUtil.printException("FileUtil.copySo", e); + } - if (!sourceFile.exists() && Build.CPU_ABI2 != null) { - name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so; - sourceFile = new File(sourceDir, name); + return isSuccess; + } - if (!sourceFile.exists()) { - name = "lib" + File.separator + "armeabi" + File.separator + so; - sourceFile = new File(sourceDir, name); + @Deprecated + public static boolean copySo(File sourceDir, String soName, String dest, ArrayList supportedAbis) { + boolean isSuccess = false; + + try { + for(String abi : supportedAbis) { + if (abi != null) { + LogUtil.d("try supported abi:", abi); + File sourceSoFile = new File(sourceDir, "lib" + File.separator + abi + File.separator + soName); + if (sourceSoFile.exists()) { + isSuccess = copyFile(sourceSoFile.getAbsolutePath(), dest + File.separator + "lib" + File.separator + soName); + break; } } - if (sourceFile.exists()) { - isSuccess = copyFile(sourceFile.getAbsolutePath(), dest + File.separator + "lib" + File.separator + so); - } } if (!isSuccess) { - Toast.makeText(PluginLoader.getApplication(), "安装 " + so + " 失败: NO_MATCHING_ABIS", Toast.LENGTH_LONG).show(); + LogUtil.e("安装 " + soName + " 失败: NO_MATCHING_ABIS"); + if (DEBUG && Thread.currentThread() == Looper.getMainLooper().getThread()) { + Toast.makeText(FairyGlobal.getHostApplication(), "安装 " + soName + " 失败: NO_MATCHING_ABIS", Toast.LENGTH_LONG).show(); + } } } catch(Exception e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.copySo", e); } - return true; + return isSuccess; } - public static Set unZipSo(String apkFile, File tempDir) { HashSet result = null; @@ -133,7 +155,7 @@ public static Set unZipSo(String apkFile, File tempDir) { tempDir.mkdirs(); } - LogUtil.d("开始so文件", tempDir.getAbsolutePath()); + LogUtil.v("开始解压so", tempDir.getAbsolutePath()); ZipFile zfile = null; boolean isSuccess = false; @@ -194,27 +216,27 @@ public static Set unZipSo(String apkFile, File tempDir) { } isSuccess = true; } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.unZipSo", e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.unZipSo", e); } } if (bis != null) { try { bis.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.unZipSo", e); } } if (zfile != null) { try { zfile.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.unZipSo", e); } } } @@ -236,13 +258,13 @@ public static void readFileFromJar(String jarFilePath, String metaInfo) { } } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.readFileFromJar", e); } finally { if (jarFile != null) { try { jarFile.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("FileUtil.readFileFromJar", e); } } } @@ -263,19 +285,15 @@ public static boolean deleteAll(File file) { } } } - LogUtil.d("delete", file.getAbsolutePath()); return file.delete(); } public static void printAll(File file) { - if (DEBUG) { - LogUtil.d("printAll", file.getAbsolutePath()); - if (file.isDirectory()) { - File[] childFiles = file.listFiles(); - if (childFiles != null && childFiles.length > 0) { - for (int i = 0; i < childFiles.length; i++) { - printAll(childFiles[i]); - } + if (file.isDirectory()) { + File[] childFiles = file.listFiles(); + if (childFiles != null && childFiles.length > 0) { + for (int i = 0; i < childFiles.length; i++) { + printAll(childFiles[i]); } } } @@ -296,4 +314,8 @@ public static String streamToString(InputStream input) throws IOException { return sb.toString(); } + public static boolean checkPathSafe(String path) { + return path != null && !path.contains("..") && !path.contains(" "); + } + } diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FreeReflection.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FreeReflection.java new file mode 100644 index 00000000..d8eb0eeb --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/FreeReflection.java @@ -0,0 +1,124 @@ +package com.limpoxe.fairy.util; + +import static android.os.Build.VERSION.SDK_INT; + +import android.content.Context; +import android.os.Build; +import android.util.Base64; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.Method; + +import dalvik.system.DexFile; + +/** + * Copy From FreeReflection + * https://github.com/tiann/FreeReflection + */ +public class FreeReflection { + private static final String TAG = "FreeReflection"; + + private static Object sVmRuntime; + private static Method setHiddenApiExemptions; + + private static final String DEX = "ZGV4CjAzNQCl4EprGS2pXI/v3OwlBrlfRnX5rmkKVdN0CwAAcAAAAHhWNBIAAAAAAAAAAMgKAABEAAAAcAAAABMAAACAAQAACwAAAMwBAAAMAAAAUAIAAA8AAACwAgAAAwAAACgDAADsBwAAiAMAABYGAAAYBgAAHQYAACcGAAAvBgAAPwYAAEsGAABbBgAAcAYAAIIGAACJBgAAkQYAAJQGAACYBgAAnAYAAKIGAAClBgAAqgYAAMUGAADrBgAABwcAABsHAAAuBwAARAcAAFgHAABsBwAAgAcAAJcHAACzBwAA2wcAAAIIAAAlCAAAMQgAAEIIAABLCAAAUAgAAFMIAABhCAAAbwgAAHMIAAB2CAAAeggAAI4IAACjCAAAuAgAAMEIAADaCAAA3QgAAOUIAADwCAAA+QgAAAoJAAAeCQAAMQkAAD0JAABFCQAAUgkAAGwJAAB0CQAAfQkAAJgJAAChCQAArQkAAMUJAADXCQAA3QkAAOUJAADzCQAACwAAABEAAAASAAAAEwAAABQAAAAVAAAAFwAAABgAAAAZAAAAGgAAABsAAAAcAAAAHQAAAB4AAAAjAAAAJwAAACkAAAAqAAAAKwAAAAwAAAAAAAAA3AUAAA0AAAAAAAAA5AUAAA4AAAAAAAAA7AUAAA8AAAACAAAAAAAAABAAAAAGAAAA+AUAABAAAAAKAAAAAAYAACMAAAAOAAAAAAAAACYAAAAOAAAACAYAACcAAAAPAAAAAAAAACgAAAAPAAAACAYAACgAAAAPAAAAEAYAAAIAAAA/AAAAAwAAACEAAAALAAcABAAAAAsABwAFAAAACwAPAAkAAAALAAcACgAAAAsAAAAkAAAACwAHACUAAAAMAAcAIgAAAAwABgA9AAAADAAKAD4AAAANAAcAIgAAAAEAAwAzAAAABAACAC4AAAAFAAUANAAAAAYABgADAAAACAAHADcAAAAKAAQANgAAAAsABgADAAAADAAGAAIAAAAMAAYAAwAAAAwACQAvAAAADAAKAC8AAAAMAAgAMAAAAA0ABgADAAAADQABAEEAAAANAAAAQgAAAAsAAAARAAAABgAAAAAAAAAIAAAAAAAAAHgKAABmCgAADAAAABEAAAAGAAAAAAAAAAcAAAAAAAAAjgoAAHIKAAANAAAAAQAAAAYAAAAAAAAAIAAAAAAAAACxCgAAdQoAAAEAAQABAAAAAwoAAAQAAABwEAMAAAAOAAoAAAADAAEACAoAAHsAAABgBQEAEwYcADRlbQAcBQUAGgYxABIXI3cQABIIHAkHAE0JBwhuMAIAZQcMARwFBQAaBjQAEicjdxAAEggcCQcATQkHCBIYHAkQAE0JBwhuMAIAZQcMAhIFEhYjZhEAEgcaCC0ATQgGB24wBQBRBgwEHwQFABIlI1URABIGGgc1AE0HBQYSFhIHTQcFBm4wBQBCBQwDHwMKABIlI1URABIGGgc+AE0HBQYSFhIXI3cQABIIHAkSAE0JBwhNBwUGbjAFAEIFDAUfBQoAaQUKABIFEgYjZhEAbjAFAFMGDAVpBQkADgANABoFBgAaBjsAcTABAGUAKPcAAAYAAABrAAEAAQEJcgEAAQABAAAANwoAAAQAAABwEAMAAAAOAAMAAQABAAAAPAoAAAsAAAASECMAEgASAU0CAAFxEAoAAAAKAA8AAAAIAAEAAwABAEIKAAAdAAAAEhESAmIDCQA4AwYAYgMKADkDBAABIQ8BYgMKAGIECQASFSNVEQASBk0HBQZuMAUAQwUo8g0AASEo7wAADAAAAA0AAQABAQkaAwAAAAEAAABSCgAADQAAABIQIwASABIBGgIPAE0CAAFxEAoAAAAKAA8AAAABAAEAAQAAAFcKAAAEAAAAcBADAAAADgAEAAEAAQAAAFwKAAAeAAAAEgBgAQEAEwIcADUhAwAPAHEACwAAAAoBOQH7/xoAMgBxEAQAAABuEAAAAwAMAFIAAABxEA4AAAAKACjqAQAAAAAAAAABAAAAAQAAAAMAAAAHAAcACQAAAAIAAAAGABEAAgAAAAcAEAABAAAABwAAAAEAAAASAAAAAzEuMAAIPGNsaW5pdD4ABjxpbml0PgAOQVBQTElDQVRJT05fSUQACkJVSUxEX1RZUEUADkJvb3RzdHJhcENsYXNzABNCb290c3RyYXBDbGFzcy5qYXZhABBCdWlsZENvbmZpZy5qYXZhAAVERUJVRwAGRkxBVk9SAAFJAAJJSQACSUwABElMTEwAAUwAA0xMTAAZTGFuZHJvaWQvY29udGVudC9Db250ZXh0OwAkTGFuZHJvaWQvY29udGVudC9wbS9BcHBsaWNhdGlvbkluZm87ABpMYW5kcm9pZC9vcy9CdWlsZCRWRVJTSU9OOwASTGFuZHJvaWQvdXRpbC9Mb2c7ABFMamF2YS9sYW5nL0NsYXNzOwAUTGphdmEvbGFuZy9DbGFzczwqPjsAEkxqYXZhL2xhbmcvT2JqZWN0OwASTGphdmEvbGFuZy9TdHJpbmc7ABJMamF2YS9sYW5nL1N5c3RlbTsAFUxqYXZhL2xhbmcvVGhyb3dhYmxlOwAaTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsAJkxtZS93ZWlzaHUvZnJlZXJlZmxlY3Rpb24vQnVpbGRDb25maWc7ACVMbWUvd2Vpc2h1L3JlZmxlY3Rpb24vQm9vdHN0cmFwQ2xhc3M7ACFMbWUvd2Vpc2h1L3JlZmxlY3Rpb24vUmVmbGVjdGlvbjsAClJlZmxlY3Rpb24AD1JlZmxlY3Rpb24uamF2YQAHU0RLX0lOVAADVEFHAAFWAAxWRVJTSU9OX0NPREUADFZFUlNJT05fTkFNRQACVkwAAVoAAlpMABJbTGphdmEvbGFuZy9DbGFzczsAE1tMamF2YS9sYW5nL09iamVjdDsAE1tMamF2YS9sYW5nL1N0cmluZzsAB2NvbnRleHQAF2RhbHZpay5zeXN0ZW0uVk1SdW50aW1lAAFlAAZleGVtcHQACWV4ZW1wdEFsbAAHZm9yTmFtZQAPZnJlZS1yZWZsZWN0aW9uABJnZXRBcHBsaWNhdGlvbkluZm8AEWdldERlY2xhcmVkTWV0aG9kAApnZXRSdW50aW1lAAZpbnZva2UAC2xvYWRMaWJyYXJ5ABhtZS53ZWlzaHUuZnJlZXJlZmxlY3Rpb24ABm1ldGhvZAAHbWV0aG9kcwAZcmVmbGVjdCBib290c3RyYXAgZmFpbGVkOgAHcmVsZWFzZQAKc1ZtUnVudGltZQAWc2V0SGlkZGVuQXBpRXhlbXB0aW9ucwAQdGFyZ2V0U2RrVmVyc2lvbgAEdGhpcwAGdW5zZWFsAAx1bnNlYWxOYXRpdmUADnZtUnVudGltZUNsYXNzAAYABw4AFgAHDmr/AwEyCwEVEAMCNQvwBAREBhcBEg8DAzYLARsPqQUCBQMFBBkeAwAvCgAOAAcOACwBOgcOADYBOwcsnRriAQEDAC8KHgBIAAcOAA0ABw4AEwEtBx1yGWtaAAYXOBc8HxcABAEXAQEXBgEXHwYAAQACGQEZARkBGQEZARkGgYAEiAcDAAUACBoBCgEKB4iABKAHAYGABLQJAQnMCQGJAfQJAQnMCgEAAwALGgyBgAT4CgEJkAsBigIAAAAADgAAAAAAAAABAAAAAAAAAAEAAABEAAAAcAAAAAIAAAATAAAAgAEAAAMAAAALAAAAzAEAAAQAAAAMAAAAUAIAAAUAAAAPAAAAsAIAAAYAAAADAAAAKAMAAAEgAAAIAAAAiAMAAAEQAAAHAAAA3AUAAAIgAABEAAAAFgYAAAMgAAAIAAAAAwoAAAUgAAADAAAAZgoAAAAgAAADAAAAeAoAAAAQAAABAAAAyAoAAA=="; + + static { + if (SDK_INT >= Build.VERSION_CODES.P) { + try { + Method forName = Class.class.getDeclaredMethod("forName", String.class); + Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class); + + Class vmRuntimeClass = (Class) forName.invoke(null, "dalvik.system.VMRuntime"); + Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null); + setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class}); + sVmRuntime = getRuntime.invoke(null); + } catch (Throwable e) { + Log.w(TAG, "reflect bootstrap failed:", e); + } + } + } + + /** + * make the method exempted from hidden API check. + * + * @param method the method signature prefix. + * @return true if success. + */ + public static boolean exempt(String method) { + return exempt(new String[]{method}); + } + + /** + * make specific methods exempted from hidden API check. + * + * @param methods the method signature prefix, such as "Ldalvik/system", "Landroid" or even "L" + * @return true if success + */ + public static boolean exempt(String... methods) { + if (sVmRuntime == null || setHiddenApiExemptions == null) { + return false; + } + + try { + setHiddenApiExemptions.invoke(sVmRuntime, new Object[]{methods}); + return true; + } catch (Throwable e) { + return false; + } + } + + /** + * Make all hidden API exempted. + * + * @return true if success. + */ + public static boolean unseal() { + return exempt(new String[]{"L"}); + } + + public static boolean exemptAll(Context context) { + if (SDK_INT < 28) { + // Below Android P, ignore + return true; + } + + // try exempt API first. + if (unseal()) { + return true; + } + if (unsealByDexFile(context)) { + return true; + } + + return false; + } + + private static boolean unsealByDexFile(Context context) { + byte[] bytes = Base64.decode(DEX, Base64.NO_WRAP); + try { + File codeCacheDir = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + codeCacheDir = context.getCodeCacheDir(); + } else { + return false; + } + File code = new File(codeCacheDir, System.currentTimeMillis() + ".dex"); + + FileOutputStream fos = new FileOutputStream(code); + fos.write(bytes); + fos.flush(); + fos.close(); + + DexFile dexFile = new DexFile(code); + Class bootstrapClass = dexFile.loadClass("me.weishu.reflection.BootstrapClass", null); + Method exemptAll = bootstrapClass.getDeclaredMethod("exemptAll"); + return (boolean) exemptAll.invoke(null); + } catch (Throwable e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/LogUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/LogUtil.java new file mode 100644 index 00000000..407db3a9 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/LogUtil.java @@ -0,0 +1,106 @@ +package com.limpoxe.fairy.util; + +import android.util.Log; + +public class LogUtil { + + private static boolean isEnable = false; + + private static final int stackLevel = 4; + + public static void v(Object... msg) { + printLog(Log.VERBOSE, msg); + } + + public static void i(Object... msg) { + printLog(Log.INFO, msg); + } + + public static void d(Object... msg) { + printLog(Log.DEBUG, msg); + } + + public static void w(Object... msg) { + printLog(Log.WARN, msg); + } + + public static void e(Object... msg) { + printLog(Log.ERROR, msg); + } + + private static void printLog(int level, Object... msg) { + if (isEnable) { + StringBuilder str = new StringBuilder(); + if (msg != null) { + for (Object obj : msg) { + str.append("||").append(obj); + } + if (str.length() > 1) { + str.delete(0, 2);//删除开头的分隔符 + } + } else { + str.append("null"); + } + try { + StackTraceElement[] sts = Thread.currentThread().getStackTrace(); + StackTraceElement st = null; + String tag = null; + if (sts != null && sts.length > stackLevel) { + st = sts[stackLevel]; + if (st != null) { + String fileName = st.getFileName(); + tag = (fileName == null) ? "Unkown" : fileName.replace(".java", ""); + str.insert(0, "[" + tag + "." + st.getMethodName() + "() line " + st.getLineNumber() + "]\n>>>[") + .append("]"); + } + } + while (str.length() > 0) { + DEFAULT_LOGHANDLER.publish("Fairy", level, str.substring(0, Math.min(2000, str.length())).toString()); + str.delete(0, 2000); + } + } catch (Exception exception) { + exception.printStackTrace(); + } + } + } + + public static void printStackTrace() { + if (isEnable) { + try { + StackTraceElement[] sts = Thread.currentThread().getStackTrace(); + for (StackTraceElement stackTraceElement : sts) { + DEFAULT_LOGHANDLER.publish("Log_StackTrace", Log.ERROR, stackTraceElement.toString()); + } + } catch (Exception exception) { + exception.printStackTrace(); + } + } + } + + public static void printException(String msg, Throwable e) { + if (isEnable) { + DEFAULT_LOGHANDLER.publish("Log_StackTrace", Log.ERROR, msg + '\n' + Log.getStackTraceString(e)); + } + } + + public static interface LogHandler { + + void publish(String tag, int level, String message); + + } + + public static LogHandler DEFAULT_LOGHANDLER = new LogHandler() { + @Override + public void publish(String tag, int level, String message) { + Log.println(level, tag, message); + } + }; + + public static void setEnable(boolean isLogEnable) { + isEnable = isLogEnable; + } + + public static void setLogHandler(LogHandler logHandler) { + DEFAULT_LOGHANDLER = logHandler; + } +} diff --git a/PluginCore/src/com/plugin/util/ManifestReader.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ManifestReader.java similarity index 99% rename from PluginCore/src/com/plugin/util/ManifestReader.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/util/ManifestReader.java index 70103819..3d15d59e 100644 --- a/PluginCore/src/com/plugin/util/ManifestReader.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ManifestReader.java @@ -1,4 +1,9 @@ -package com.plugin.util; +package com.limpoxe.fairy.util; + +import android.util.TypedValue; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import java.io.EOFException; import java.io.File; @@ -8,11 +13,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import android.util.TypedValue; - /** * Read xml document from Android's binary xml file. */ @@ -32,13 +32,13 @@ public static String getManifestXMLFromAPK(String apkPath) { ZipEntry entry = file.getEntry(DEFAULT_XML); rs = getManifestXMLFromAPK(file, entry); } catch (Exception e) { - e.printStackTrace(); + LogUtil.printException("ManifestReader.getManifestXMLFromAPK", e); } finally { if (file != null) { try { file.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtil.printException("ManifestReader.getManifestXMLFromAPK", e); } } } @@ -94,7 +94,7 @@ public static String getManifestXMLFromAPK(ZipFile file, ZipEntry entry) { } } } catch (Exception e) { - e.printStackTrace(); + LogUtil.printException("ManifestReader.getManifestXMLFromAPK", e); } finally { parser.close(); } diff --git a/PluginCore/src/com/plugin/util/PackageVerifyer.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/PackageVerifyer.java similarity index 74% rename from PluginCore/src/com/plugin/util/PackageVerifyer.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/util/PackageVerifyer.java index bea11ff3..0ce15e4e 100644 --- a/PluginCore/src/com/plugin/util/PackageVerifyer.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/PackageVerifyer.java @@ -1,7 +1,12 @@ -package com.plugin.util; +package com.limpoxe.fairy.util; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.util.Log; +import android.os.Bundle; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; import java.io.BufferedInputStream; import java.io.IOException; @@ -209,4 +214,39 @@ public static boolean isSignaturesSame(Signature[] s1, Signature[] s2) { } return false; } + + public static boolean isCompatibleWithHost(PluginDescriptor pluginDescriptor) { + // 检查当前宿主版本是否匹配此非独立插件需要的版本 + PackageManager packageManager = FairyGlobal.getHostApplication().getPackageManager(); + String requireHostVerName = pluginDescriptor.getRequiredHostVersionName(); + if (FairyGlobal.isNeedVerifyHostVersionName() && !pluginDescriptor.isStandalone() && requireHostVerName != null) { + //是非独立插件,而且指定了插件运行需要的的宿主版本 + try { + PackageInfo hostPackageInfo = packageManager.getPackageInfo(FairyGlobal.getHostApplication().getPackageName(), PackageManager.GET_META_DATA); + //判断宿主版本是否满足要求 + LogUtil.v(pluginDescriptor.getPackageName(), requireHostVerName, hostPackageInfo.versionName); + if (!requireHostVerName.equals(hostPackageInfo.versionName)) { + Bundle metaData = hostPackageInfo.applicationInfo.metaData; + if (metaData != null) { + String compatibleWithHostVersion = metaData.getString("fairy_compatibleWithHostVersion"); + if (compatibleWithHostVersion != null && !compatibleWithHostVersion.trim().equals("fairy_compatibleWithHostVersion_NOT_SET")) { + LogUtil.e("compatibleWithHostVersion=" + compatibleWithHostVersion); + String[] compatibleVersions = compatibleWithHostVersion.trim().split(","); + for(String compatibleVer : compatibleVersions) { + if(requireHostVerName.equals(compatibleVer.trim())) { + LogUtil.e("插件版本命中了宿主配置的兼容版本号!表明当前宿主版本支持此插件版本"); + return true; + } + } + } + } + LogUtil.e("当前宿主版本不支持此插件版本", "宿主versionName:" + hostPackageInfo.versionName, "插件RequiredHostVersionName:" + pluginDescriptor.getRequiredHostVersionName()); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("PluginManagerService.installPlugin", e); + } + } + return true; + } } diff --git a/PluginCore/src/com/plugin/util/NotificationHelper.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/PendingIntentHelper.java similarity index 59% rename from PluginCore/src/com/plugin/util/NotificationHelper.java rename to FairyPlugin/src/main/java/com/limpoxe/fairy/util/PendingIntentHelper.java index 6d02371b..6950d393 100644 --- a/PluginCore/src/com/plugin/util/NotificationHelper.java +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/PendingIntentHelper.java @@ -1,28 +1,20 @@ -package com.plugin.util; +package com.limpoxe.fairy.util; -import android.app.PendingIntent; import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.os.Build; -import android.widget.RemoteViews; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginIntentResolver; -import com.plugin.core.PluginLoader; - -import java.io.File; -import java.util.ArrayList; +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.PluginIntentResolver; /** * Created by cailiming on 16/1/10. */ -public class NotificationHelper { +public class PendingIntentHelper { /** * used before send notification * @param intent * @return */ - public static Intent resolveNotificationIntent(Intent intent, int type) { + public static Intent resolvePendingIntent(Intent intent, int type) { if (type == PluginDescriptor.BROADCAST) { diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ProcessUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ProcessUtil.java new file mode 100644 index 00000000..1fd20700 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ProcessUtil.java @@ -0,0 +1,78 @@ +package com.limpoxe.fairy.util; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.os.Build; +import android.text.TextUtils; + +import com.limpoxe.fairy.core.FairyGlobal; +import com.limpoxe.fairy.manager.PluginManagerProvider; + +import java.util.List; + +public class ProcessUtil { + + private static Boolean isPluginProcess; + + /** + * 因为判断进程的需要依赖activityManager.getRunningAppProcesses方法 + * 而此方法又会被框架hook,所以这个方法必需在框架hookgetRunningAppProcesses前先hook一次,并将结果缓存到成员变量中 + * 这样才能不受hook影响 + * @param context + * @return + */ + public static boolean isPluginProcess(Context context) { + + if (isPluginProcess == null) { + String processName = getCurProcessName(context); + String pluginProcessName = getPluginProcessName(context); + + if (TextUtils.isEmpty(processName) || TextUtils.isEmpty(pluginProcessName)) { + LogUtil.e("a fatal error happened, should throw an exception here?", "processName:" + processName + ", pluginProcessName:" + pluginProcessName); + } + + isPluginProcess = processName.equals(pluginProcessName); + } + return isPluginProcess; + } + + /** + * 这个方法能正确判断当前插件是否为插件进程,是因为在hook插件的进程的判断方法前(getRunningProcess),已经判断并 + * 缓存了当前进程是否为插件进程的结果 + * @return + */ + public static boolean isPluginProcess() { + return isPluginProcess(FairyGlobal.getHostApplication()); + } + + private static String getCurProcessName(Context context) { + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List list = activityManager.getRunningAppProcesses(); + if (list != null) { + for (ActivityManager.RunningAppProcessInfo appProcess : list) { + if (appProcess != null && appProcess.pid == android.os.Process.myPid()) { + return appProcess.processName; + } + } + } + return ""; + } + + private static String getPluginProcessName(Context context) { + try { + if (Build.VERSION.SDK_INT >= 9) { + //这里取个巧, 直接查询ContentProvider的信息中包含的processName + //因为Contentprovider是被配置在插件进程的. + //但是这个api只支持9及以上, + ProviderInfo pinfo = context.getPackageManager().getProviderInfo(new ComponentName(context, PluginManagerProvider.class), 0); + return pinfo==null?"":pinfo.processName; + } + } catch (PackageManager.NameNotFoundException e) { + LogUtil.printException("ProcessUtil.getPluginProcessName", e); + } + return ""; + } +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/RefInvoker.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/RefInvoker.java new file mode 100644 index 00000000..bdee3731 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/RefInvoker.java @@ -0,0 +1,351 @@ +package com.limpoxe.fairy.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unchecked") +public class RefInvoker { + + private static final ClassLoader system = ClassLoader.getSystemClassLoader(); + private static final ClassLoader bootloader = system.getParent(); + private static final ClassLoader application = RefInvoker.class.getClassLoader(); + + private static HashMap clazzCache = new HashMap(); + + public static Class forName(String clazzName) throws ClassNotFoundException { + Class clazz = clazzCache.get(clazzName); + if (clazz == null) { + clazz = Class.forName(clazzName); + ClassLoader cl = clazz.getClassLoader(); + if (cl == system || cl == application || cl == bootloader) { + clazzCache.put(clazzName, clazz); + } + } + return clazz; + } + + public static Object newInstance(String className, Class[] paramTypes, Object[] paramValues) { + try { + Class clazz = forName(className); + Constructor constructor = clazz.getConstructor(paramTypes); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + return constructor.newInstance(paramValues); + } catch (ClassNotFoundException e) { + LogUtil.printException("ClassNotFoundException", e); + } catch (NoSuchMethodException e) { + LogUtil.printException("NoSuchMethodException", e); + } catch (IllegalAccessException e) { + LogUtil.printException("IllegalAccessException", e); + } catch (InstantiationException e) { + LogUtil.printException("InstantiationException", e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if(cause instanceof RuntimeException) { + throw (RuntimeException)cause; + } else if(cause instanceof Error) { + throw (Error)cause; + } else { + throw new RuntimeException("fail to newInstance " + className); + } + } + return null; + } + + public static Object invokeMethod(Object target, String className, String methodName, Class[] paramTypes, + Object[] paramValues) { + + try { + Class clazz = forName(className); + return invokeMethod(target, clazz, methodName, paramTypes, paramValues); + }catch (ClassNotFoundException e) { + LogUtil.printException("ClassNotFoundException", e); + } + return null; + } + + public static Object invokeMethod(Object target, Class clazz, String methodName, Class[] paramTypes, + Object[] paramValues) { + try { + Method method = clazz.getDeclaredMethod(methodName, paramTypes); + if (!method.isAccessible()) { + method.setAccessible(true); + } + return method.invoke(target, paramValues); + } catch (SecurityException e) { + LogUtil.printException("SecurityException", e); + } catch (IllegalArgumentException e) { + LogUtil.printException("IllegalArgumentException", e); + } catch (IllegalAccessException e) { + LogUtil.printException("IllegalAccessException", e); + } catch (NoSuchMethodException e) { + //这个日志... + LogUtil.e("NoSuchMethodException", methodName); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if(cause instanceof RuntimeException) { + throw (RuntimeException)cause; + } else if(cause instanceof Error) { + throw (Error)cause; + } else { + throw new RuntimeException("fail to calling method " + methodName); + } + } + return null; + } + + @SuppressWarnings("rawtypes") + public static Object getField(Object target, String className, String fieldName) { + try { + Class clazz = forName(className); + return getField(target, clazz, fieldName); + } catch (ClassNotFoundException e) { + LogUtil.printException("ClassNotFoundException", e); + } + return null; + } + + @SuppressWarnings("rawtypes") + public static Object getField(Object target, Class clazz, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + if (!field.isAccessible()) { + field.setAccessible(true); + } + return field.get(target); + } catch (SecurityException e) { + LogUtil.printException("RefInvoker.getField", e); + } catch (NoSuchFieldException e) { + // try supper for Miui, Miui has a class named MiuiPhoneWindow + try { + Field field = clazz.getSuperclass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(target); + } catch (Exception superE) { + //LogUtil.printException("RefInvoker.getField", e); + //LogUtil.printException("RefInvoker.getField", superE); + } + } catch (IllegalArgumentException e) { + LogUtil.printException("RefInvoker.getField", e); + } catch (IllegalAccessException e) { + LogUtil.printException("RefInvoker.getField", e); + } + return null; + + } + + @SuppressWarnings("rawtypes") + public static void setField(Object target, String className, String fieldName, Object fieldValue) { + try { + Class clazz = forName(className); + setField(target, clazz, fieldName, fieldValue); + } catch (ClassNotFoundException e) { + LogUtil.printException("RefInvoker.setField", e); + } + } + + public static void setField(Object target, Class clazz, String fieldName, Object fieldValue) { + try { + Field field = clazz.getDeclaredField(fieldName); + if (!field.isAccessible()) { + field.setAccessible(true); + } + field.set(target, fieldValue); + } catch (SecurityException e) { + LogUtil.printException("RefInvoker.setField", e); + } catch (NoSuchFieldException e) { + // try supper for Miui, Miui has a class named MiuiPhoneWindow + try { + Field field = clazz.getSuperclass().getDeclaredField(fieldName); + if (!field.isAccessible()) { + field.setAccessible(true); + } + field.set(target, fieldValue); + } catch (Exception superE) { + LogUtil.printException("RefInvoker.setField", e); + //superE.printStackTrace(); + } + } catch (IllegalArgumentException e) { + LogUtil.printException("RefInvoker.setField", e); + } catch (IllegalAccessException e) { + LogUtil.printException("RefInvoker.setField", e); + } + } + + public static Method findMethod(Object object, String methodName, Class[] paramClasses) { + try { + return object.getClass().getDeclaredMethod(methodName, paramClasses); + } catch (NoSuchMethodException e) { + LogUtil.printException("RefInvoker.findMethod", e); + } + return null; + } + + public static Method findMethod(Object object, String methodName, Object[] params) { + if (params == null) { + try { + return object.getClass().getDeclaredMethod(methodName, (Class[])null); + } catch (NoSuchMethodException e) { + LogUtil.printException("RefInvoker.findMethod", e); + } + return null; + } else { + Method[] methods = object.getClass().getDeclaredMethods(); + boolean isFound = false; + Method method = null; + for(Method m: methods) { + if (m.getName().equals(methodName)) { + Class[] types = m.getParameterTypes(); + if (types.length == params.length) { + isFound = true; + for(int i = 0; i < params.length; i++) { + if (!(types[i] == params[i].getClass() || (types[i].isPrimitive() && primitiveToWrapper(types[i]) == params[i].getClass()))) { + isFound = false; + break; + } + } + if (isFound) { + method = m; + break; + } + } + } + } + return method; + } + } + + private static final Map, Class> primitiveWrapperMap = new HashMap, Class>(); + + static { + primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); + primitiveWrapperMap.put(Byte.TYPE, Byte.class); + primitiveWrapperMap.put(Character.TYPE, Character.class); + primitiveWrapperMap.put(Short.TYPE, Short.class); + primitiveWrapperMap.put(Integer.TYPE, Integer.class); + primitiveWrapperMap.put(Long.TYPE, Long.class); + primitiveWrapperMap.put(Double.TYPE, Double.class); + primitiveWrapperMap.put(Float.TYPE, Float.class); + primitiveWrapperMap.put(Void.TYPE, Void.TYPE); + } + + static Class primitiveToWrapper(final Class cls) { + Class convertedClass = cls; + if (cls != null && cls.isPrimitive()) { + convertedClass = primitiveWrapperMap.get(cls); + } + return convertedClass; + } + + public static ArrayList dumpAllInfo(String className) { + try { + Class clazz = Class.forName(className); + return dumpAllInfo(clazz); + } catch (ClassNotFoundException e) { + LogUtil.printException("RefInvoker.dumpAllInfo", e); + } + return null; + } + + public static ArrayList dumpAllInfo(Class clazz) { + ArrayList arrayList = new ArrayList(); + + LogUtil.i("clazz=" + clazz.getName()); + LogUtil.i("Superclass=" + clazz.getSuperclass()); + + Constructor[] ctors = clazz.getDeclaredConstructors(); + if (ctors != null) { + LogUtil.w("DeclaredConstructors--------------------" + ctors.length); + for(Constructor c : ctors){ + LogUtil.i(c); + arrayList.add(c); + } + } + + Constructor[] publicCtors = clazz.getConstructors(); + if (publicCtors != null) { + LogUtil.w("Constructors-------------------------" + publicCtors.length); + for(Constructor c :publicCtors){ + LogUtil.i(c); + arrayList.add(c); + } + } + + Method[] mtds = clazz.getDeclaredMethods(); + if (mtds != null) { + LogUtil.w("DeclaredMethods-------------------------" + mtds.length); + for(Method m : mtds){ + LogUtil.i(m); + arrayList.add(m); + } + } + + Method[] mts = clazz.getMethods(); + if (mts != null) { + LogUtil.w("Methods-------------------------" + mts.length); + for(Method m : mts){ + LogUtil.i(m); + arrayList.add(m); + } + } + + Class[] inners = clazz.getDeclaredClasses(); + if (inners != null) { + LogUtil.w("DeclaredClasses-------------------------" + inners.length); + for(Class c : inners){ + LogUtil.i(c.getName()); + arrayList.add(c.getName()); + } + } + + Class[] classes = clazz.getClasses(); + if (classes != null) { + LogUtil.w("classes-------------------------" + classes.length); + for(Class c : classes){ + LogUtil.i(c.getName()); + arrayList.add(c.getName()); + } + } + + Field[] dfields = clazz.getDeclaredFields(); + if (dfields != null) { + LogUtil.w("DeclaredFields-------------------------" + dfields.length); + for(Field f : dfields){ + LogUtil.i(f); + arrayList.add(f); + } + } + + Field[] fields = clazz.getFields(); + if (fields != null) { + LogUtil.w("Fields-------------------------" + fields.length); + for(Field f : fields){ + LogUtil.i(f); + arrayList.add(f); + } + } + + Annotation[] anns = clazz.getAnnotations(); + if (anns != null) { + LogUtil.w("Annotations-------------------------" + anns.length); + for(Annotation an : anns){ + LogUtil.i(an); + arrayList.add(an); + } + } + return arrayList; + } + + public static ArrayList dumpAllInfo(Object object) { + Class clazz = object.getClass(); + return dumpAllInfo(clazz); + } + +} diff --git a/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ResourceUtil.java b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ResourceUtil.java new file mode 100644 index 00000000..b96c99d4 --- /dev/null +++ b/FairyPlugin/src/main/java/com/limpoxe/fairy/util/ResourceUtil.java @@ -0,0 +1,225 @@ +package com.limpoxe.fairy.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import com.limpoxe.fairy.content.PluginDescriptor; +import com.limpoxe.fairy.core.FairyGlobal; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Created by cailiming + */ +public class ResourceUtil { + + public static String getString(String value, Context pluginContext) { + String idHex = null; + if (value != null && value.startsWith("@") && value.length() == 9) { + idHex = value.replace("@", ""); + + } else if (value != null && value.startsWith("@android:") && value.length() == 17) { + idHex = value.replace("@android:", ""); + } + + if (idHex != null) { + try { + int id = (int)Long.parseLong(idHex, 16); + //此时context可能还没有初始化 + if (pluginContext != null) { + String des = pluginContext.getString(id); + return des; + } + } catch (Exception e) { + LogUtil.printException("ResourceUtil.getString", e); + } + } + + return value; + } + + public static Boolean getBoolean(String value, Context pluginContext) { + String idHex = null; + if (value != null && value.startsWith("@") && value.length() == 9) { + idHex = value.replace("@", ""); + + } else if (value != null && value.startsWith("@android:") && value.length() == 17) { + idHex = value.replace("@android:", ""); + } + + if (idHex != null) { + try { + int id = (int)Long.parseLong(idHex, 16); + //此时context可能还没有初始化 + if (pluginContext != null) { + return pluginContext.getResources().getBoolean(id); + } + } catch (Exception e) { + LogUtil.printException("ResourceUtil.getBoolean", e); + } + } else if (value != null) { + return Boolean.parseBoolean(value); + } + + return null; + } + + /** + * use parseResId() instead + */ + @Deprecated + public static int getResourceId(String value) { + return parseResId(value); + } + + public static int parseResId(String value) { + String idHex = null; + if (value != null && value.contains(":")) { + idHex = value.split(":")[1]; + } else if (value != null && value.startsWith("@") && value.length() == 9) { + idHex = value.replace("@", ""); + } + if (idHex != null) { + try { + int id = (int)Long.parseLong(idHex, 16); + return id; + } catch (Exception e) { + LogUtil.printException("ResourceUtil.parseResId", e); + } + } + return 0; + } + + public static String getLabel(PluginDescriptor pluginDescriptor) { + PackageInfo info = pluginDescriptor.getPackageInfo(PackageManager.GET_ACTIVITIES); + if (info != null) { + String label = null; + try { + if (pluginDescriptor.isStandalone() || !isMainResId(info.applicationInfo.labelRes)){ + label = FairyGlobal.getHostApplication().getPackageManager().getApplicationLabel(info.applicationInfo).toString(); + } + } catch (Resources.NotFoundException e) { + } + if (label == null || label.equals(pluginDescriptor.getPackageName())) { + //可能设置的lable是来自宿主的资源 + if (pluginDescriptor.getDescription() != null) { + int id = ResourceUtil.parseResId(pluginDescriptor.getDescription()); + if (id != 0) { + //再宿主中查一次 + try { + label = FairyGlobal.getHostApplication().getResources().getString(id); + } catch (Resources.NotFoundException e) { + } + } + } + } + if (label != null) { + return label; + } + } + return pluginDescriptor.getDescription(); + } + + public static Drawable getLogo(PluginDescriptor pluginDescriptor) { + if (Build.VERSION.SDK_INT >= 9) { + PackageInfo info = pluginDescriptor.getPackageInfo(PackageManager.GET_ACTIVITIES); + if (info != null) { + Drawable logo = FairyGlobal.getHostApplication().getPackageManager().getApplicationLogo(info.applicationInfo); + return logo; + } + } + return null; + } + + public static Drawable getIcon(PluginDescriptor pluginDescriptor) { + if (Build.VERSION.SDK_INT >= 9) { + PackageManager pm = FairyGlobal.getHostApplication().getPackageManager(); + PackageInfo info = pluginDescriptor.getPackageInfo(PackageManager.GET_ACTIVITIES); + if (info != null) { + Drawable logo = pm.getApplicationIcon(info.applicationInfo); + return logo; + } + } + return null; + } + + public static boolean isMainResId(int resid) { + int packageId = resid >> 24; + if (packageId != 0x7f) {//加这个判断是为了支持通过修改aapt的方式进行资源分组 + return false; + } + + //这里之所以这样判断是因为 宿主的public.xml中限制了宿主的资源id范围 + //如果public.xml配置在插件中, 这里需要将这个判断反过来 + return resid>>16 > 0x7f2F || resid>>16 == 0x7f01; + } + + public static void rewriteRValues(ClassLoader cl, String packageName, int id) { + final Class rClazz; + try { + rClazz = cl.loadClass(packageName + ".R"); + } catch (ClassNotFoundException e) { + LogUtil.d("No resource references to update in package " + packageName); + return; + } + + final Method callback; + try { + callback = rClazz.getMethod("onResourcesLoaded", int.class); + } catch (NoSuchMethodException e) { + // No rewriting to be done. + return; + } + + Throwable cause; + try { + callback.invoke(null, id); + return; + } catch (IllegalAccessException e) { + cause = e; + } catch (InvocationTargetException e) { + cause = e.getCause(); + } + + throw new RuntimeException("Failed to rewrite resource references for " + packageName, + cause); + } + + public static String covent2Hex(String resId) { + if (resId == null) { + return null; + } + if (resId.startsWith("@")) { + if (resId.contains(":")) { + String[] idStr = resId.split(":"); + //size一定等于2,不等于2的情况我也管不着啦 + idStr[1] = Long.toHexString(Long.parseLong(idStr[1]) & 0xFFFFFFFFL); + return idStr[0] + ":" + lengthAlign(idStr[1]); + } else { + String[] idStr = resId.split("@"); + //size一定等于2,不等于2的情况我也管不着啦 + idStr[1] = Long.toHexString(Long.parseLong(idStr[1]) & 0xFFFFFFFFL); + return "@" + lengthAlign(idStr[1]); + } + } else { + return resId; + } + } + + private static String lengthAlign(String idStr) { + if (idStr.length() < 8) {//PPTTNNNN + int pad = 8 - idStr.length(); + String fill = ""; + for(int i = 0; i < pad; i++) { + fill = fill + "0"; + } + idStr = fill + idStr; + } + return idStr; + } +} diff --git a/PluginBase/build.gradle b/PluginBase/build.gradle deleted file mode 100644 index b9236044..00000000 --- a/PluginBase/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 14 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } -} - -build.doLast { - - exportJar.execute() - - //将编译好的插件apk复制到Main工程的assets目录下, 便于测试 - copy { - println 'copy plugin apk to assets... ' + buildDir.absolutePath + '/outputs/apk/PluginBase-debug.apk' - from(buildDir.absolutePath + '/outputs/apk/') { - include 'PluginBase-debug.apk' - } - into(project(':PluginMain').getProjectDir().absolutePath + '/assets/') - } -} - -task exportJar(type: Jar) { - from buildDir.absolutePath + '/intermediates/classes/debug/' - destinationDir = file(buildDir.absolutePath + '/outputs/') -} \ No newline at end of file diff --git a/PluginBase/src/main/AndroidManifest.xml b/PluginBase/src/main/AndroidManifest.xml deleted file mode 100644 index b4b1e397..00000000 --- a/PluginBase/src/main/AndroidManifest.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/PluginCore/.gitignore b/PluginCore/.gitignore deleted file mode 100644 index 7792e06b..00000000 --- a/PluginCore/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -gen/ diff --git a/PluginCore/AndroidManifest.xml b/PluginCore/AndroidManifest.xml deleted file mode 100644 index b5aa2d2d..00000000 --- a/PluginCore/AndroidManifest.xml +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PluginCore/README b/PluginCore/README deleted file mode 100644 index e69de29b..00000000 diff --git a/PluginCore/build.gradle b/PluginCore/build.gradle deleted file mode 100644 index 2c135155..00000000 --- a/PluginCore/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 8 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } - - lintOptions { - checkReleaseBuilds false - // Or, if you prefer, you can continue to check for errors in release builds, - // but continue the build even when errors are found: - abortOnError false - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - //java.excludes = [''] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - renderscript.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] - } - - // Move the tests to tests/java, tests/res, etc... - instrumentTest.setRoot('tests') - - // Move the build types to build-types/ - // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... - // This moves them out of them default location under src//... which would - // conflict with src/ being used by the main source set. - // Adding new build types or product flavors should be accompanied - // by a similar customization. - debug.setRoot('build-types/debug') - release.setRoot('build-types/release') - } - - buildTypes { - release { - minifyEnabled false - proguardFiles 'proguard-rules.pro' - } - } -} - -build.doLast { - - exportJar.execute() - -} - -task exportJar(type: Jar) { - from buildDir.absolutePath + '/intermediates/classes/debug/' - destinationDir = file(buildDir.absolutePath + '/outputs/') -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/content/LoadedPlugin.java b/PluginCore/src/com/plugin/content/LoadedPlugin.java deleted file mode 100644 index 9be8f348..00000000 --- a/PluginCore/src/com/plugin/content/LoadedPlugin.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.plugin.content; - -import android.app.Application; -import android.content.Context; -import android.content.res.Resources; - -import dalvik.system.DexClassLoader; - -/** - * Created by cailiming on 16/3/9. - * - */ -public class LoadedPlugin { - - public final DexClassLoader pluginClassLoader; - public final Context pluginContext; - public final Resources pluginResource; - - public final String pluginPackageName; - public final String pluginSourceDir; - - public LoadedPlugin(String packageName, - String pluginSourceDir, - Context pluginContext, - DexClassLoader pluginClassLoader) { - this.pluginPackageName = packageName; - this.pluginSourceDir = pluginSourceDir; - this.pluginContext = pluginContext; - this.pluginClassLoader = pluginClassLoader; - this.pluginResource = pluginContext.getResources(); - } - - public Application pluginApplication; -} diff --git a/PluginCore/src/com/plugin/content/PluginDescriptor.java b/PluginCore/src/com/plugin/content/PluginDescriptor.java deleted file mode 100644 index 4ec4c653..00000000 --- a/PluginCore/src/com/plugin/content/PluginDescriptor.java +++ /dev/null @@ -1,413 +0,0 @@ -package com.plugin.content; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import com.plugin.util.LogUtil; -import com.plugin.util.ResourceUtil; - -import android.app.Application; -import android.content.Intent; -import android.os.Bundle; - - -/** - *
- * @author cailiming
- * 
- */ -public class PluginDescriptor implements Serializable { - - private static final long serialVersionUID = -7545734825911798344L; - - public static final int UNKOWN = 0; - public static final int BROADCAST = 1; - public static final int ACTIVITY = 2; - public static final int SERVICE = 4; - public static final int PROVIDER = 6; - public static final int FRAGMENT = 8; - public static final int FUNCTION = 9; - - private String packageName; - - private String version; - - private String description; - - private boolean isStandalone; - - private boolean isEnabled; - - private String applicationName; - - private int applicationIcon; - - private int applicationLogo; - - private int applicationTheme; - - /** - * 定义在插件Manifest中的meta-data标签 - */ - private transient Bundle metaData; - - private HashMap providerInfos = new HashMap(); - - /** - * key: fragment id, - * value: fragment class - */ - private HashMap fragments = new HashMap(); - - /** - * key: localservice id, - * value: localservice class - */ - private HashMap functions = new HashMap(); - - /** - * key: activity class name - * value: intentfilter list - */ - private HashMap> activitys = new HashMap>(); - - /** - * key: activity class name - * value: activity info in Manifest - */ - private HashMap activityInfos = new HashMap(); - - /** - * key: service class name - * value: intentfilter list - */ - private HashMap> services = new HashMap>(); - - /** - * key: receiver class name - * value: intentfilter list - */ - private HashMap> receivers = new HashMap>(); - - private String installedPath; - - private String[] dependencies; - - private ArrayList muliDexList; - - //=============getter and setter====================== - - public String getPackageName() { - return packageName; - } - - public void setPackageName(String packageName) { - this.packageName = packageName; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public int getApplicationIcon() { - return applicationIcon; - } - - public void setApplicationIcon(int icon) { - this.applicationIcon = icon; - } - - public int getApplicationLogo() { - return applicationLogo; - } - - public void setApplicationLogo(int logo) { - this.applicationLogo = logo; - } - - public int getApplicationTheme() { - return applicationTheme; - } - - public void setApplicationTheme(int theme) { - this.applicationTheme = theme; - } - - public Bundle getMetaData() { - if (metaData == null) { - if (installedPath != null) { - metaData = ResourceUtil.getApplicationMetaData(installedPath); - if (metaData == null) { - metaData = new Bundle(); - } - } - } - return metaData; - } - - public HashMap getFragments() { - return fragments; - } - - public void setfragments(HashMap fragments) { - this.fragments = fragments; - } - - public HashMap getFunctions() { - return functions; - } - - public void setFunctions(HashMap functions) { - this.functions = functions; - } - - public HashMap> getReceivers() { - return receivers; - } - - public void setReceivers(HashMap> receivers) { - this.receivers = receivers; - } - - public HashMap> getActivitys() { - return activitys; - } - - public void setActivitys(HashMap> activitys) { - this.activitys = activitys; - } - - public HashMap getActivityInfos() { - return activityInfos; - } - - public void setActivityInfos(HashMap activityInfos) { - this.activityInfos = activityInfos; - } - - public HashMap> getServices() { - return services; - } - - public void setServices(HashMap> services) { - this.services = services; - } - - public String getInstalledPath() { - return installedPath; - } - - public void setInstalledPath(String installedPath) { - this.installedPath = installedPath; - } - - public String[] getDependencies() { - return dependencies; - } - - public void setDependencies(String[] dependencies) { - this.dependencies = dependencies; - } - - public List getMuliDexList() { - return muliDexList; - } - - public void setMuliDexList(ArrayList muliDexList) { - this.muliDexList = muliDexList; - } - - public String getApplicationName() { - return applicationName; - } - - public void setApplicationName(String applicationName) { - this.applicationName = applicationName; - } - - public boolean isEnabled() { - return isEnabled; - } - - public void setEnabled(boolean isEnabled) { - this.isEnabled = isEnabled; - } - - public boolean isStandalone() { - return isStandalone; - } - - public void setStandalone(boolean isStandalone) { - this.isStandalone = isStandalone; - } - - public HashMap getProviderInfos() { - return providerInfos; - } - - public void setProviderInfos(HashMap providerInfos) { - this.providerInfos = providerInfos; - } - - /** - * 需要根据id查询的只有fragment - * @param clazzId - * @return - */ - public String getPluginClassNameById(String clazzId) { - String clazzName = getFragments().get(clazzId); - - if (clazzName == null) { - LogUtil.d("PluginDescriptor", "clazzName not found for classId ", clazzId); - } else { - LogUtil.d("PluginDescriptor", "clazzName found ", clazzName); - } - return clazzName; - } - - /** - * 需要根据Id查询的只有fragment - * @param clazzId - * @return - */ - public boolean containsFragment(String clazzId) { - if (getFragments().containsKey(clazzId) && isEnabled()) { - return true; - } - return false; - } - - /** - * 根据className查询 - * @param clazzName - * @return - */ - public boolean containsName(String clazzName) { - if (getFragments().containsValue(clazzName) && isEnabled()) { - return true; - } else if (getActivitys().containsKey(clazzName) && isEnabled()) { - return true; - } else if (getReceivers().containsKey(clazzName) && isEnabled()) { - return true; - } else if (getServices().containsKey(clazzName) && isEnabled()) { - return true; - } else if (getProviderInfos().containsKey(clazzName) && isEnabled()) { - return true; - } else if (getApplicationName().equals(clazzName) && !clazzName.equals(Application.class.getName()) && isEnabled()) { - return true; - } - return false; - } - - /** - * 获取class的类型: activity - * @return - */ - public int getType(String clazzName) { - if (getFragments().containsValue(clazzName) && isEnabled()) { - return FRAGMENT; - } else if (getActivitys().containsKey(clazzName) && isEnabled()) { - return ACTIVITY; - } else if (getReceivers().containsKey(clazzName) && isEnabled()) { - return BROADCAST; - } else if (getServices().containsKey(clazzName) && isEnabled()) { - return SERVICE; - } else if (getProviderInfos().containsKey(clazzName) && isEnabled()) { - return PROVIDER; - } - return UNKOWN; - } - - public List matchPlugin(Intent intent, int type) { - PluginDescriptor plugin = this; - List result = null; - String clazzName = null; - // 如果是通过组件进行匹配的, 这里忽略了packageName - if (intent.getComponent() != null) { - if (plugin.containsName(intent.getComponent().getClassName())) { - clazzName = intent.getComponent().getClassName(); - result = new ArrayList(1); - result.add(clazzName); - return result;//暂时不考虑不同的插件中配置了相同名称的组件的问题,先到先得 - } - } else { - // 如果是通过IntentFilter进行匹配的 - if (type == PluginDescriptor.ACTIVITY) { - - ArrayList list = findClassNameByIntent(intent, plugin.getActivitys()); - - if (list != null && list.size() >0) { - result = new ArrayList(1); - result.add(list.get(0)); - return result;//暂时不考虑多个Activity配置了相同的Intent的问题,先到先得 - } - - } else if (type == PluginDescriptor.SERVICE) { - - ArrayList list = findClassNameByIntent(intent, plugin.getServices()); - - if (list != null && list.size() >0) { - result = new ArrayList(1); - result.add(list.get(0)); - return result;//service本身不支持多匹配,,先到先得 - } - - } else if (type == PluginDescriptor.BROADCAST) { - - ArrayList list = findClassNameByIntent(intent, plugin.getReceivers()); - if (list != null && list.size() >0) { - result = new ArrayList(); - result.addAll(list);//暂时不考虑去重的问题 - return result; - } - } - } - return null; - } - - private static ArrayList findClassNameByIntent(Intent intent, HashMap> intentFilter) { - if (intentFilter != null) { - ArrayList targetClassNameList = null; - - Iterator>> entry = intentFilter.entrySet().iterator(); - while (entry.hasNext()) { - Map.Entry> item = entry.next(); - Iterator values = item.getValue().iterator(); - while (values.hasNext()) { - PluginIntentFilter filter = values.next(); - int result = filter.match(intent.getAction(), intent.getType(), intent.getScheme(), - intent.getData(), intent.getCategories()); - - if (result != PluginIntentFilter.NO_MATCH_ACTION - && result != PluginIntentFilter.NO_MATCH_CATEGORY - && result != PluginIntentFilter.NO_MATCH_DATA - && result != PluginIntentFilter.NO_MATCH_TYPE) { - if (targetClassNameList == null) { - targetClassNameList = new ArrayList(); - } - targetClassNameList.add(item.getKey()); - break; - } - } - } - return targetClassNameList; - } - return null; - } -} diff --git a/PluginCore/src/com/plugin/core/HostClassLoader.java b/PluginCore/src/com/plugin/core/HostClassLoader.java deleted file mode 100644 index 502edb72..00000000 --- a/PluginCore/src/com/plugin/core/HostClassLoader.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.plugin.core; - -import com.plugin.content.PluginProviderInfo; -import com.plugin.util.LogUtil; - -import dalvik.system.DexClassLoader; - -/** - * 为了支持Receiver和ContentProvider,增加此类。 - * - * @author Administrator - * - */ -public class HostClassLoader extends DexClassLoader { - - public HostClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { - super(dexPath, optimizedDirectory, libraryPath, parent); - } - - @Override - public String findLibrary(String name) { - LogUtil.d("findLibrary", name); - return super.findLibrary(name); - } - - @Override - protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { - - //Just for Receiver and service - if (className.startsWith(PluginIntentResolver.CLASS_PREFIX)) { - String realName = className.replace(PluginIntentResolver.CLASS_PREFIX, ""); - LogUtil.d("className ", className, "target", realName); - Class clazz = PluginLoader.loadPluginClassByName(realName); - if (clazz != null) { - return clazz; - } - } else if (className.startsWith(PluginProviderInfo.CLASS_PREFIX)) { - //Just for contentprovider - String realName = className.replace(PluginProviderInfo.CLASS_PREFIX, ""); - LogUtil.d("className ", className, "target", realName); - Class clazz = PluginLoader.loadPluginClassByName(realName); - if (clazz != null) { - return clazz; - } - } - - return super.loadClass(className, resolve); - } - -} diff --git a/PluginCore/src/com/plugin/core/PluginAppTrace.java b/PluginCore/src/com/plugin/core/PluginAppTrace.java deleted file mode 100644 index 5eb4eadc..00000000 --- a/PluginCore/src/com/plugin/core/PluginAppTrace.java +++ /dev/null @@ -1,351 +0,0 @@ -package com.plugin.core; - -import android.app.Service; -import android.content.Context; -import android.content.pm.ServiceInfo; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; - -import com.plugin.core.app.ActivityThread; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; -import com.plugin.util.RefInvoker; - -import java.util.Map; - -/** - * 插件Receiver免注册的主要实现原理 - * - * @author cailiming - * - */ -public class PluginAppTrace implements Handler.Callback { - - private final Handler mHandler; - - public PluginAppTrace(Handler handler) { - mHandler = handler; - } - - @Override - public boolean handleMessage(Message msg) { - - LogUtil.d(">>> handling: ", CodeConst.codeToString(msg.what)); - - Result result = beforeHandle(msg); - - try { - - mHandler.handleMessage(msg); - - LogUtil.d(">>> done: " + CodeConst.codeToString(msg.what)); - - } finally { - - afterHandle(msg, result); - - } - - return true; - } - - private Result beforeHandle(Message msg) { - - switch (msg.what) { - - case CodeConst.LAUNCH_ACTIVITY: - case CodeConst.RELAUNCH_ACTIVITY: - - beforeLaunchActivityFor360Safe(); - - return null; - - case CodeConst.RECEIVER: - - return beforeReceiver(msg); - - case CodeConst.CREATE_SERVICE: - - return beforeCreateService(msg); - - case CodeConst.STOP_SERVICE: - - return beforeStopService(msg); - } - return null; - } - - private static void beforeLaunchActivityFor360Safe() { - // 检查mInstrumention是否已经替换成功。 - // 之所以要检查,是因为如果手机上安装了360手机卫士等app,它们可能会劫持用户app的ActivityThread对象, - // 导致在PluginApplication的onCreate方法里面替换mInstrumention可能会失败 - // 所以这里再做一次检查 - PluginInjector.injectInstrumentation(); - } - - private static Result beforeReceiver(Message msg) { - if (ProcessUtil.isPluginProcess()) { - Class clazz = PluginIntentResolver.resolveReceiverForClassLoader(msg.obj); - - if (clazz != null) { - PluginInjector.hackHostClassLoaderIfNeeded(); - - Context baseContext = PluginLoader.getApplication().getBaseContext(); - Context newBase = PluginLoader.getDefaultPluginContext(clazz); - - PluginInjector.replaceReceiverContext(baseContext, newBase); - - Result result = new Result(); - result.baseContext = baseContext; - - return result; - } else { - //宿主的receiver的context不需要处理,在framework中receiver的context本身是对appliction的包装。 - //而宿主的application的base已经本更换过了 - } - } - - return null; - } - - private static Result beforeCreateService(Message msg) { - Result result = new Result(); - if (ProcessUtil.isPluginProcess()) { - String serviceName = PluginIntentResolver.resolveServiceForClassLoader(msg.obj); - - if (serviceName.startsWith(PluginIntentResolver.CLASS_PREFIX)) { - PluginInjector.hackHostClassLoaderIfNeeded(); - } - - result.serviceName = serviceName; - } else { - ServiceInfo info = (ServiceInfo) RefInvoker.getFieldObject(msg.obj, "android.app.ActivityThread$CreateServiceData", "info"); - result.serviceName = info.name; - } - - return result; - } - - private static Result beforeStopService(Message msg) { - if (ProcessUtil.isPluginProcess()) { - //销毁service时回收映射关系 - Object activityThread = ActivityThread.currentActivityThread(); - if (activityThread != null) { - Map services = ActivityThread.getAllServices(); - if (services != null) { - Service service = services.get(msg.obj); - if (service != null) { - String pluginServiceClassName = service.getClass().getName(); - LogUtil.d("STOP_SERVICE", pluginServiceClassName); - PluginManagerHelper.unBindStubService(pluginServiceClassName); - } - } - } - } - - return null; - } - - private static void afterHandle(Message msg, Result result) { - switch (msg.what) { - case CodeConst.RECEIVER: - - afterReceiver(result); - - break; - - case CodeConst.CREATE_SERVICE: - - afterCreateService(result); - - break; - } - } - - private static void afterReceiver(Result result) { - if (ProcessUtil.isPluginProcess()) { - if (result != null && result.baseContext != null) { - RefInvoker.setFieldObject(result.baseContext, "android.app.ContextImpl", "mReceiverRestrictedContext", null); - } - } - } - - private static void afterCreateService(Result result) { - - if (result.serviceName.startsWith(PluginIntentResolver.CLASS_PREFIX)) { - //拿到创建好的service,重新 设置mBase和mApplicaiton - //由于这步操作是再service得oncreate之后执行,所以再插件service得oncreate中不应尝试通过此service的context执行操作 - PluginInjector.replacePluginServiceContext(result.serviceName.replace(PluginIntentResolver.CLASS_PREFIX, "")); - } else { - //注入一个无害的BaseContext, 主要是为了重写宿主Service的sentBroadCast和startService方法 - PluginInjector.replaceHostServiceContext(result.serviceName); - } - } - - static class Result { - String serviceName; - Context baseContext; - } - - private static class CodeConst { - public static final int LAUNCH_ACTIVITY = 100; - public static final int PAUSE_ACTIVITY = 101; - public static final int PAUSE_ACTIVITY_FINISHING = 102; - public static final int STOP_ACTIVITY_SHOW = 103; - public static final int STOP_ACTIVITY_HIDE = 104; - public static final int SHOW_WINDOW = 105; - public static final int HIDE_WINDOW = 106; - public static final int RESUME_ACTIVITY = 107; - public static final int SEND_RESULT = 108; - public static final int DESTROY_ACTIVITY = 109; - public static final int BIND_APPLICATION = 110; - public static final int EXIT_APPLICATION = 111; - public static final int NEW_INTENT = 112; - public static final int RECEIVER = 113; - public static final int CREATE_SERVICE = 114; - public static final int SERVICE_ARGS = 115; - public static final int STOP_SERVICE = 116; - public static final int REQUEST_THUMBNAIL = 117; - public static final int CONFIGURATION_CHANGED = 118; - public static final int CLEAN_UP_CONTEXT = 119; - public static final int GC_WHEN_IDLE = 120; - public static final int BIND_SERVICE = 121; - public static final int UNBIND_SERVICE = 122; - public static final int DUMP_SERVICE = 123; - public static final int LOW_MEMORY = 124; - public static final int ACTIVITY_CONFIGURATION_CHANGED = 125; - public static final int RELAUNCH_ACTIVITY = 126; - public static final int PROFILER_CONTROL = 127; - public static final int CREATE_BACKUP_AGENT = 128; - public static final int DESTROY_BACKUP_AGENT = 129; - public static final int SUICIDE = 130; - public static final int REMOVE_PROVIDER = 131; - public static final int ENABLE_JIT = 132; - public static final int DISPATCH_PACKAGE_BROADCAST = 133; - public static final int SCHEDULE_CRASH = 134; - public static final int DUMP_HEAP = 135; - public static final int DUMP_ACTIVITY = 136; - public static final int SLEEPING = 137; - public static final int SET_CORE_SETTINGS = 138; - public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139; - public static final int TRIM_MEMORY = 140; - public static final int DUMP_PROVIDER = 141; - public static final int UNSTABLE_PROVIDER_DIED = 142; - public static final int REQUEST_ASSIST_CONTEXT_EXTRAS = 143; - public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144; - public static final int INSTALL_PROVIDER = 145; - public static final int ON_NEW_ACTIVITY_OPTIONS = 146; - public static final int CANCEL_VISIBLE_BEHIND = 147; - public static final int BACKGROUND_VISIBLE_BEHIND_CHANGED = 148; - public static final int ENTER_ANIMATION_COMPLETE = 149; - - public static String codeToString(int code) { - switch (code) { - case LAUNCH_ACTIVITY: - return "LAUNCH_ACTIVITY"; - case PAUSE_ACTIVITY: - return "PAUSE_ACTIVITY"; - case PAUSE_ACTIVITY_FINISHING: - return "PAUSE_ACTIVITY_FINISHING"; - case STOP_ACTIVITY_SHOW: - return "STOP_ACTIVITY_SHOW"; - case STOP_ACTIVITY_HIDE: - return "STOP_ACTIVITY_HIDE"; - case SHOW_WINDOW: - return "SHOW_WINDOW"; - case HIDE_WINDOW: - return "HIDE_WINDOW"; - case RESUME_ACTIVITY: - return "RESUME_ACTIVITY"; - case SEND_RESULT: - return "SEND_RESULT"; - case DESTROY_ACTIVITY: - return "DESTROY_ACTIVITY"; - case BIND_APPLICATION: - return "BIND_APPLICATION"; - case EXIT_APPLICATION: - return "EXIT_APPLICATION"; - case NEW_INTENT: - return "NEW_INTENT"; - case RECEIVER: - return "RECEIVER"; - case CREATE_SERVICE: - return "CREATE_SERVICE"; - case SERVICE_ARGS: - return "SERVICE_ARGS"; - case STOP_SERVICE: - return "STOP_SERVICE"; - case REQUEST_THUMBNAIL: - return "REQUEST_THUMBNAIL"; - case CONFIGURATION_CHANGED: - return "CONFIGURATION_CHANGED"; - case CLEAN_UP_CONTEXT: - return "CLEAN_UP_CONTEXT"; - case GC_WHEN_IDLE: - return "GC_WHEN_IDLE"; - case BIND_SERVICE: - return "BIND_SERVICE"; - case UNBIND_SERVICE: - return "UNBIND_SERVICE"; - case DUMP_SERVICE: - return "DUMP_SERVICE"; - case LOW_MEMORY: - return "LOW_MEMORY"; - case ACTIVITY_CONFIGURATION_CHANGED: - return "ACTIVITY_CONFIGURATION_CHANGED"; - case RELAUNCH_ACTIVITY: - return "RELAUNCH_ACTIVITY"; - case PROFILER_CONTROL: - return "PROFILER_CONTROL"; - case CREATE_BACKUP_AGENT: - return "CREATE_BACKUP_AGENT"; - case DESTROY_BACKUP_AGENT: - return "DESTROY_BACKUP_AGENT"; - case SUICIDE: - return "SUICIDE"; - case REMOVE_PROVIDER: - return "REMOVE_PROVIDER"; - case ENABLE_JIT: - return "ENABLE_JIT"; - case DISPATCH_PACKAGE_BROADCAST: - return "DISPATCH_PACKAGE_BROADCAST"; - case SCHEDULE_CRASH: - return "SCHEDULE_CRASH"; - case DUMP_HEAP: - return "DUMP_HEAP"; - case DUMP_ACTIVITY: - return "DUMP_ACTIVITY"; - case SLEEPING: - return "SLEEPING"; - case SET_CORE_SETTINGS: - return "SET_CORE_SETTINGS"; - case UPDATE_PACKAGE_COMPATIBILITY_INFO: - return "UPDATE_PACKAGE_COMPATIBILITY_INFO"; - case TRIM_MEMORY: - return "TRIM_MEMORY"; - case DUMP_PROVIDER: - return "DUMP_PROVIDER"; - case UNSTABLE_PROVIDER_DIED: - return "UNSTABLE_PROVIDER_DIED"; - case REQUEST_ASSIST_CONTEXT_EXTRAS: - return "REQUEST_ASSIST_CONTEXT_EXTRAS"; - case TRANSLUCENT_CONVERSION_COMPLETE: - return "TRANSLUCENT_CONVERSION_COMPLETE"; - case INSTALL_PROVIDER: - return "INSTALL_PROVIDER"; - case ON_NEW_ACTIVITY_OPTIONS: - return "ON_NEW_ACTIVITY_OPTIONS"; - case CANCEL_VISIBLE_BEHIND: - return "CANCEL_VISIBLE_BEHIND"; - case BACKGROUND_VISIBLE_BEHIND_CHANGED: - return "BACKGROUND_VISIBLE_BEHIND_CHANGED"; - case ENTER_ANIMATION_COMPLETE: - return "ENTER_ANIMATION_COMPLETE"; - } - return "(unknown: " + code +")"; - } - } - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/PluginApplication.java b/PluginCore/src/com/plugin/core/PluginApplication.java deleted file mode 100644 index 00b5e9ee..00000000 --- a/PluginCore/src/com/plugin/core/PluginApplication.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.plugin.core; - -import android.app.Application; -import android.content.Context; -import android.content.ContextWrapper; - -public class PluginApplication extends Application { - - @Override - protected void attachBaseContext(Context base) { - - super.attachBaseContext(base); - - PluginLoader.initLoader(this); - } - - /** - * 重写这个方法是为了支持Receiver,否则会出现ClassCast错误 - * - * @return - */ - @Override - public Context getBaseContext() { - return PluginLoader.fixBaseContextForReceiver(super.getBaseContext()); - } -} diff --git a/PluginCore/src/com/plugin/core/PluginClassLoader.java b/PluginCore/src/com/plugin/core/PluginClassLoader.java deleted file mode 100644 index bdce1829..00000000 --- a/PluginCore/src/com/plugin/core/PluginClassLoader.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.plugin.core; - -import com.plugin.content.LoadedPlugin; -import com.plugin.util.LogUtil; - -import java.util.ArrayList; -import java.util.List; - -import dalvik.system.DexClassLoader; - -/** - * 为了支持插件间依赖,增加此类。 - * - * @author Administrator - * - */ -public class PluginClassLoader extends DexClassLoader { - - private String[] dependencies; - private List multiDexClassLoaderList; - - public PluginClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent, - String[] dependencies, List multiDexList) { - super(dexPath, optimizedDirectory, libraryPath, parent); - this.dependencies = dependencies; - - if (multiDexList != null) { - if (multiDexClassLoaderList == null) { - multiDexClassLoaderList = new ArrayList(multiDexList.size()); - for(String path: multiDexList) { - multiDexClassLoaderList.add(new DexClassLoader(path, optimizedDirectory, libraryPath, parent)); - } - } - } - } - - @Override - public String findLibrary(String name) { - LogUtil.d("findLibrary", name); - return super.findLibrary(name); - } - - @Override - protected Class findClass(String className) throws ClassNotFoundException { - Class clazz = null; - ClassNotFoundException suppressed = null; - try { - clazz = super.findClass(className); - } catch (ClassNotFoundException e) { - suppressed = e; - } - - if (clazz == null && !className.startsWith("android.view")) {//这里判断android.view 是为了解决webview的问题 - - if (multiDexClassLoaderList != null) { - for(DexClassLoader dexLoader : multiDexClassLoaderList) { - try { - clazz = dexLoader.loadClass(className); - } catch (ClassNotFoundException e) { - } - if (clazz != null) { - break; - } - } - } - - if (clazz == null && dependencies != null) { - for (String dependencePluginId: dependencies) { - - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(dependencePluginId); - - if (plugin != null) { - try { - clazz = plugin.pluginClassLoader.loadClass(className); - } catch (ClassNotFoundException e) { - } - if (clazz != null) { - break; - } - } else { - LogUtil.e("PluginClassLoader", "未找到插件", dependencePluginId, className); - } - } - } - } - - if (clazz == null && suppressed != null) { - throw suppressed; - } - - return clazz; - } -} diff --git a/PluginCore/src/com/plugin/core/PluginContextTheme.java b/PluginCore/src/com/plugin/core/PluginContextTheme.java deleted file mode 100644 index 26e84a1d..00000000 --- a/PluginCore/src/com/plugin/core/PluginContextTheme.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.plugin.core; - -import android.app.Application; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.database.DatabaseErrorHandler; -import android.database.sqlite.SQLiteDatabase; -import android.view.LayoutInflater; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.localservice.LocalServiceManager; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.io.File; -import java.util.ArrayList; - -public class PluginContextTheme extends PluginBaseContextWrapper { - private int mThemeResource; - Resources.Theme mTheme; - private LayoutInflater mInflater; - - Resources mResources; - private final ClassLoader mClassLoader; - private Application mPluginApplication; - protected final PluginDescriptor mPluginDescriptor; - - private ArrayList receivers = new ArrayList(); - - public PluginContextTheme(PluginDescriptor pluginDescriptor, - Context base, Resources resources, - ClassLoader classLoader) { - super(base); - mPluginDescriptor = pluginDescriptor; - mResources = resources; - mClassLoader = classLoader; - } - - public void setPluginApplication(Application pluginApplication) { - this.mPluginApplication = pluginApplication; - } - - @Override - protected void attachBaseContext(Context newBase) { - super.attachBaseContext(newBase); - } - - @Override - public ClassLoader getClassLoader() { - return mClassLoader; - } - - @Override - public AssetManager getAssets() { - return mResources.getAssets(); - } - - @Override - public Resources getResources() { - return mResources; - } - - /** - * 传0表示使用系统默认主题,最终的现实样式和客户端程序的minSdk应该有关系。 即系统针对不同的minSdk设置了不同的默认主题样式 - * 传非0的话表示传过来什么主题就显示什么主题 - */ - @Override - public void setTheme(int resid) { - mThemeResource = resid; - initializeTheme(); - } - - @Override - public Resources.Theme getTheme() { - if (mTheme != null) { - return mTheme; - } - - Object result = RefInvoker.invokeStaticMethod(Resources.class.getName(), "selectDefaultTheme", new Class[]{ - int.class, int.class}, new Object[]{mThemeResource, - getBaseContext().getApplicationInfo().targetSdkVersion}); - if (result != null) { - mThemeResource = (Integer) result; - } - - initializeTheme(); - - return mTheme; - } - - @Override - public Object getSystemService(String name) { - if (LAYOUT_INFLATER_SERVICE.equals(name)) { - if (mInflater == null) { - mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); - } - return mInflater; - } - - Object service = getBaseContext().getSystemService(name); - - if (service == null) { - service = LocalServiceManager.getService(name); - } - - return service; - } - - - private void initializeTheme() { - final boolean first = mTheme == null; - if (first) { - mTheme = getResources().newTheme(); - Resources.Theme theme = getBaseContext().getTheme(); - if (theme != null) { - mTheme.setTo(theme); - } - } - mTheme.applyStyle(mThemeResource, true); - } - - @Override - public String getPackageName() { - //如果返回插件本身的packageName可能会引起一些问题。 - //如packagemanager、activitymanager、wifi、window、inputservice - //等等系统服务会获取packageName去查询信息,如果获取到插件的packageName则会crash - //除非再增加对系统服务方法hook才能解决 - //最简单的办法还是这里保留返回宿主的packageName, - //在代码中自行区分是需要使用插件自己的还是宿主的 - //if (mPluginDescriptor.isStandalone()) { - // return mPluginDescriptor.getPackageName(); - //} else { - return super.getPackageName(); - //} - } - - /** - * 隔离插件间的SharedPreferences - * @param name - * @param mode - * @return - */ - @Override - public SharedPreferences getSharedPreferences(String name, int mode) { - String realName = mPluginDescriptor.getPackageName() + "_" + name; - LogUtil.d(realName); - return super.getSharedPreferences(realName, mode); - } - - /** - * 隔离插件间的Database - * @param name - * @param mode - * @param factory - * @return - */ - @Override - public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) { - String realName = mPluginDescriptor.getPackageName() + "_" + name; - LogUtil.d(realName); - return super.openOrCreateDatabase(realName, mode, factory); - } - - /** - * 隔离插件间的Database - * @param name - * @param mode - * @param factory - * @return - */ - @Override - public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) { - String realName = mPluginDescriptor.getPackageName() + "_" + name; - LogUtil.d(realName); - return super.openOrCreateDatabase(realName, mode, factory, errorHandler); - } - - @Override - public boolean deleteDatabase(String name) { - String realName = mPluginDescriptor.getPackageName() + "_" + name; - LogUtil.d(realName); - return super.deleteDatabase(realName); - } - - @Override - public File getDatabasePath(String name) { - String realName = mPluginDescriptor.getPackageName() + "_" + name; - LogUtil.d(realName); - return super.getDatabasePath(realName); - } - - @Override - public Context getApplicationContext() { - return mPluginApplication; - } - - @Override - public ApplicationInfo getApplicationInfo() { - return super.getApplicationInfo(); - } - - @Override - public String getPackageCodePath() { - return mPluginDescriptor.getInstalledPath(); - } - - @Override - public String getPackageResourcePath() { - return mPluginDescriptor.getInstalledPath(); - } - - public PluginDescriptor getPluginDescriptor() { - return mPluginDescriptor; - } - - @Override - public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - receivers.add(receiver); - return super.registerReceiver(receiver, filter); - } - - @Override - public void unregisterReceiver(BroadcastReceiver receiver) { - super.unregisterReceiver(receiver); - receivers.remove(receiver); - } - - public void unregisterAllReceiver() { - for (BroadcastReceiver br: - receivers) { - super.unregisterReceiver(br); - } - receivers.clear(); - } -} diff --git a/PluginCore/src/com/plugin/core/PluginCreator.java b/PluginCore/src/com/plugin/core/PluginCreator.java deleted file mode 100644 index 77958602..00000000 --- a/PluginCore/src/com/plugin/core/PluginCreator.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.plugin.core; - -import java.io.File; -import java.util.List; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import android.content.Context; -import android.content.res.AssetManager; -import android.content.res.Resources; -import dalvik.system.DexClassLoader; - -public class PluginCreator { - - private PluginCreator() { - } - - /** - * 根据插件apk文件,创建插件dex的classloader - * - * @param absolutePluginApkPath - * 插件apk文件路径 - * @return - */ - public static DexClassLoader createPluginClassLoader(String absolutePluginApkPath, boolean isStandalone, - String[] dependences, List pluginApkMultDexPath) { - - String apkParentDir = new File(absolutePluginApkPath).getParent(); - - File optDir = new File(apkParentDir, "dalvik-cache"); - optDir.mkdirs(); - - File libDir = new File(apkParentDir, "lib"); - libDir.mkdirs(); - - if (!isStandalone) {//非独立插件 - return new PluginClassLoader( - absolutePluginApkPath, - optDir.getAbsolutePath(), - libDir.getAbsolutePath(), - PluginLoader.class.getClassLoader(),//宿主classloader - dependences,//插件依赖的插件 - null); - } else {//独立插件 - return new PluginClassLoader( - absolutePluginApkPath, - optDir.getAbsolutePath(), - libDir.getAbsolutePath(), - /* - * In theory this should be the "system" class loader; in practice we - * don't use that and can happily (and more efficiently) use the - * bootstrap class loader. - */ - ClassLoader.getSystemClassLoader().getParent(),//系统classloader - null,//独立插件无依赖 - pluginApkMultDexPath); - } - - } - - /** - * 根据插件apk文件,创建插件资源文件,同时绑定宿主程序的资源,这样就可以在插件中使用宿主程序的资源。 - * - * @return - */ - public static Resources createPluginResource(String mainApkPath, Resources mainRes, PluginDescriptor pluginDescriptor) { - String absolutePluginApkPath = pluginDescriptor.getInstalledPath(); - boolean isStandalone = pluginDescriptor.isStandalone(); - String[] dependencies = pluginDescriptor.getDependencies(); - - try { - String[] assetPaths = buildAssetPath(isStandalone, mainApkPath, - absolutePluginApkPath, dependencies); - AssetManager assetMgr = AssetManager.class.newInstance(); - RefInvoker.invokeMethod(assetMgr, AssetManager.class.getName(), "addAssetPaths", - new Class[] { String[].class }, new Object[] { assetPaths }); - - Resources pluginRes = new PluginResourceWrapper(assetMgr, mainRes.getDisplayMetrics(), - mainRes.getConfiguration(), pluginDescriptor); - - return pluginRes; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - private static String[] buildAssetPath(boolean isStandalone, String app, String plugin, String[] dependencies) { - dependencies = null;//暂不支持资源多级依赖, 会导致插件难以维护 - String[] assetPaths = new String[isStandalone ? 1 : (2 + (dependencies==null?0:dependencies.length))]; - -// if (!isStandalone) { -// // 不可更改顺序否则不能兼容4.x -// assetPaths[0] = app; -// assetPaths[1] = plugin; -// if ("vivo".equalsIgnoreCase(Build.BRAND) || "oppo".equalsIgnoreCase(Build.BRAND) -// || "Coolpad".equalsIgnoreCase(Build.BRAND)) { -// // 但是!!!如是OPPO或者vivo4.x系统的话 ,要吧这个顺序反过来,否则在混合模式下会找不到资源 -// assetPaths[0] = plugin; -// assetPaths[1] = app; -// } -// LogUtil.d("create Plugin Resource from: ", assetPaths[0], assetPaths[1]); -// } else { -// assetPaths[0] = plugin; -// LogUtil.d("create Plugin Resource from: ", assetPaths[0]); -// } - - - if (!isStandalone) { - // 不可更改顺序否则不能兼容4.x,如华为P7-Android4.4.2 - assetPaths[0] = plugin; - if (dependencies != null) { - //插件间资源依赖,这里需要遍历添加dependencies - //这里只处理1级依赖,若被依赖的插件又依赖其他插件,这里不做支持 - //插件依赖插件,如果被依赖的插件中包含资源文件,则需要在所有的插件中提供public.xml文件来分组资源id - for(int i = 0; i < dependencies.length; i++) { - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(dependencies[i]); - if (pd != null) { - assetPaths[1+ i] = pd.getInstalledPath(); - } else { - assetPaths[1+ i] = ""; - } - } - } - assetPaths[assetPaths.length -1] = app; - LogUtil.d("create Plugin Resource from: ", assetPaths[0], assetPaths[1]); - } else { - assetPaths[0] = plugin; - LogUtil.d("create Plugin Resource from: ", assetPaths[0]); - } - - return assetPaths; - - } - - /** - * 创建插件的Context - * @return - */ - public static Context createPluginContext(PluginDescriptor pluginDescriptor, Context base, Resources pluginRes, - DexClassLoader pluginClassLoader) { - return new PluginContextTheme(pluginDescriptor, base, pluginRes, pluginClassLoader); - } -} diff --git a/PluginCore/src/com/plugin/core/PluginInjector.java b/PluginCore/src/com/plugin/core/PluginInjector.java deleted file mode 100644 index 84903d21..00000000 --- a/PluginCore/src/com/plugin/core/PluginInjector.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.plugin.core; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.Application; -import android.app.Instrumentation; -import android.app.Service; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ProviderInfo; -import android.os.Build; -import android.os.IBinder; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.Window; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginActivityInfo; -import com.plugin.content.PluginDescriptor; -import com.plugin.content.PluginProviderInfo; -import com.plugin.core.annotation.AnnotationProcessor; -import com.plugin.core.annotation.FragmentContainer; -import com.plugin.core.app.ActivityThread; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; -import com.plugin.util.RefInvoker; -import com.plugin.util.ResourceUtil; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class PluginInjector { - - private static final String android_content_ContextWrapper_mBase = "mBase"; - - private static final String android_content_ContextThemeWrapper_attachBaseContext = "attachBaseContext"; - private static final String android_content_ContextThemeWrapper_mResources = "mResources"; - private static final String android_content_ContextThemeWrapper_mTheme = "mTheme"; - - private static final String android_app_Activity_mInstrumentation = "mInstrumentation"; - private static final String android_app_Activity_mActivityInfo = "mActivityInfo"; - - /** - * 替换宿主程序Application对象的mBase是为了修改它的几个StartActivity、 - * StartService和SendBroadcast方法 - */ - static void injectBaseContext(Context context) { - LogUtil.d("替换宿主程序Application对象的mBase"); - Context base = (Context)RefInvoker.getFieldObject(context, ContextWrapper.class.getName(), - android_content_ContextWrapper_mBase); - Context newBase = new PluginBaseContextWrapper(base); - RefInvoker.setFieldObject(context, ContextWrapper.class.getName(), - android_content_ContextWrapper_mBase, newBase); - } - - /** - * 注入Instrumentation主要是为了支持Activity - */ - static void injectInstrumentation() { - // 给Instrumentation添加一层代理,用来实现隐藏api的调用 - LogUtil.d("替换宿主程序Intstrumentation"); - ActivityThread.wrapInstrumentation(); - } - - static void injectHandlerCallback() { - LogUtil.d("向宿主程序消息循环插入回调器"); - ActivityThread.wrapHandler(); - } - - public static void installContentProviders(Context context, Collection pluginProviderInfos) { - LogUtil.d("安装插件ContentProvider", pluginProviderInfos.size()); - PluginInjector.hackHostClassLoaderIfNeeded(); - List providers = new ArrayList(); - for (PluginProviderInfo pluginProviderInfo : pluginProviderInfos) { - ProviderInfo p = new ProviderInfo(); - //name做上标记,表示是来自插件,方便classloader进行判断 - p.name = PluginProviderInfo.CLASS_PREFIX + pluginProviderInfo.getName(); - p.authority = pluginProviderInfo.getAuthority(); - p.applicationInfo = context.getApplicationInfo(); - p.exported = pluginProviderInfo.isExported(); - p.packageName = context.getApplicationInfo().packageName; - providers.add(p); - } - - ActivityThread.installContentProviders(context, providers); - } - - static void injectInstrumetionFor360Safe(Activity activity, Instrumentation pluginInstrumentation) { - // 检查mInstrumention是否已经替换成功。 - // 之所以要检查,是因为如果手机上安装了360手机卫士等app,它们可能会劫持用户app的ActivityThread对象, - // 导致在PluginApplication的onCreate方法里面替换mInstrumention可能会失败 - // 所以这里再做一次检查 - Instrumentation instrumention = (Instrumentation) RefInvoker.getFieldObject(activity, Activity.class.getName(), - android_app_Activity_mInstrumentation); - if (!(instrumention instanceof PluginInstrumentionWrapper)) { - // 说明被360还原了,这里再次尝试替换 - RefInvoker.setFieldObject(activity, Activity.class.getName(), android_app_Activity_mInstrumentation, pluginInstrumentation); - } - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - static void injectActivityContext(Activity activity) { - Intent intent = activity.getIntent(); - FragmentContainer fragmentContainer = AnnotationProcessor.getFragmentContainer(activity.getClass()); - // 如果是打开插件中的activity, 或者是打开的用来显示插件fragment的宿主activity - if (ProcessUtil.isPluginProcess() && (fragmentContainer != null || - PluginManagerHelper.isStubActivity(intent.getComponent().getClassName()))) { - // 为了不需要重写插件Activity的attachBaseContext方法为: - // 我们在activityoncreate之前去完成attachBaseContext的事情 - - Context pluginContext = null; - PluginDescriptor pd = null; - - //是打开的用来显示插件fragment的宿主activity - if (fragmentContainer != null) { - // 为了能够在宿主中的Activiy里面展示来自插件的普通Fragment, - // 我们将宿主程序中用来展示插件普通Fragment的Activity的Context也替换掉 - - if (!TextUtils.isEmpty(fragmentContainer.pluginId())) { - - pd = PluginManagerHelper.getPluginDescriptorByPluginId(fragmentContainer.pluginId()); - - LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(fragmentContainer.pluginId()); - - pluginContext = PluginLoader.getNewPluginComponentContext(plugin.pluginContext, activity.getBaseContext(), 0); - - } else if (!TextUtils.isEmpty(fragmentContainer.fragmentId())) { - String classId = null; - try { - //TODO 不应该从intent中去取参数 - classId = intent.getStringExtra(fragmentContainer.fragmentId()); - } catch (Exception e) { - LogUtil.printException("这里的Intent如果包含来自插件的VO对象实例," + - "会产生ClassNotFound异常", e); - } - if (classId != null) { - @SuppressWarnings("rawtypes") - Class clazz = PluginLoader.loadPluginFragmentClassById(classId); - - pd = PluginManagerHelper.getPluginDescriptorByClassName(clazz.getName()); - - LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pd.getPackageName()); - - pluginContext = PluginLoader.getNewPluginComponentContext(plugin.pluginContext, activity.getBaseContext(), 0); - - } else { - return; - } - } else { - LogUtil.e("FragmentContainer注解至少配置一个参数:pluginId, fragmentId"); - return; - } - - } else { - //是打开插件中的activity - pd = PluginManagerHelper.getPluginDescriptorByClassName(activity.getClass().getName()); - - LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pd.getPackageName()); - - pluginContext = PluginLoader.getNewPluginComponentContext(plugin.pluginContext, activity.getBaseContext(), 0); - - //获取插件Application对象 - Application pluginApp = plugin.pluginApplication; - - //重设mApplication - RefInvoker.setFieldObject(activity, Activity.class.getName(), - "mApplication", pluginApp); - - } - - PluginActivityInfo pluginActivityInfo = pd.getActivityInfos().get(activity.getClass().getName()); - ActivityInfo activityInfo = (ActivityInfo) RefInvoker.getFieldObject(activity, Activity.class.getName(), - android_app_Activity_mActivityInfo); - int pluginAppTheme = getPluginTheme(activityInfo, pluginActivityInfo, pd); - - LogUtil.e("Theme", "0x" + Integer.toHexString(pluginAppTheme), activity.getClass().getName()); - - resetActivityContext(pluginContext, activity, pluginAppTheme); - - resetWindowConfig(pluginContext, pd, activity, activityInfo, pluginActivityInfo); - - activity.setTitle(activity.getClass().getName()); - - } else { - // 如果是打开宿主程序的activity,注入一个无害的Context,用来在宿主程序中startService和sendBroadcast时检查打开的对象是否是插件中的对象 - // 插入Context - Context mainContext = new PluginBaseContextWrapper(activity.getBaseContext()); - RefInvoker.setFieldObject(activity, ContextWrapper.class.getName(), android_content_ContextWrapper_mBase, null); - RefInvoker.invokeMethod(activity, ContextThemeWrapper.class.getName(), android_content_ContextThemeWrapper_attachBaseContext, - new Class[]{Context.class}, new Object[]{mainContext}); - } - } - - static void resetActivityContext(final Context pluginContext, final Activity activity, - final int pluginAppTheme) { - if (pluginContext == null) { - return; - } - - // 重设BaseContext - RefInvoker.setFieldObject(activity, ContextWrapper.class.getName(), android_content_ContextWrapper_mBase, null); - RefInvoker.invokeMethod(activity, ContextThemeWrapper.class.getName(), android_content_ContextThemeWrapper_attachBaseContext, - new Class[]{Context.class }, new Object[] { pluginContext }); - - // 由于在attach的时候Resource已经被初始化了,所以需要重置Resource - RefInvoker.setFieldObject(activity, ContextThemeWrapper.class.getName(), android_content_ContextThemeWrapper_mResources, null); - - // 重设theme - if (pluginAppTheme != 0) { - RefInvoker.setFieldObject(activity, ContextThemeWrapper.class.getName(), android_content_ContextThemeWrapper_mTheme, null); - activity.setTheme(pluginAppTheme); - } - // 重设theme - ((PluginContextTheme)pluginContext).mTheme = null; - pluginContext.setTheme(pluginAppTheme); - - //重设mContext - RefInvoker.setFieldObject(activity.getWindow(), Window.class.getName(), - "mContext", pluginContext); - - //重设mWindowStyle - RefInvoker.setFieldObject(activity.getWindow(), Window.class.getName(), - "mWindowStyle", null); - - // 重设LayoutInflater - LogUtil.d(activity.getWindow().getClass().getName()); - RefInvoker.setFieldObject(activity.getWindow(), activity.getWindow().getClass().getName(), - "mLayoutInflater", LayoutInflater.from(activity)); - - // 如果api>=11,还要重设factory2 - if (Build.VERSION.SDK_INT >= 11) { - RefInvoker.invokeMethod(activity.getWindow().getLayoutInflater(), LayoutInflater.class.getName(), - "setPrivateFactory", new Class[]{LayoutInflater.Factory2.class}, new Object[]{activity}); - } - } - - static void resetWindowConfig(final Context pluginContext, final PluginDescriptor pd, - final Activity activity, - final ActivityInfo activityInfo, - final PluginActivityInfo pluginActivityInfo) { - - if (pluginActivityInfo != null) { - if (null != pluginActivityInfo.getWindowSoftInputMode()) { - activity.getWindow().setSoftInputMode(Integer.parseInt(pluginActivityInfo.getWindowSoftInputMode().replace("0x", ""), 16)); - } - if (Build.VERSION.SDK_INT >= 14) { - if (null != pluginActivityInfo.getUiOptions()) { - activity.getWindow().setUiOptions(Integer.parseInt(pluginActivityInfo.getUiOptions().replace("0x", ""), 16)); - } - } - if (null != pluginActivityInfo.getScreenOrientation()) { - int orientation = Integer.parseInt(pluginActivityInfo.getScreenOrientation()); - //noinspection ResourceType - if (orientation != activityInfo.screenOrientation && !activity.isChild()) { - //noinspection ResourceType - activity.setRequestedOrientation(orientation); - } - } - if (Build.VERSION.SDK_INT >= 18 && !activity.isChild()) { - Boolean isImmersive = ResourceUtil.getBoolean(pluginActivityInfo.getImmersive(), pluginContext); - if (isImmersive != null) { - activity.setImmersive(isImmersive); - } - } - - LogUtil.d(activity.getClass().getName(), "immersive", pluginActivityInfo.getImmersive()); - LogUtil.d(activity.getClass().getName(), "screenOrientation", pluginActivityInfo.getScreenOrientation()); - LogUtil.d(activity.getClass().getName(), "launchMode", pluginActivityInfo.getLaunchMode()); - LogUtil.d(activity.getClass().getName(), "windowSoftInputMode", pluginActivityInfo.getWindowSoftInputMode()); - LogUtil.d(activity.getClass().getName(), "uiOptions", pluginActivityInfo.getUiOptions()); - } - - //如果是独立插件,由于没有合并资源,这里还需要替换掉 mActivityInfo, - //避免activity试图通过ActivityInfo中的资源id来读取资源时失败 - activityInfo.icon = pd.getApplicationIcon(); - activityInfo.logo = pd.getApplicationLogo(); - if (Build.VERSION.SDK_INT >= 19) { - activity.getWindow().setIcon(activityInfo.icon); - activity.getWindow().setLogo(activityInfo.logo); - } - } - - /*package*/static void replaceReceiverContext(Context baseContext, Context newBase) { - - if (baseContext.getClass().getName().equals("android.app.ContextImpl")) { - ContextWrapper receiverRestrictedContext = (ContextWrapper) RefInvoker.invokeMethod(baseContext, "android.app.ContextImpl", "getReceiverRestrictedContext", (Class[]) null, (Object[]) null); - RefInvoker.setFieldObject(receiverRestrictedContext, ContextWrapper.class.getName(), "mBase", newBase); - } - } - - /*package*/static void replacePluginServiceContext(String serviceName) { - Map services = ActivityThread.getAllServices(); - if (services != null) { - Iterator itr = services.values().iterator(); - while(itr.hasNext()) { - Service service = itr.next(); - if (service != null && service.getClass().getName().equals(serviceName) ) { - - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByClassName(serviceName); - - LoadedPlugin plugin = PluginLauncher.instance().getRunningPlugin(pd.getPackageName()); - - RefInvoker.setFieldObject(service, ContextWrapper.class.getName(), "mBase", - PluginLoader.getNewPluginComponentContext(plugin.pluginContext, - service.getBaseContext(), pd.getApplicationTheme())); - - RefInvoker.setFieldObject(service, Service.class.getName(), "mApplication", plugin.pluginApplication); - - RefInvoker.setFieldObject(service, Service.class, "mClassName", PluginManagerHelper.bindStubService(service.getClass().getName())); - - //这里不退出循环,是因为在多进程情况下,杀死插件进程,自动恢复service时有个bug导致一个service同时存在多个service实例 - //这里做个遍历保护 - //break; - } - - } - } - } - - /*package*/static void replaceHostServiceContext(String serviceName) { - Map services = ActivityThread.getAllServices(); - if (services != null) { - Iterator itr = services.values().iterator(); - while(itr.hasNext()) { - Service service = itr.next(); - if (service != null && service.getClass().getName().equals(serviceName) ) { - PluginInjector.injectBaseContext(service); - break; - } - - } - } - } - - /** - * 主题的选择顺序为 先选择插件Activity配置的主题,再选择插件Application配置的主题,再选择宿主Activity主题 - * @param activityInfo - * @param pluginActivityInfo - * @param pd - * @return - */ - private static int getPluginTheme(ActivityInfo activityInfo, PluginActivityInfo pluginActivityInfo, PluginDescriptor pd) { - int pluginAppTheme = 0; - if (pluginActivityInfo != null ) { - pluginAppTheme = ResourceUtil.getResourceId(pluginActivityInfo.getTheme()); - } - if (pluginAppTheme == 0) { - pluginAppTheme = pd.getApplicationTheme(); - } - if (pluginAppTheme == 0) { - //If the activity defines a theme, that is used; else, the application theme is used. - pluginAppTheme = activityInfo.getThemeResource(); - } - return pluginAppTheme; - } - - /** - * 通常系统服务实例内部都有一个成员变量private final Context mContext; - * - * 这个成员变量通常是一个ContextImpl实例。 - * - * @param manager 通过getSystemService获取的系统服务。例如 ActivityManager - * - */ - static void replaceContext(Object manager, Context context) { - Object original = RefInvoker.getFieldObject(manager, manager.getClass(), "mContext"); - if (original != null) {//表示确实存在此成员变量对象,替换掉 - RefInvoker.setFieldObject(manager, manager.getClass().getName(), "mContext", context); - } - } - - /** - * 如果插件中不包含service、receiver和contentprovider,是不需要替换classloader的 - */ - public static void hackHostClassLoaderIfNeeded() { - Object mLoadedApk = RefInvoker.getFieldObject(PluginLoader.getApplication(), Application.class.getName(), - "mLoadedApk"); - ClassLoader originalLoader = (ClassLoader) RefInvoker.getFieldObject(mLoadedApk, "android.app.LoadedApk", - "mClassLoader"); - if (!(originalLoader instanceof HostClassLoader)) { - HostClassLoader newLoader = new HostClassLoader("", PluginLoader.getApplication() - .getCacheDir().getAbsolutePath(), - PluginLoader.getApplication().getCacheDir().getAbsolutePath(), originalLoader); - RefInvoker.setFieldObject(mLoadedApk, "android.app.LoadedApk", "mClassLoader", newLoader); - } - } -} diff --git a/PluginCore/src/com/plugin/core/PluginInstrumentionWrapper.java b/PluginCore/src/com/plugin/core/PluginInstrumentionWrapper.java deleted file mode 100644 index bf72cafe..00000000 --- a/PluginCore/src/com/plugin/core/PluginInstrumentionWrapper.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.plugin.core; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.Application; -import android.app.Fragment; -import android.app.Instrumentation; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.UserHandle; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.annotation.AnnotationProcessor; -import com.plugin.core.annotation.ComponentContainer; -import com.plugin.core.manager.PluginActivityMonitor; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.core.systemservice.AndroidWebkitWebViewFactoryProvider; -import com.plugin.core.viewfactory.PluginViewFactory; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; -import com.plugin.util.RefInvoker; - -import java.util.Iterator; -import java.util.Set; - -/** - * 插件Activity免注册的主要实现原理。 如有必要,可以增加被代理的方法数量。 - * - * @author cailiming - * - */ -public class PluginInstrumentionWrapper extends Instrumentation { - - private static final String RELAUNCH_FLAG = "relaunch.category."; - - private final Instrumentation realInstrumention; - private PluginActivityMonitor monitor; - - public PluginInstrumentionWrapper(Instrumentation instrumentation) { - this.realInstrumention = instrumentation; - this.monitor = new PluginActivityMonitor(); - } - - @Override - public boolean onException(Object obj, Throwable e) { - if (obj instanceof Activity) { - ((Activity) obj).finish(); - } else if (obj instanceof Service) { - ((Service) obj).stopSelf(); - } - LogUtil.printException("记录错误信息", e); - return super.onException(obj, e); - } - - @Override - public Application newApplication(ClassLoader cl, String className, Context context) - throws InstantiationException, IllegalAccessException, - ClassNotFoundException { - if (ProcessUtil.isPluginProcess()) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pluginDescriptor != null) { - return PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()).pluginApplication; - } - } - return super.newApplication(cl, className, context); - } - - @Override - public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, - IllegalAccessException, ClassNotFoundException { - if (ProcessUtil.isPluginProcess()) { - // 将PluginStubActivity替换成插件中的activity - if (PluginManagerHelper.isStubActivity(className)) { - - String action = intent.getAction(); - - LogUtil.d(action, className); - - if (action != null && action.contains(PluginIntentResolver.CLASS_SEPARATOR)) { - String[] targetClassName = action.split(PluginIntentResolver.CLASS_SEPARATOR); - String pluginClassName = targetClassName[0]; - - Class clazz = PluginLoader.loadPluginClassByName(pluginClassName); - if (clazz != null) { - className = pluginClassName; - cl = clazz.getClassLoader(); - - intent.setExtrasClassLoader(cl); - if (targetClassName.length >1) { - //之前为了传递classNae,intent的action被修改过 这里再把Action还原到原始的Action - intent.setAction(targetClassName[1]); - } else { - intent.setAction(null); - } - //添加一个标记符 - intent.addCategory(RELAUNCH_FLAG + className); - } - } else if (PluginManagerHelper.isExact(className, PluginDescriptor.ACTIVITY)) { - //这个逻辑是为了支持外部app唤起配置了stub_exact的插件Activity - Class clazz = PluginLoader.loadPluginClassByName(className); - if (clazz != null) { - cl = clazz.getClassLoader(); - } - } else { - //进入这个分支可能是因为activity重启了,比如横竖屏切换,由于上面的分支已经把Action还原到原始到Action了 - //这里只能通过之前添加的标记符来查找className - Set category = intent.getCategories(); - if (category != null) { - Iterator itr = category.iterator(); - while (itr.hasNext()) { - String cate = itr.next(); - - if (cate.startsWith(RELAUNCH_FLAG)) { - className = cate.replace(RELAUNCH_FLAG, ""); - - Class clazz = PluginLoader.loadPluginClassByName(className); - cl = clazz.getClassLoader(); - break; - } - } - } - } - } else { - //到这里有2中种情况 - //1、确实是宿主Activity - //2、是插件Activity,但是上面的if没有识别出来(这种情况目前只发现在ActivityGroup情况下会出现,因为ActivityGroup不会触发resolveActivity方法,导致Intent没有更换) - //判断上述两种情况可以通过ClassLoader的类型来判断, 判断出来以后补一个resolveActivity方法 - if (cl instanceof PluginClassLoader) { - PluginIntentResolver.resolveActivity(intent); - } - } - } - - return super.newActivity(cl, className, intent); - } - - @Override - public void callActivityOnCreate(Activity activity, Bundle icicle) { - - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - PluginInjector.injectActivityContext(activity); - - Intent intent = activity.getIntent(); - - if (intent != null) { - intent.setExtrasClassLoader(activity.getClassLoader()); - } - - if (ProcessUtil.isPluginProcess()) { - //是否启用控件级插件功能 - //控件级插件功能默认是关闭的 - //在宿主的需要支持控件级插件的Activity上配置下面这个注解,用来启用此功能 - //之所以默认关闭此功能,是因为控件级插件和换肤不能共存 - ComponentContainer componentContainer = AnnotationProcessor.getComponentContainer(activity.getClass()); - if (componentContainer != null) { - new PluginViewFactory(activity, activity.getWindow(), new PluginViewCreator()).installViewFactory(); - } - - AndroidWebkitWebViewFactoryProvider.switchWebViewContext(activity); - } - - super.callActivityOnCreate(activity, icicle); - - monitor.onActivityCreate(activity); - - } - - - @Override - public void callActivityOnDestroy(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - monitor.onActivityDestory(activity); - - super.callActivityOnDestroy(activity); - } - - @Override - public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - if (savedInstanceState != null) { - savedInstanceState.setClassLoader(activity.getClassLoader()); - } - - super.callActivityOnRestoreInstanceState(activity, savedInstanceState); - } - - @Override - public void callActivityOnPostCreate(Activity activity, Bundle icicle) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - if (icicle != null) { - icicle.setClassLoader(activity.getClassLoader()); - } - - super.callActivityOnPostCreate(activity, icicle); - } - - @Override - public void callActivityOnNewIntent(Activity activity, Intent intent) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - if (intent != null) { - intent.setExtrasClassLoader(activity.getClassLoader()); - } - - super.callActivityOnNewIntent(activity, intent); - } - - @Override - public void callActivityOnStart(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnStart(activity); - } - - @Override - public void callActivityOnRestart(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnRestart(activity); - } - - @Override - public void callActivityOnResume(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnResume(activity); - } - - @Override - public void callActivityOnStop(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnStop(activity); - } - - @Override - public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - - if (outState != null) { - outState.setClassLoader(activity.getClassLoader()); - } - - super.callActivityOnSaveInstanceState(activity, outState); - } - - @Override - public void callActivityOnPause(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnPause(activity); - } - - @Override - public void callActivityOnUserLeaving(Activity activity) { - PluginInjector.injectInstrumetionFor360Safe(activity, this); - super.callActivityOnUserLeaving(activity); - } - - public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, - Intent intent, int requestCode, Bundle options) { - - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivity", new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, - Intent.class, int.class, Bundle.class }, new Object[] { who, contextThread, token, target, - intent, requestCode, options }); - - return (ActivityResult) result; - } - - public void execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, - Intent[] intents, Bundle options) { - - PluginIntentResolver.resolveActivity(intents); - - RefInvoker - .invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), "execStartActivities", - new Class[]{Context.class, IBinder.class, IBinder.class, Activity.class, Intent[].class, - Bundle.class}, new Object[]{who, contextThread, token, target, intents, options}); - } - - public void execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, - Intent[] intents, Bundle options, int userId) { - - PluginIntentResolver.resolveActivity(intents); - - RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivitiesAsUser", new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, - Intent[].class, Bundle.class, int.class }, new Object[] { who, contextThread, token, target, - intents, options, userId }); - } - - public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, - Fragment target, Intent intent, int requestCode, Bundle options) { - - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivity", new Class[] { Context.class, IBinder.class, IBinder.class, - Fragment.class, Intent.class, int.class, Bundle.class }, new Object[] { who, - contextThread, token, target, intent, requestCode, options }); - - return (ActivityResult) result; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, - Intent intent, int requestCode, Bundle options, UserHandle user) { - - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivity", new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, - Intent.class, int.class, Bundle.class, UserHandle.class }, new Object[] { who, contextThread, - token, target, intent, requestCode, options, user }); - - return (ActivityResult) result; - } - - - ///////////// Android 4.0.4及以下 /////////////// - - public ActivityResult execStartActivity( - Context who, IBinder contextThread, IBinder token, Activity target, - Intent intent, int requestCode) { - - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivity", new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, - Intent.class, int.class }, new Object[] { who, contextThread, - token, target, intent, requestCode }); - - return (ActivityResult) result; - } - - public void execStartActivities(Context who, IBinder contextThread, - IBinder token, Activity target, Intent[] intents) { - PluginIntentResolver.resolveActivity(intents); - - RefInvoker - .invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), "execStartActivities", - new Class[]{Context.class, IBinder.class, IBinder.class, Activity.class, Intent[].class}, - new Object[]{who, contextThread, token, target, intents}); - } - - public ActivityResult execStartActivity( - Context who, IBinder contextThread, IBinder token, Fragment target, - Intent intent, int requestCode) { - - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivity", new Class[] { Context.class, IBinder.class, IBinder.class, Fragment.class, - Intent.class, int.class }, new Object[] { who, contextThread, - token, target, intent, requestCode }); - - return (ActivityResult) result; - } - - /////// For Android 5.1 - public ActivityResult execStartActivityAsCaller( - Context who, IBinder contextThread, IBinder token, Activity target, - Intent intent, int requestCode, Bundle options, int userId) { - PluginIntentResolver.resolveActivity(intent); - - Object result = RefInvoker.invokeMethod(realInstrumention, android.app.Instrumentation.class.getName(), - "execStartActivityAsCaller", new Class[] { Context.class, IBinder.class, IBinder.class, Activity.class, - Intent.class, int.class, Bundle.class, int.class}, new Object[] { who, contextThread, - token, target, intent, requestCode, options, userId}); - return (ActivityResult)result; - } - - public void execStartActivityFromAppTask( - Context who, IBinder contextThread, Object appTask, - Intent intent, Bundle options) { - - PluginIntentResolver.resolveActivity(intent); - - try { - RefInvoker.invokeMethod(realInstrumention, Instrumentation.class.getName(), - "execStartActivityFromAppTask", new Class[]{Context.class, IBinder.class, - Class.forName("android.app.IAppTask"), Intent.class, Bundle.class,}, - new Object[]{who, contextThread, appTask, intent, options}); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} diff --git a/PluginCore/src/com/plugin/core/PluginIntentResolver.java b/PluginCore/src/com/plugin/core/PluginIntentResolver.java deleted file mode 100644 index 5be47ab1..00000000 --- a/PluginCore/src/com/plugin/core/PluginIntentResolver.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.plugin.core; - -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.ServiceInfo; -import android.os.Build; - -import com.plugin.content.PluginActivityInfo; -import com.plugin.content.PluginDescriptor; -import com.plugin.content.PluginReceiverIntent; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.util.ArrayList; - -public class PluginIntentResolver { - - public static final String CLASS_SEPARATOR = "_RECEIVER_AND_ACTIVITY_"; - public static final String CLASS_PREFIX = "_RECEIVER_AND_SERVICE_"; - - public static void resolveService(Intent service) { - ArrayList classNameList = PluginLoader.matchPlugin(service, PluginDescriptor.SERVICE); - if (classNameList != null && classNameList.size() > 0) { - String stubServiceName = PluginManagerHelper.bindStubService(classNameList.get(0)); - if (stubServiceName != null) { - service.setComponent(new ComponentName(PluginLoader.getApplication().getPackageName(), stubServiceName)); - } - } - } - - public static ArrayList resolveReceiver(final Intent intent) { - // 如果在插件中发现了匹配intent的receiver项目,替换掉ClassLoader - // 不需要在这里记录目标className,className将在Intent中传递 - ArrayList result = new ArrayList(); - ArrayList classNameList = PluginLoader.matchPlugin(intent, PluginDescriptor.BROADCAST); - if (classNameList != null && classNameList.size() > 0) { - for(String className: classNameList) { - Intent newIntent = new Intent(intent); - newIntent.setComponent(new ComponentName(PluginLoader.getApplication().getPackageName(), - PluginManagerHelper.bindStubReceiver())); - //hackReceiverForClassLoader检测到这个标记后会进行替换 - newIntent.setAction(className + CLASS_SEPARATOR + (intent.getAction() == null ? "" : intent.getAction())); - result.add(newIntent); - } - } else { - result.add(intent); - } - return result; - } - - /* package */static Class resolveReceiverForClassLoader(Object msgObj) { - Intent intent = (Intent) RefInvoker.getFieldObject(msgObj, "android.app.ActivityThread$ReceiverData", "intent"); - if (intent.getComponent().getClassName().equals(PluginManagerHelper.bindStubReceiver())) { - String action = intent.getAction(); - LogUtil.d("action", action); - if (action != null) { - String[] targetClassName = action.split(CLASS_SEPARATOR); - @SuppressWarnings("rawtypes") - Class clazz = PluginLoader.loadPluginClassByName(targetClassName[0]); - if (clazz != null) { - intent.setExtrasClassLoader(clazz.getClassLoader()); - //由于之前intent被修改过 这里再吧Intent还原到原始的intent - if (targetClassName.length > 1) { - intent.setAction(targetClassName[1]); - } else { - intent.setAction(null); - } - } - // PluginClassLoader检测到这个特殊标记后会进行替换 - intent.setComponent(new ComponentName(intent.getComponent().getPackageName(), - CLASS_PREFIX + targetClassName[0])); - - if (Build.VERSION.SDK_INT >= 21) { - if (intent.getExtras() != null) { - PluginReceiverIntent newIntent = new PluginReceiverIntent(intent); - RefInvoker.setFieldObject(msgObj, "android.app.ActivityThread$ReceiverData", "intent", newIntent); - } - } - - return clazz; - } - } - return null; - } - - /* package */static String resolveServiceForClassLoader(Object msgObj) { - ServiceInfo info = (ServiceInfo) RefInvoker.getFieldObject(msgObj, "android.app.ActivityThread$CreateServiceData", "info"); - //通过映射查找 - String targetClassName = PluginManagerHelper.getBindedPluginServiceName(info.name); - //TODO 或许可以通过这个方式来处理service - //info.applicationInfo = XXX - - LogUtil.d("hackServiceName", info.name, info.packageName, info.processName, "targetClassName", targetClassName, info.applicationInfo.packageName); - - if (targetClassName != null) { - info.name = CLASS_PREFIX + targetClassName; - } else { - LogUtil.e("hackServiceName 没有找到映射关系, 有2个可能:1、确实是宿主service;2、映射表出了异常。如果是映射表出了异常会导致classNotFound", info.name); - PluginManagerHelper.dumpServiceInfo(); - } - return info.name; - } - - public static void resolveActivity(Intent intent) { - // 如果在插件中发现Intent的匹配项,记下匹配的插件Activity的ClassName - ArrayList classNameList = PluginLoader.matchPlugin(intent, PluginDescriptor.ACTIVITY); - if (classNameList != null && classNameList.size() > 0) { - - String className = classNameList.get(0); - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByClassName(className); - - PluginActivityInfo pluginActivityInfo = pd.getActivityInfos().get(className); - - String stubActivityName = PluginManagerHelper.bindStubActivity(className, Integer.parseInt(pluginActivityInfo.getLaunchMode())); - - intent.setComponent( - new ComponentName(PluginLoader.getApplication().getPackageName(), stubActivityName)); - //PluginInstrumentationWrapper检测到这个标记后会进行替换 - intent.setAction(className + CLASS_SEPARATOR + (intent.getAction()==null?"":intent.getAction())); - } - } - - /* package */static void resolveActivity(Intent[] intent) { - // 不常用。需要时再实现此方法, - } - -} diff --git a/PluginCore/src/com/plugin/core/PluginLauncher.java b/PluginCore/src/com/plugin/core/PluginLauncher.java deleted file mode 100644 index f4d78f61..00000000 --- a/PluginCore/src/com/plugin/core/PluginLauncher.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.plugin.core; - - -import android.app.Activity; -import android.app.Application; -import android.app.Instrumentation; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.app.ActivityThread; -import com.plugin.core.app.AndroidAppApplication; -import com.plugin.core.localservice.LocalServiceManager; -import com.plugin.core.manager.PluginActivityMonitor; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.core.systemservice.AndroidWebkitWebViewFactoryProvider; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; -import com.plugin.util.RefInvoker; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import dalvik.system.DexClassLoader; - -/** - *
- * @author cailiming
- * 
- * - */ -public class PluginLauncher implements Serializable { - - private static PluginLauncher runtime; - - private HashMap loadedPluginMap = new HashMap(); - - private PluginLauncher() { - if (!ProcessUtil.isPluginProcess()) { - throw new IllegalAccessError("本类仅在插件进程使用"); - } - } - - public static PluginLauncher instance() { - if (runtime == null) { - synchronized (PluginLauncher.class) { - if (runtime == null) { - runtime = new PluginLauncher(); - } - } - } - return runtime; - } - - public LoadedPlugin getRunningPlugin(String packageName) { - return loadedPluginMap.get(packageName); - } - - public LoadedPlugin startPlugin(String packageName) { - LoadedPlugin plugin = loadedPluginMap.get(packageName); - - if (plugin == null) { - LogUtil.e("正在初始化插件 " + packageName + ": Resources, DexClassLoader, Context, Application"); - - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); - - Resources pluginRes = PluginCreator.createPluginResource( - PluginLoader.getApplication().getApplicationInfo().sourceDir, - PluginLoader.getApplication().getResources(), pluginDescriptor); - - DexClassLoader pluginClassLoader = PluginCreator.createPluginClassLoader( - pluginDescriptor.getInstalledPath(), - pluginDescriptor.isStandalone(), - pluginDescriptor.getDependencies(), - pluginDescriptor.getMuliDexList()); - - Context pluginContext = PluginCreator.createPluginContext( - pluginDescriptor, - PluginLoader.getApplication().getBaseContext(), - pluginRes, - pluginClassLoader); - - //插件Context默认主题设置为插件application主题 - pluginContext.setTheme(pluginDescriptor.getApplicationTheme()); - - plugin = new LoadedPlugin(packageName, - pluginDescriptor.getInstalledPath(), - pluginContext, - pluginClassLoader); - - loadedPluginMap.put(packageName, plugin); - - Application pluginApplication = callPluginApplicationOnCreate(pluginContext, pluginClassLoader, pluginDescriptor); - - plugin.pluginApplication = pluginApplication;//这里之所以不放在LoadedPlugin的构造器里面,是因为contentprovider在安装时loadclass,造成死循环 - - try { - ActivityThread.installPackageInfo(PluginLoader.getApplication(), packageName, pluginDescriptor, - pluginClassLoader, pluginRes, pluginApplication); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - - LogUtil.e("初始化插件" + packageName + "完成"); - } else { - //LogUtil.d("IS RUNNING", packageName); - } - - return plugin; - } - - private Application callPluginApplicationOnCreate(Context pluginContext, DexClassLoader classLoader, PluginDescriptor pluginDescriptor) { - - Application application = null; - - try { - LogUtil.d("创建插件Application", pluginDescriptor.getApplicationName()); - //阻止自动安装multidex - try { - Class mulitDex = classLoader.loadClass("android.support.multidex.MultiDex"); - RefInvoker.setFieldObject(null, mulitDex, "IS_VM_MULTIDEX_CAPABLE", true); - } catch (Exception e) { - } - application = Instrumentation.newApplication(classLoader.loadClass(pluginDescriptor.getApplicationName()), - pluginContext); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - //安装ContentProvider, 在插件Application对象构造以后,oncreate调用之前 - PluginInjector.installContentProviders(PluginLoader.getApplication(), pluginDescriptor.getProviderInfos().values()); - - //执行onCreate - if (application != null) { - - ((PluginContextTheme)pluginContext).setPluginApplication(application); - - //先拿到宿主的crashHandler - Thread.UncaughtExceptionHandler old = Thread.getDefaultUncaughtExceptionHandler(); - - application.onCreate(); - - // 再还原宿主的crashHandler,这里之所以需要还原CrashHandler, - // 是因为如果插件中自己设置了自己的crashHandler(通常是在oncreate中), - // 会导致当前进程的主线程的handler被意外修改。 - // 如果有多个插件都有设置自己的crashHandler,也会导致混乱 - // 所以这里直接屏蔽掉插件的crashHandler - //TODO 或许也可以做成消息链进行分发? - Thread.setDefaultUncaughtExceptionHandler(old); - - if (Build.VERSION.SDK_INT >= 14) { - application.registerActivityLifecycleCallbacks(new LifecycleCallbackBrige()); - } - - } - - return application; - } - - public void stopPlugin(String packageName, PluginDescriptor pluginDescriptor) { - - LoadedPlugin plugin = getRunningPlugin(packageName); - - if (plugin == null) { - return; - } - // - //退出WebView, LocalService、Activity、BroadcastReceiver、LocalBroadcastManager, Service、AssetManager、ContentProvider、fragment - // - - //退出webview - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - AndroidWebkitWebViewFactoryProvider.switchWebViewContext(PluginLoader.getApplication()); - } - }); - - //退出LocalService - LocalServiceManager.unRegistService(pluginDescriptor); - - //退出Activity - PluginLoader.getApplication().sendBroadcast(new Intent(plugin.pluginPackageName + PluginActivityMonitor.ACTION_UN_INSTALL_PLUGIN)); - - //退出BroadcastReceiver - //广播一般有个注册方式 - //1、activity注册 - // 这种方式,在上一步Activitiy退出时会退出,所以不用处理 - //2、application注册 - // 这里需要处理这种方式注册的广播,这种方式注册的广播会被PluginContextTheme对象记录下来 - - ((PluginContextTheme) plugin.pluginApplication.getBaseContext()).unregisterAllReceiver(); - - //退出 LocalBroadcastManager - Object mInstance = RefInvoker.getStaticFieldObject("android.support.v4.content.LocalBroadcastManager", "mInstance"); - if (mInstance != null) { - HashMap> mReceivers = (HashMap>)RefInvoker.getFieldObject(mInstance, - "android.support.v4.content.LocalBroadcastManager", "mReceivers"); - if (mReceivers != null) { - Iterator ir = mReceivers.keySet().iterator(); - while(ir.hasNext()) { - BroadcastReceiver item = ir.next(); - if (item.getClass().getClassLoader() == plugin.pluginClassLoader) { - RefInvoker.invokeMethod(mInstance, "android.support.v4.content.LocalBroadcastManager", - "unregisterReceiver", new Class[]{BroadcastReceiver.class}, new Object[]{item}); - } - } - } - } - - //退出Service - //bindservie启动的service应该不需要处理,退出activity的时候会unbind - Map map = ActivityThread.getAllServices(); - if (map != null) { - Collection list = map.values(); - for (Service s :list) { - if (s.getClass().getClassLoader() == plugin.pluginClassLoader) { - s.stopSelf(); - } - } - } - - //退出AssetManager - //pluginDescriptor.getPluginContext().getResources().getAssets().close(); - - //退出ContentProvider - //TODO ContentProvider如何退出? - //ActivityThread.releaseProvider(IContentProvider provider, boolean stable) - - //退出fragment - //即退出由FragmentManager保存的Fragment - //TODO fragment如何退出? - - loadedPluginMap.remove(packageName); - } - - public boolean isRunning(String packageName) { - return loadedPluginMap.get(packageName) != null; - } - - static class LifecycleCallbackBrige implements android.app.Application.ActivityLifecycleCallbacks { - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - AndroidAppApplication.dispatchActivityCreated(PluginLoader.getApplication(), activity, savedInstanceState); - } - - @Override - public void onActivityStarted(Activity activity) { - AndroidAppApplication.dispatchActivityStarted(PluginLoader.getApplication(), activity); - } - - @Override - public void onActivityResumed(Activity activity) { - AndroidAppApplication.dispatchActivityResumed(PluginLoader.getApplication(), activity); - } - - @Override - public void onActivityPaused(Activity activity) { - AndroidAppApplication.dispatchActivityPaused(PluginLoader.getApplication(), activity); - } - - @Override - public void onActivityStopped(Activity activity) { - AndroidAppApplication.dispatchActivityStopped(PluginLoader.getApplication(), activity); - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - AndroidAppApplication.dispatchActivitySaveInstanceState(PluginLoader.getApplication(), activity, outState); - } - - @Override - public void onActivityDestroyed(Activity activity) { - AndroidAppApplication.dispatchActivityDestroyed(PluginLoader.getApplication(), activity); - } - } -} diff --git a/PluginCore/src/com/plugin/core/PluginLoader.java b/PluginCore/src/com/plugin/core/PluginLoader.java deleted file mode 100644 index d8d90ec4..00000000 --- a/PluginCore/src/com/plugin/core/PluginLoader.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.plugin.core; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.localservice.LocalServiceManager; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.core.systemservice.AndroidAppIActivityManager; -import com.plugin.core.systemservice.AndroidAppINotificationManager; -import com.plugin.core.systemservice.AndroidAppIPackageManager; -import com.plugin.core.systemservice.AndroidViewLayoutInflater; -import com.plugin.core.systemservice.AndroidWebkitWebViewFactoryProvider; -import com.plugin.core.systemservice.AndroidWidgetToast; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; - -import dalvik.system.DexClassLoader; - -public class PluginLoader { - - private static Application sApplication; - private static boolean isLoaderInited = false; - - private PluginLoader() { - } - - public static Application getApplication() { - return sApplication; - } - - /** - * 初始化loader, 只可调用一次 - * - * @param app - */ - public static synchronized void initLoader(Application app) { - - if (!isLoaderInited) { - LogUtil.d("插件框架初始化中..."); - isLoaderInited = true; - sApplication = app; - - AndroidAppIActivityManager.installProxy(); - AndroidAppINotificationManager.installProxy(); - AndroidAppIPackageManager.installProxy(sApplication.getPackageManager()); - - if (ProcessUtil.isPluginProcess()) { - AndroidWidgetToast.installProxy(); - AndroidViewLayoutInflater.installPluginCustomViewConstructorCache(); - //不可在主进程中同步安装,因为此时ActivityThread还没有准备好, 会导致空指针。 - new Handler().post(new Runnable() { - @Override - public void run() { - AndroidWebkitWebViewFactoryProvider.installProxy(); - } - }); - } - - PluginInjector.injectHandlerCallback(); - PluginInjector.injectInstrumentation(); - PluginInjector.injectBaseContext(sApplication); - - if (ProcessUtil.isPluginProcess()) { - Iterator itr = PluginManagerHelper.getPlugins().iterator(); - while (itr.hasNext()) { - PluginDescriptor plugin = itr.next(); - LocalServiceManager.registerService(plugin); - } - if (Build.VERSION.SDK_INT >= 14) { - sApplication.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - } - - @Override - public void onActivityStarted(Activity activity) { - } - - @Override - public void onActivityResumed(Activity activity) { - } - - @Override - public void onActivityPaused(Activity activity) { - } - - @Override - public void onActivityStopped(Activity activity) { - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - } - - @Override - public void onActivityDestroyed(Activity activity) { - Intent intent = activity.getIntent(); - if (intent != null && intent.getComponent() != null) { - PluginManagerHelper.unBindLaunchModeStubActivity(intent.getComponent().getClassName(), activity.getClass().getName()); - } - } - }); - } - } - LogUtil.d("插件框架初始化完成"); - } - } - - public static Context fixBaseContextForReceiver(Context superApplicationContext) { - if (superApplicationContext instanceof ContextWrapper) { - return ((ContextWrapper)superApplicationContext).getBaseContext(); - } else { - return superApplicationContext; - } - } - - - /** - * 根据插件中的classId加载一个插件中的class - * - * @param clazzId - * @return - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @SuppressWarnings("rawtypes") - public static Class loadPluginFragmentClassById(String clazzId) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByFragmentId(clazzId); - if (pluginDescriptor != null) { - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor.getPackageName()); - - DexClassLoader pluginClassLoader = plugin.pluginClassLoader; - - String clazzName = pluginDescriptor.getPluginClassNameById(clazzId); - if (clazzName != null) { - try { - Class pluginClazz = ((ClassLoader) pluginClassLoader).loadClass(clazzName); - LogUtil.d("loadPluginClass for clazzId", clazzId, "clazzName", clazzName, "success"); - return pluginClazz; - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - } - - LogUtil.e("loadPluginClass for clazzId", clazzId, "fail"); - - return null; - - } - - @SuppressWarnings("rawtypes") - public static Class loadPluginClassByName(String clazzName) { - - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(clazzName); - - if (pluginDescriptor != null) { - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginDescriptor.getPackageName()); - - DexClassLoader pluginClassLoader = plugin.pluginClassLoader; - - try { - Class pluginClazz = ((ClassLoader) pluginClassLoader).loadClass(clazzName); - LogUtil.d("loadPluginClass Success for clazzName ", clazzName); - return pluginClazz; - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (java.lang.IllegalAccessError illegalAccessError) { - illegalAccessError.printStackTrace(); - throw new IllegalAccessError("出现这个异常最大的可能是插件dex和" + - "宿主dex包含了相同的class导致冲突, " + - "请检查插件的编译脚本,确保排除了所有公共依赖库的jar"); - } - - } - - LogUtil.e("loadPluginClass Fail for clazzName ", clazzName); - - return null; - - } - - /** - * 获取当前class所在插件的Context - * 每个插件只有1个DefaultContext, - * 是当前插件中所有class公用的Context - * - * @param clazz - * @return - */ - public static Context getDefaultPluginContext(@SuppressWarnings("rawtypes") Class clazz) { - - Context pluginContext = null; - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(clazz.getName()); - - if (pluginDescriptor != null) { - pluginContext = PluginLauncher.instance().getRunningPlugin(pluginDescriptor.getPackageName()).pluginContext;; - } else { - LogUtil.e("PluginDescriptor Not Found for ", clazz.getName()); - } - - if (pluginContext == null) { - LogUtil.e("Context Not Found for ", clazz.getName()); - } - - return pluginContext; - } - - /** - * 根据当前插件的默认Context, 为当前插件的组件创建一个单独的context - * - * @param pluginContext - * @param base 由系统创建的Context。 其实际类型应该是ContextImpl - * @return - */ - /*package*/ static Context getNewPluginComponentContext(Context pluginContext, Context base, int theme) { - PluginContextTheme newContext = null; - if (pluginContext != null) { - newContext = (PluginContextTheme)PluginCreator.createPluginContext(((PluginContextTheme) pluginContext).getPluginDescriptor(), - base, pluginContext.getResources(), - (DexClassLoader) pluginContext.getClassLoader()); - - newContext.setPluginApplication((Application) ((PluginContextTheme) pluginContext).getApplicationContext()); - - newContext.setTheme(sApplication.getApplicationContext().getApplicationInfo().theme); - } - return newContext; - } - - public static Context getNewPluginApplicationContext(String pluginId) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginId); - - if (plugin != null) { - PluginContextTheme newContext = (PluginContextTheme)PluginCreator.createPluginContext( - ((PluginContextTheme) plugin.pluginContext).getPluginDescriptor(), - sApplication.getBaseContext(), plugin.pluginResource, plugin.pluginClassLoader); - - newContext.setPluginApplication(plugin.pluginApplication); - - newContext.setTheme(pluginDescriptor.getApplicationTheme()); - - - return newContext; - } - - return null; - } - - public static boolean isInstalled(String pluginId, String pluginVersion) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - if (pluginDescriptor != null) { - LogUtil.d(pluginId, pluginDescriptor.getVersion(), pluginVersion); - return pluginDescriptor.getVersion().equals(pluginVersion); - } - return false; - } - - /** - */ - public static ArrayList matchPlugin(Intent intent, int type) { - ArrayList result = null; - - String packageName = intent.getPackage(); - if (packageName == null && intent.getComponent() != null) { - packageName = intent.getComponent().getPackageName(); - } - if (packageName != null && !packageName.equals(PluginLoader.getApplication().getPackageName())) { - PluginDescriptor dp = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); - if (dp != null) { - List list = dp.matchPlugin(intent, type); - if (list != null && list.size() > 0) { - if (result == null) { - result = new ArrayList<>(); - } - result.addAll(list); - } - } - } else { - Iterator itr = PluginManagerHelper.getPlugins().iterator(); - while (itr.hasNext()) { - List list = itr.next().matchPlugin(intent, type); - if (list != null && list.size() > 0) { - if (result == null) { - result = new ArrayList<>(); - } - result.addAll(list); - } - if (result != null && type != PluginDescriptor.BROADCAST) { - break; - } - } - - } - return result; - } - -} diff --git a/PluginCore/src/com/plugin/core/PluginManifestParser.java b/PluginCore/src/com/plugin/core/PluginManifestParser.java deleted file mode 100644 index 8d1bbc00..00000000 --- a/PluginCore/src/com/plugin/core/PluginManifestParser.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.plugin.core; - -import android.app.Application; -import android.content.pm.ActivityInfo; - -import com.plugin.content.PluginActivityInfo; -import com.plugin.content.PluginDescriptor; -import com.plugin.content.PluginIntentFilter; -import com.plugin.content.PluginProviderInfo; -import com.plugin.util.LogUtil; -import com.plugin.util.ManifestReader; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class PluginManifestParser { - - public static PluginDescriptor parseManifest(String pluginPath) { - - try { - ZipFile zipFile = new ZipFile(new File(pluginPath), ZipFile.OPEN_READ); - ZipEntry manifestXmlEntry = zipFile.getEntry(ManifestReader.DEFAULT_XML); - String manifestXml = ManifestReader.getManifestXMLFromAPK(zipFile, manifestXmlEntry); - - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - XmlPullParser parser = factory.newPullParser(); - parser.setInput(new StringReader(manifestXml)); - int eventType = parser.getEventType(); - String namespaceAndroid = null; - String packageName = null; - - ArrayList dependencies = null; - - PluginDescriptor desciptor = new PluginDescriptor(); - do { - switch (eventType) { - case XmlPullParser.START_DOCUMENT: { - break; - } - case XmlPullParser.START_TAG: { - String tag = parser.getName(); - if ("manifest".equals(tag)) { - - namespaceAndroid = parser.getNamespace("android"); - - packageName = parser.getAttributeValue(null, "package"); - String versionCode = parser.getAttributeValue(namespaceAndroid, "versionCode"); - String versionName = parser.getAttributeValue(namespaceAndroid, "versionName"); - - //用这个字段来标记apk是独立apk,还是需要依赖主程序的class和resource - //当这个值等于宿主程序packageName时,则认为这个插件是需要依赖宿主的class和resource的 - String sharedUserId = parser.getAttributeValue(namespaceAndroid, "sharedUserId"); - - desciptor.setPackageName(packageName); - desciptor.setVersion(versionName + "_" + versionCode); - - desciptor.setStandalone(sharedUserId == null || !PluginLoader.getApplication().getPackageName().equals(sharedUserId)); - - LogUtil.d(packageName, versionCode, versionName, sharedUserId); - } else if ("meta-data".equals(tag)) { - - String name = parser.getAttributeValue(namespaceAndroid, "name"); - String value = parser.getAttributeValue(namespaceAndroid, "value"); - - if (name != null) { - -// HashMap metaData = desciptor.getMetaData(); -// if (metaData == null) { -// metaData = new HashMap(); -// desciptor.setMetaData(metaData); -// } -// if (value != null && value.startsWith("@") && value.length() == 9) { -// String idHex = value.replace("@", ""); -// try { -// int id = Integer.parseInt(idHex, 16); -// value = Integer.toString(id); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } -// metaData.put(name, value); - - LogUtil.d("meta-data", name, value); - - } - - } else if ("exported-fragment".equals(tag)) { - - String name = parser.getAttributeValue(namespaceAndroid, "name"); - String value = parser.getAttributeValue(namespaceAndroid, "value"); - - if (name != null) { - - HashMap fragments = desciptor.getFragments(); - if (fragments == null) { - fragments = new HashMap(); - desciptor.setfragments(fragments); - } - fragments.put(name, value); - LogUtil.d(name, value); - - } - - } else if ("exported-service".equals(tag)) { - - String name = parser.getAttributeValue(namespaceAndroid, "name"); - String value = parser.getAttributeValue(namespaceAndroid, "value"); - - if (name != null) { - - HashMap functions = desciptor.getFunctions(); - if (functions == null) { - functions = new HashMap(); - desciptor.setFunctions(functions); - } - functions.put(name, value); - LogUtil.d(name, value); - - } - - } else if ("uses-library".equals(tag)) { - - String name = parser.getAttributeValue(namespaceAndroid, "name"); - - if (dependencies == null) { - dependencies = new ArrayList(); - } - dependencies.add(name); - - } else if ("application".equals(tag)) { - - String applicationName = parser.getAttributeValue(namespaceAndroid, "name"); - if (applicationName == null) { - applicationName = Application.class.getName(); - } - applicationName = getName(applicationName, packageName); - desciptor.setApplicationName(applicationName); - - desciptor.setDescription(parser.getAttributeValue(namespaceAndroid, "label")); - - //这里不解析主题,后面会通过packageManager查询 - - LogUtil.d("applicationName", applicationName, " Description ", desciptor.getDescription()); - - } else if ("activity".equals(tag)) { - - String windowSoftInputMode = parser.getAttributeValue(namespaceAndroid, "windowSoftInputMode");//strin - String hardwareAccelerated = parser.getAttributeValue(namespaceAndroid, "hardwareAccelerated");//int string - String launchMode = parser.getAttributeValue(namespaceAndroid, "launchMode");//string - String screenOrientation = parser.getAttributeValue(namespaceAndroid, "screenOrientation");//string - String theme = parser.getAttributeValue(namespaceAndroid, "theme");//int - String immersive = parser.getAttributeValue(namespaceAndroid, "immersive");//int string - String uiOptions = parser.getAttributeValue(namespaceAndroid, "uiOptions");//int string - - HashMap> map = desciptor.getActivitys(); - if (map == null) { - map = new HashMap>(); - desciptor.setActivitys(map); - } - String name = addIntentFilter(map, packageName, namespaceAndroid, parser, "activity"); - - HashMap infos = desciptor.getActivityInfos(); - if (infos == null) { - infos = new HashMap(); - desciptor.setActivityInfos(infos); - } - - PluginActivityInfo pluginActivityInfo = infos.get(name); - if (pluginActivityInfo == null) { - pluginActivityInfo = new PluginActivityInfo(); - infos.put(name, pluginActivityInfo); - } - pluginActivityInfo.setHardwareAccelerated(hardwareAccelerated); - pluginActivityInfo.setImmersive(immersive); - if (launchMode == null) { - launchMode = String.valueOf(ActivityInfo.LAUNCH_MULTIPLE); - } - pluginActivityInfo.setLaunchMode(launchMode); - pluginActivityInfo.setName(name); - pluginActivityInfo.setScreenOrientation(screenOrientation); - pluginActivityInfo.setTheme(theme); - pluginActivityInfo.setWindowSoftInputMode(windowSoftInputMode); - pluginActivityInfo.setUiOptions(uiOptions); - - } else if ("receiver".equals(tag)) { - - HashMap> map = desciptor.getReceivers(); - if (map == null) { - map = new HashMap>(); - desciptor.setReceivers(map); - } - addIntentFilter(map, packageName, namespaceAndroid, parser, "receiver"); - - } else if ("service".equals(tag)) { - - HashMap> map = desciptor.getServices(); - if (map == null) { - map = new HashMap>(); - desciptor.setServices(map); - } - addIntentFilter(map, packageName, namespaceAndroid, parser, "service"); - - } else if ("provider".equals(tag)) { - - String name = parser.getAttributeValue(namespaceAndroid, "name"); - String author = parser.getAttributeValue(namespaceAndroid, "authorities"); - String exported = parser.getAttributeValue(namespaceAndroid, "exported"); - HashMap providers = desciptor.getProviderInfos(); - if (providers == null) { - providers = new HashMap(); - desciptor.setProviderInfos(providers); - } - - PluginProviderInfo info = new PluginProviderInfo(); - info.setName(name); - info.setExported(Boolean.getBoolean(exported)); - info.setAuthority(author); - - providers.put(name, info); - } - break; - } - case XmlPullParser.END_TAG: { - break; - } - } - eventType = parser.next(); - } while (eventType != XmlPullParser.END_DOCUMENT); - - desciptor.setEnabled(true); - - //有可能没有配置application节点,这里需要检查一下application - if (desciptor.getApplicationName() == null) { - desciptor.setApplicationName(Application.class.getName()); - } - - if (dependencies != null) { - desciptor.setDependencies((String[])dependencies.toArray(new String[0])); - } - - return desciptor; - } catch (XmlPullParserException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - private static String addIntentFilter(HashMap> map, String packageName, String namespace, - XmlPullParser parser, String endTagName) throws XmlPullParserException, IOException { - int eventType = parser.getEventType(); - String activityName = parser.getAttributeValue(namespace, "name"); - activityName = getName(activityName, packageName); - - ArrayList filters = map.get(activityName); - if (filters == null) { - filters = new ArrayList(); - map.put(activityName, filters); - } - - PluginIntentFilter intentFilter = new PluginIntentFilter(); - do { - switch (eventType) { - case XmlPullParser.START_TAG: { - String tag = parser.getName(); - if ("intent-filter".equals(tag)) { - intentFilter = new PluginIntentFilter(); - filters.add(intentFilter); - } else { - intentFilter.readFromXml(tag, parser); - } - } - } - eventType = parser.next(); - } while (!endTagName.equals(parser.getName()));//再次到达,表示一个标签结束了 - - return activityName; - } - - private static String getName(String nameOrig, String pkgName) { - if (nameOrig == null) { - return null; - } - StringBuilder sb = null; - if (nameOrig.startsWith(".")) { - sb = new StringBuilder(); - sb.append(pkgName); - sb.append(nameOrig); - } else if (!nameOrig.contains(".")) { - sb = new StringBuilder(); - sb.append(pkgName); - sb.append('.'); - sb.append(nameOrig); - } else { - return nameOrig; - } - return sb.toString(); - } - -} diff --git a/PluginCore/src/com/plugin/core/PluginPublicXmlConst.java b/PluginCore/src/com/plugin/core/PluginPublicXmlConst.java deleted file mode 100644 index e0c5a8f4..00000000 --- a/PluginCore/src/com/plugin/core/PluginPublicXmlConst.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.plugin.core; - -import android.util.SparseArray; - -/** - * Consts in public.xml - */ -public class PluginPublicXmlConst { - public static final int public_static_final_host_attr = 0x7f01; - public static final int public_static_final_host_drawable = 0x7f32; - public static final int public_static_final_host_layout = 0x7f33; - public static final int public_static_final_host_anim = 0x7f34; - public static final int public_static_final_host_xml = 0x7f35; - public static final int public_static_final_host_raw = 0x7f36; - public static final int public_static_final_host_dimen = 0x7f37; - public static final int public_static_final_host_string = 0x7f38; - public static final int public_static_final_host_style = 0x7f39; - public static final int public_static_final_host_color = 0x7f3a; - public static final int public_static_final_host_id = 0x7f3b; - public static final int public_static_final_host_bool = 0x7f3c; - public static final int public_static_final_host_int = 0x7f3d; - public static final int public_static_final_host_array = 0x7f3e; - public static final int public_static_final_host_menu = 0x7f3f; - public static final int public_static_final_host_mipmap = 0x7f40; - - public static SparseArray resourceMap = new SparseArray(16); - - static { - resourceMap.put(public_static_final_host_attr, "attr"); - resourceMap.put(public_static_final_host_drawable, "drawable"); - resourceMap.put(public_static_final_host_layout, "layout"); - resourceMap.put(public_static_final_host_anim, "anim"); - resourceMap.put(public_static_final_host_xml, "xml"); - resourceMap.put(public_static_final_host_raw, "raw"); - resourceMap.put(public_static_final_host_dimen, "dimen"); - resourceMap.put(public_static_final_host_string, "string"); - resourceMap.put(public_static_final_host_style, "style"); - resourceMap.put(public_static_final_host_color, "color"); - resourceMap.put(public_static_final_host_id, "id"); - resourceMap.put(public_static_final_host_bool, "bool"); - resourceMap.put(public_static_final_host_int, "int"); - resourceMap.put(public_static_final_host_array, "array"); - resourceMap.put(public_static_final_host_menu, "menu"); - resourceMap.put(public_static_final_host_mipmap, "mipmap"); - } -} diff --git a/PluginCore/src/com/plugin/core/PluginResourceWrapper.java b/PluginCore/src/com/plugin/core/PluginResourceWrapper.java deleted file mode 100644 index b016c10b..00000000 --- a/PluginCore/src/com/plugin/core/PluginResourceWrapper.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.plugin.core; - -import android.content.res.AssetManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.text.TextUtils; -import android.util.DisplayMetrics; - -import com.plugin.content.PluginDescriptor; -import com.plugin.util.LogUtil; -import com.plugin.util.ResourceUtil; - -import java.lang.reflect.Field; - -/** - * 根据不同的rom,可能需要重写更多的方法,目前发现的几个机型的问题暂时只需要重写下面2个方法。 - * @author cailiming - * - */ -public class PluginResourceWrapper extends Resources { - - private PluginDescriptor mPluginDescriptor; - - public PluginResourceWrapper(AssetManager assets, DisplayMetrics metrics, - Configuration config, PluginDescriptor pluginDescriptor) { - super(assets, metrics, config); - this.mPluginDescriptor = pluginDescriptor; - } - - @Override - public String getResourcePackageName(int resid) throws NotFoundException { - try { - return super.getResourcePackageName(resid); - } catch(NotFoundException e) { - LogUtil.e("NotFoundException Try Following", Integer.toHexString(resid)); - - //就目前测试的情况来看,只有Coolpad、vivo、oppo等手机会在上面抛异常,走到这里来, - //华为、三星、小米等手机不会到这里来。 - if (ResourceUtil.isMainResId(resid)) { - return PluginLoader.getApplication().getPackageName(); - } - throw new NotFoundException("Unable to find resource ID #0x" - + Integer.toHexString(resid)); - } - } - - @Override - public String getResourceName(int resid) throws NotFoundException { - try { - return super.getResourceName(resid); - } catch(NotFoundException e) { - LogUtil.e("NotFoundException Try Following"); - - //就目前测试的情况来看,只有Coolpad、vivo、oppo等手机会在上面抛异常,走到这里来, - //华为、三星、小米等手机不会到这里来。 - if (ResourceUtil.isMainResId(resid)) { - return PluginLoader.getApplication().getResources().getResourceName(resid); - } - throw new NotFoundException("Unable to find resource ID #0x" - + Integer.toHexString(resid)); - } - } - - /** - * 重写此方法主要是为了修正在插件中通过 - * getIdentifier(name, type, getPackageName())此中形式反查id时 - * - * 如果第三个参数通过调用getPackageName获得,由于此方法返回的是宿主的包名,可能会得不到预期的结果 - * - * @param name - * @param defType - * @param defPackage - * @return - */ - @Override - public int getIdentifier(String name, String defType, String defPackage) { - - if (TextUtils.isDigitsOnly(name)) { - return super.getIdentifier(name, defType, defPackage); - } - - //传了packageName,而且不是宿主的packageName, 则直接返回 - if (!TextUtils.isEmpty(defPackage) && !PluginLoader.getApplication().getPackageName().equals(defPackage)) { - return super.getIdentifier(name, defType, defPackage); - } - - //package:type/entry - //第一段 “package:“ 第二段 ”type/“ 第三段 “entry” - String packageName = null; - String type = null; - String entry = null; - - String[] pte = name.split(":"); - String[] te; - if (pte.length == 2) { - packageName = pte[0]; - te = pte[1].split("/"); - } else { - te = pte[0].split("/"); - } - - if (te.length == 2) { - type = te[0]; - entry = te[1]; - } else { - entry = te[0]; - } - - if (packageName == null) { - packageName = defPackage; - } - - if (type == null) { - type = defType; - } - - if (PluginLoader.getApplication().getPackageName().equals(packageName)) { - if (mPluginDescriptor.isStandalone()) { - packageName = mPluginDescriptor.getPackageName(); - } else { - // 判断是否在真的在宿主中 - Class rClass = null; - try { - String className = packageName + ".R$" + type; - rClass = this.getClass().getClassLoader().loadClass(className); - Field field = rClass.getDeclaredField(entry); - if (field == null) { - //不在宿主中,换成插件的 - packageName = mPluginDescriptor.getPackageName(); - } - } catch (Exception e) { - //不在宿主中,换成插件的 - packageName = mPluginDescriptor.getPackageName(); - } - } - } - return super.getIdentifier(entry, type, packageName); - - } -} - diff --git a/PluginCore/src/com/plugin/core/PluginThemeHelper.java b/PluginCore/src/com/plugin/core/PluginThemeHelper.java deleted file mode 100644 index 5ae879e6..00000000 --- a/PluginCore/src/com/plugin/core/PluginThemeHelper.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.plugin.core; - -import android.app.Activity; -import android.content.Context; -import android.view.LayoutInflater; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; - -import java.lang.reflect.Field; -import java.util.HashMap; - -public class PluginThemeHelper { - - public static int getPluginThemeIdByName(String pluginId, String themeName) { - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - if (pd != null) { - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginId); - - if (plugin != null) { - return plugin.pluginResource.getIdentifier(themeName, "style", pd.getPackageName()); - } - } - return 0; - } - - public static HashMap getAllPluginThemes(String pluginId) { - HashMap themes = new HashMap(); - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - if (pd != null) { - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin pluing = PluginLauncher.instance().startPlugin(pluginId); - - try { - Class pluginRstyle = pluing.pluginClassLoader.loadClass(pluginId + ".R$style"); - if (pluginRstyle != null) { - Field[] fields = pluginRstyle.getDeclaredFields(); - if (fields != null) { - for (Field field : - fields) { - field.setAccessible(true); - int themeResId = field.getInt(null); - themes.put(field.getName(), themeResId); - } - } - } - - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - - - return themes; - } - - /** - * Used by host for skin - * 宿主程序使用插件主题 - */ - public static void applyPluginTheme(Activity activity, String pluginId, int themeResId) { - - LayoutInflater layoutInflater = LayoutInflater.from(activity); - if (layoutInflater.getFactory() == null) { - if (!(activity.getBaseContext() instanceof PluginContextTheme)) { - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - if (pd != null) { - - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin pluing = PluginLauncher.instance().startPlugin(pluginId); - - //注入插件上下文和主题 - Context defaultContext = pluing.pluginContext; - Context pluginContext = PluginLoader.getNewPluginComponentContext(defaultContext, - ((PluginBaseContextWrapper)activity.getBaseContext()).getBaseContext(), 0); - PluginInjector.resetActivityContext(pluginContext, activity, themeResId); - - } - } - } else { - //启用了控件级插件的页面 不能使用换肤功能呢 - //参见注解ComponentContainer - //还有一个判断方式是通过注解来判断 - LogUtil.e("启用了控件级插件的页面 不能使用换肤功能呢"); - } - - } - - /** - * Used by plugin for Theme - * 插件使用插件主题 - */ - public static void setTheme(Context pluginContext, int resId) { - if (pluginContext instanceof PluginContextTheme) { - ((PluginContextTheme)pluginContext).mTheme = null; - pluginContext.setTheme(resId); - } - } - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/PluginViewCreator.java b/PluginCore/src/com/plugin/core/PluginViewCreator.java deleted file mode 100644 index 629b6aed..00000000 --- a/PluginCore/src/com/plugin/core/PluginViewCreator.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.plugin.core; - -import android.content.Context; -import android.content.ContextWrapper; -import android.util.AttributeSet; -import android.view.InflateException; -import android.view.LayoutInflater; -import android.view.View; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.LogUtil; - -import java.lang.reflect.Constructor; - -/** - * 控件级插件的实现原理 - * - * @author cailiming - * - */ -public class PluginViewCreator implements LayoutInflater.Factory { - - @Override - public View onCreateView(String name, Context context, AttributeSet attrs) { - - //可以在这里全局替换控件类型 - if ("TextView".equals(name)) { - //return new CustomTextView(); - } else if ("ImageView".equals(name)) { - //return new CustomImageView(); - } - - return createViewFromTag(context, name, attrs); - - } - - private View createViewFromTag(Context context, String name, AttributeSet attrs) { - if (name.equals("pluginView")) { - name = attrs.getAttributeValue(null, "class"); - String pluginId = attrs.getAttributeValue(null, "context"); - try { - View view = createView(context, pluginId, name, attrs); - if (view != null) { - return view; - } - } catch (Exception e) { - } finally { - } - - View view = new View(context, attrs); - view.setVisibility(View.GONE); - return view; - } - - return null; - } - - private View createView(Context Context, String pluginId, String name, AttributeSet atts) - throws ClassNotFoundException, InflateException { - try { - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByPluginId(pluginId); - - if (pd != null) { - - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginId); - - Context baseContext = Context; - if (!(baseContext instanceof PluginContextTheme)) { - baseContext = ((ContextWrapper)baseContext).getBaseContext(); - } - if (baseContext instanceof PluginContextTheme) { - baseContext = ((PluginContextTheme) baseContext).getBaseContext(); - } - Context pluginViewContext = PluginLoader.getNewPluginComponentContext(plugin.pluginContext, baseContext, pd.getApplicationTheme()); - Class clazz = pluginViewContext.getClassLoader() - .loadClass(name).asSubclass(View.class); - - Constructor constructor = clazz.getConstructor(new Class[] { - Context.class, AttributeSet.class}); - constructor.setAccessible(true); - return constructor.newInstance(new Object[]{pluginViewContext , atts}); - } else { - LogUtil.e("未找到插件" + pluginId + ",请确认是否已安装"); - } - } catch (Exception e) { - return null; - } - return null; - } - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/annotation/AnnotationProcessor.java b/PluginCore/src/com/plugin/core/annotation/AnnotationProcessor.java deleted file mode 100644 index fe74dcb4..00000000 --- a/PluginCore/src/com/plugin/core/annotation/AnnotationProcessor.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.plugin.core.annotation; - -import java.lang.annotation.Annotation; - -public class AnnotationProcessor { - - public static FragmentContainer getFragmentContainer(Class clazz) { - FragmentContainer fragmentContainer = (FragmentContainer)clazz.getAnnotation(FragmentContainer.class); - return fragmentContainer; - } - - public static ComponentContainer getComponentContainer(Class clazz) { - ComponentContainer componentContainer = (ComponentContainer)clazz.getAnnotation(ComponentContainer.class); - return componentContainer; - } - -} diff --git a/PluginCore/src/com/plugin/core/annotation/ComponentContainer.java b/PluginCore/src/com/plugin/core/annotation/ComponentContainer.java deleted file mode 100644 index 35bd16f9..00000000 --- a/PluginCore/src/com/plugin/core/annotation/ComponentContainer.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.plugin.core.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 在宿主程序的activity中标记这个注解, - * 用来通知插件框架,宿主的这个activity也需要替换上下文,用来嵌入来自插件的View - */ - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface ComponentContainer { -} diff --git a/PluginCore/src/com/plugin/core/annotation/FragmentContainer.java b/PluginCore/src/com/plugin/core/annotation/FragmentContainer.java deleted file mode 100644 index 42611723..00000000 --- a/PluginCore/src/com/plugin/core/annotation/FragmentContainer.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.plugin.core.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 在宿主程序的activity中标记这个注解, - * 用来通知插件框架,宿主的这个activity也需要替换上下文,用来嵌入来自插件的fragment - * - * 如果是插件自身提供的fragment容器来嵌入来自插件自身的fragment,无需添加此标记。 - */ - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface FragmentContainer { - public String pluginId() default ""; - public String fragmentId() default ""; -} diff --git a/PluginCore/src/com/plugin/core/app/ActivityThread.java b/PluginCore/src/com/plugin/core/app/ActivityThread.java deleted file mode 100644 index 90f06423..00000000 --- a/PluginCore/src/com/plugin/core/app/ActivityThread.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.plugin.core.app; - -import android.app.Application; -import android.app.Instrumentation; -import android.app.Service; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.content.res.Resources; -import android.os.Handler; -import android.os.IBinder; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginAppTrace; -import com.plugin.core.PluginInstrumentionWrapper; -import com.plugin.core.PluginLoader; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.io.File; -import java.util.List; -import java.util.Map; - -public class ActivityThread { - - private static final String android_app_ActivityThread = "android.app.ActivityThread"; - private static final String android_app_ActivityThread_currentActivityThread = "currentActivityThread"; - private static final String android_app_ActivityThread_mInstrumentation = "mInstrumentation"; - private static final String android_app_ActivityThread_getHandler = "getHandler"; - private static final String android_app_ActivityThread_installContentProviders = "installContentProviders"; - private static final String android_app_ActivityThread_AppBindData = "android.app.ActivityThread$AppBindData"; - private static final String android_app_ActivityThread_mServices = "mServices"; - - private static final String android_os_Handler_mCallback = "mCallback"; - - private static final String android_app_ContextImpl = "android.app.ContextImpl"; - private static final String android_app_ContextImpl_getImpl = "getImpl"; - private static final String android_app_ContextImpl_mMainThread = "mMainThread"; - - private static Object sCurrentActivityThread; - private static Class sClass; - - public static Class clazz() { - if (sClass == null) { - try { - sClass = Class.forName(android_app_ActivityThread); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - return sClass; - } - - public synchronized static Object currentActivityThread() { - if (sCurrentActivityThread == null) { - // 从ThreadLocal中取出来的 - LogUtil.d("从宿主程序中取出ActivityThread对象备用"); - sCurrentActivityThread = RefInvoker.invokeStaticMethod(android_app_ActivityThread, - android_app_ActivityThread_currentActivityThread, - (Class[]) null, (Object[]) null); - - //有些情况下上面的方法拿不到,下面再换个方法尝试一次 - if (sCurrentActivityThread == null) { - Object impl = RefInvoker.invokeStaticMethod(android_app_ContextImpl, android_app_ContextImpl_getImpl, - new Class[]{Context.class}, new Object[]{PluginLoader.getApplication()}); - if (impl != null) { - sCurrentActivityThread = RefInvoker.getFieldObject(impl, android_app_ContextImpl, android_app_ContextImpl_mMainThread); - } - } - } - return sCurrentActivityThread; - } - - public static Object getResCompatibilityInfo() { - //貌似没啥用 - Object mBoundApplication = RefInvoker.getFieldObject(currentActivityThread(), android_app_ActivityThread, "mBoundApplication"); - Object compatInfo = RefInvoker.getFieldObject(mBoundApplication, android_app_ActivityThread_AppBindData, "compatInfo"); - return compatInfo; - } - - public static void installContentProviders(Context context, List providers) { - RefInvoker.invokeMethod(currentActivityThread(), - clazz(), android_app_ActivityThread_installContentProviders, - new Class[]{Context.class, List.class}, new Object[]{context, providers}); - } - - public static void wrapHandler() { - Handler handler = (Handler)RefInvoker.invokeMethod(currentActivityThread(), - clazz(), android_app_ActivityThread_getHandler, - (Class[]) null, (Object[]) null); - RefInvoker.setFieldObject(handler, Handler.class.getName(), android_os_Handler_mCallback, - new PluginAppTrace(handler)); - } - - public static void wrapInstrumentation() { - Instrumentation originalInstrumentation = (Instrumentation) RefInvoker.getFieldObject(currentActivityThread(), - clazz(), android_app_ActivityThread_mInstrumentation); - if (!(originalInstrumentation instanceof PluginInstrumentionWrapper)) { - RefInvoker.setFieldObject(currentActivityThread(), clazz(), - android_app_ActivityThread_mInstrumentation, - new PluginInstrumentionWrapper(originalInstrumentation)); - } - } - - //getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, - // ClassLoader baseLoader, boolean securityViolation, boolean includeCode) - - -// synchronized (mResourcesManager) { -// 1834 WeakReference ref; -// 1835 if (includeCode) { -// 1836 ref = mPackages.get(aInfo.packageName); -// 1837 } else { -// 1838 ref = mResourcePackages.get(aInfo.packageName); -// 1839 } -// 1840 LoadedApk packageInfo = ref != null ? ref.get() : null; -// 1841 if (packageInfo == null || (packageInfo.mResources != null -// 1842 && !packageInfo.mResources.getAssets().isUpToDate())) { -// 1843 if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package " -// 1844 : "Loading resource-only package ") + aInfo.packageName -// 1845 + " (in " + (mBoundApplication != null -// 1846 ? mBoundApplication.processName : null) -// 1847 + ")"); -// 1848 packageInfo = -// 1849 new LoadedApk(this, aInfo, compatInfo, this, baseLoader, -// 1850 securityViolation, includeCode && -// 1851 (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0); -// 1852 if (includeCode) { -// 1853 mPackages.put(aInfo.packageName, -// 1854 new WeakReference(packageInfo)); -// 1855 } else { -// 1856 mResourcePackages.put(aInfo.packageName, -// 1857 new WeakReference(packageInfo)); -// 1858 } -// 1859 } -// 1860 return packageInfo; -// 1861 } - - -//LoadedApk 构造器 -// mActivityThread = activityThread; -// 117 mApplicationInfo = aInfo; -// 118 mPackageName = aInfo.packageName; -// 119 mAppDir = aInfo.sourceDir; -// 120 final int myUid = Process.myUid(); -// 121 mResDir = aInfo.uid == myUid ? aInfo.sourceDir -// 122 : aInfo.publicSourceDir; -// 123 if (!UserHandle.isSameUser(aInfo.uid, myUid) && !Process.isIsolated()) { -// 124 aInfo.dataDir = PackageManager.getDataDirForUser(UserHandle.getUserId(myUid), -// 125 mPackageName); -// 126 } -// 127 mSharedLibraries = aInfo.sharedLibraryFiles; -// 128 mDataDir = aInfo.dataDir; -// 129 mDataDirFile = mDataDir != null ? new File(mDataDir) : null; -// 130 mLibDir = aInfo.nativeLibraryDir; -// 131 mBaseClassLoader = baseLoader; -// 132 mSecurityViolation = securityViolation; -// 133 mIncludeCode = includeCode; -// 134 mDisplayAdjustments.setCompatibilityInfo(compatInfo); -// 135 -// 136 if (mAppDir == null) { -// 137 if (ActivityThread.mSystemContext == null) { -// 138 ActivityThread.mSystemContext = -// 139 ContextImpl.createSystemContext(mainThread); -// 140 ResourcesManager resourcesManager = ResourcesManager.getInstance(); -// 141 ActivityThread.mSystemContext.getResources().updateConfiguration( -// 142 resourcesManager.getConfiguration(), -// 143 resourcesManager.getDisplayMetricsLocked( -// 144 Display.DEFAULT_DISPLAY, mDisplayAdjustments), compatInfo); -// 145 //Slog.i(TAG, "Created system resources " -// 146 // + mSystemContext.getResources() + ": " -// 147 // + mSystemContext.getResources().getConfiguration()); -// 148 } -// 149 mClassLoader = ActivityThread.mSystemContext.getClassLoader(); -// 150 mResources = ActivityThread.mSystemContext.getResources(); -// 151 } -// -// ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent(); -// 39 -// 40 synchronized (mLoaders) { -// 41 if (parent == null) { -// 42 parent = baseParent; -// 43 } -// - public static void installPackageInfo(Context hostContext, String pluginId, PluginDescriptor pluginDescriptor, - ClassLoader pluginClassLoader, Resources pluginResource, - Application pluginApplication) throws ClassNotFoundException { - - Object applicationLoaders = RefInvoker.invokeStaticMethod("android.app.ApplicationLoaders", "getDefault", (Class[]) null, (Object[]) null); - Map mLoaders = (Map)RefInvoker.getFieldObject(applicationLoaders, "android.app.ApplicationLoaders", "mLoaders"); - - mLoaders.put(pluginDescriptor.getInstalledPath(), pluginClassLoader); - try { - ApplicationInfo info = hostContext.getPackageManager().getApplicationInfo(pluginId, PackageManager.GET_SHARED_LIBRARY_FILES); - Object compatibilityInfo = getResCompatibilityInfo();//Not Sure - //先保存 - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - //会触发替换 - Object pluginLoadedApk = RefInvoker.invokeMethod(currentActivityThread(), android_app_ActivityThread, "getPackageInfoNoCheck", - new Class[]{ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")}, - new Object[]{info, compatibilityInfo}); - if (pluginLoadedApk != null) { - Class loadedAPKClass = pluginLoadedApk.getClass(); - RefInvoker.setFieldObject(pluginLoadedApk, loadedAPKClass, "mApplication", pluginApplication); - RefInvoker.setFieldObject(pluginLoadedApk, loadedAPKClass, "mResources", pluginResource); - RefInvoker.setFieldObject(pluginLoadedApk, loadedAPKClass, "mDataDirFile", new File(PluginLoader.getApplication().getApplicationInfo().dataDir)); - RefInvoker.setFieldObject(pluginLoadedApk, loadedAPKClass, "mDataDir", PluginLoader.getApplication().getApplicationInfo().dataDir); - //TODO 需要时再说 - //RefInvoker.setFieldObject(pluginLoadedApk, loadedAPKClass, "mLibDir", ); - } - //再还原 - Thread.currentThread().setContextClassLoader(classLoader); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - } - - public static Map getAllServices() { - Map services = (Map)RefInvoker.getFieldObject(currentActivityThread(), android_app_ActivityThread, android_app_ActivityThread_mServices); - return services; - } - -} diff --git a/PluginCore/src/com/plugin/core/app/AndroidAppApplication.java b/PluginCore/src/com/plugin/core/app/AndroidAppApplication.java deleted file mode 100644 index 671293e8..00000000 --- a/PluginCore/src/com/plugin/core/app/AndroidAppApplication.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.plugin.core.app; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; - -import com.plugin.util.RefInvoker; - -/** - * Created by cailiming on 16/3/11. - */ -public class AndroidAppApplication { - - public static void dispatchActivityCreated(Application application, Activity activity, Bundle savedInstanceState) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityCreated", new Class[]{Activity.class, Bundle.class}, new Object[]{activity, savedInstanceState}); - } - - public static void dispatchActivityStarted(Application application, Activity activity) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityStarted", new Class[]{Activity.class}, new Object[]{activity}); - } - - public static void dispatchActivityResumed(Application application, Activity activity) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityResumed", new Class[]{Activity.class}, new Object[]{activity}); - } - - public static void dispatchActivityPaused(Application application, Activity activity) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityPaused", new Class[]{Activity.class}, new Object[]{activity}); - } - - public static void dispatchActivityStopped(Application application, Activity activity) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityStopped", new Class[]{Activity.class}, new Object[]{activity}); - } - - public static void dispatchActivitySaveInstanceState(Application application, Activity activity, Bundle outState) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivitySaveInstanceState", new Class[]{Activity.class, Bundle.class}, new Object[]{activity, outState}); - } - - public static void dispatchActivityDestroyed(Application application, Activity activity) { - RefInvoker.invokeMethod(application, Application.class, "dispatchActivityDestroyed", new Class[]{Activity.class}, new Object[]{activity}); - - } -} diff --git a/PluginCore/src/com/plugin/core/localservice/LocalServiceFetcher.java b/PluginCore/src/com/plugin/core/localservice/LocalServiceFetcher.java deleted file mode 100644 index 0ca131ff..00000000 --- a/PluginCore/src/com/plugin/core/localservice/LocalServiceFetcher.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.plugin.core.localservice; - -/** - * Created by cailiming on 16/1/1. - */ -public abstract class LocalServiceFetcher { - int mServiceId; - String mPluginId; - private Object mCachedInstance; - - public final Object getService() { - synchronized (LocalServiceFetcher.this) { - Object service = mCachedInstance; - if (service != null) { - return service; - } - return mCachedInstance = createService(mServiceId); - } - } - - public abstract Object createService(int serviceId); - -} diff --git a/PluginCore/src/com/plugin/core/localservice/LocalServiceManager.java b/PluginCore/src/com/plugin/core/localservice/LocalServiceManager.java deleted file mode 100644 index 0e20098d..00000000 --- a/PluginCore/src/com/plugin/core/localservice/LocalServiceManager.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.plugin.core.localservice; - -import com.plugin.content.LoadedPlugin; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLauncher; -import com.plugin.util.LogUtil; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Created by cailiming on 16/1/1. - */ -public class LocalServiceManager { - - private static final HashMap SYSTEM_SERVICE_MAP = - new HashMap(); - - private LocalServiceManager() { - } - - public static void registerService(PluginDescriptor plugin) { - HashMap localServices = plugin.getFunctions(); - if (localServices != null) { - Iterator> serv = localServices.entrySet().iterator(); - while (serv.hasNext()) { - Map.Entry entry = serv.next(); - LocalServiceManager.registerService(plugin.getPackageName(), entry.getKey(), entry.getValue()); - } - } - } - - public static void registerService(final String pluginId, String serviceName, final String serviceClass) { - if (!SYSTEM_SERVICE_MAP.containsKey(serviceName)) { - LocalServiceFetcher fetcher = new LocalServiceFetcher() { - @Override - public Object createService(int serviceId) { - mPluginId = pluginId; - - //插件可能尚未初始化,确保使用前已经初始化 - LoadedPlugin plugin = PluginLauncher.instance().startPlugin(pluginId); - - if (plugin != null) { - try { - Class clazz = plugin.pluginClassLoader.loadClass(serviceClass); - return clazz.newInstance(); - } catch (Exception e) { - LogUtil.printException("获取服务失败", e); - } - } else { - LogUtil.e("PluginClassLoader", "未找到插件", pluginId); - } - return null; - } - }; - fetcher.mServiceId ++; - SYSTEM_SERVICE_MAP.put(serviceName, fetcher); - LogUtil.d("registerService", serviceName); - } else { - LogUtil.e("已注册", serviceName); - } - } - - public static Object getService(String name) { - LocalServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); - return fetcher == null ? null : fetcher.getService(); - } - - public static void unRegistService(PluginDescriptor plugin) { - Iterator> itr = SYSTEM_SERVICE_MAP.entrySet().iterator(); - while(itr.hasNext()) { - Map.Entry item = itr.next(); - if(plugin.getPackageName().equals(item.getValue().mPluginId)) { - itr.remove(); - } - } - } - -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginActivityMonitor.java b/PluginCore/src/com/plugin/core/manager/PluginActivityMonitor.java deleted file mode 100644 index b96b7ed1..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginActivityMonitor.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.plugin.core.manager; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; - -import com.plugin.core.PluginClassLoader; -import com.plugin.core.PluginContextTheme; - -import java.util.HashMap; - -public class PluginActivityMonitor { - - public static final String ACTION_UN_INSTALL_PLUGIN = "com.plugin.core.action.ACTION_UN_INSTALL_PLUGIN"; - - private HashMap receivers = new HashMap(); - - public void onActivityCreate(final Activity activity) { - if (activity.getParent() == null) { - if (activity.getClass().getClassLoader() instanceof PluginClassLoader) { - String pluginId = ((PluginContextTheme)activity.getApplication().getBaseContext()).getPluginDescriptor().getPackageName(); - BroadcastReceiver br = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - activity.finish(); - } - }; - receivers.put(activity, br); - - activity.registerReceiver(br, new IntentFilter(pluginId + ACTION_UN_INSTALL_PLUGIN)); - } - } - } - - public void onActivityDestory(Activity activity) { - if (activity.getParent() == null) { - if (activity.getClass().getClassLoader() instanceof PluginClassLoader) { - BroadcastReceiver br = receivers.remove(activity); - activity.unregisterReceiver(br); - } - } - } -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginCallback.java b/PluginCore/src/com/plugin/core/manager/PluginCallback.java deleted file mode 100644 index 00a434d4..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginCallback.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.plugin.core.manager; - -public interface PluginCallback { - - public static final String ACTION_PLUGIN_CHANGED = "com.plugin.core.action_plugin_changed"; - - void onPluginLoaderInited(); - void onPluginInstalled(String packageName, String version); - void onPluginRemoved(String packageName); - void onPluginStarted(String packageName); - void onPluginRemoveAll(); -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginCallbackImpl.java b/PluginCore/src/com/plugin/core/manager/PluginCallbackImpl.java deleted file mode 100644 index 540fa1ec..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginCallbackImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.plugin.core.manager; - -import android.content.Intent; - -import com.plugin.core.PluginLoader; -import com.plugin.util.LogUtil; - -/** - * Created by Administrator on 2015/9/13. - */ -public class PluginCallbackImpl implements PluginCallback { - - @Override - public void onPluginLoaderInited() { - LogUtil.d("PluginLoader inited"); - } - - @Override - public void onPluginInstalled(String packageName, String version) { - Intent intent = new Intent(ACTION_PLUGIN_CHANGED); - intent.putExtra("type", "install"); - intent.putExtra("id", packageName); - intent.putExtra("version", version); - PluginLoader.getApplication().sendBroadcast(intent); - } - - @Override - public void onPluginRemoved(String packageName) { - Intent intent = new Intent(ACTION_PLUGIN_CHANGED); - intent.putExtra("type", "remove"); - intent.putExtra("id", packageName); - PluginLoader.getApplication().sendBroadcast(intent); - } - - @Override - public void onPluginStarted(String packageName) { - Intent intent = new Intent(ACTION_PLUGIN_CHANGED); - intent.putExtra("type", "init"); - intent.putExtra("id", packageName); - PluginLoader.getApplication().sendBroadcast(intent); - } - - @Override - public void onPluginRemoveAll() { - Intent intent = new Intent(ACTION_PLUGIN_CHANGED); - intent.putExtra("type", "remove_all"); - PluginLoader.getApplication().sendBroadcast(intent); - } - -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginManagerHelper.java b/PluginCore/src/com/plugin/core/manager/PluginManagerHelper.java deleted file mode 100644 index 96b6060f..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginManagerHelper.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.plugin.core.manager; - -import android.annotation.TargetApi; -import android.os.Build; -import android.os.Bundle; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLoader; - -import java.util.Collection; -import java.util.HashMap; - -/** - * Created by cailiming on 16/3/11. - * - */ -public class PluginManagerHelper { - - //加个客户端进程的缓存,减少跨进程调用 - private static final HashMap localCache = new HashMap(); - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static PluginDescriptor getPluginDescriptorByClassName(String clazzName) { - - PluginDescriptor pluginDescriptor = localCache.get(clazzName); - - if (pluginDescriptor == null) { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_QUERY_BY_CLASS_NAME, clazzName, null); - pluginDescriptor = (PluginDescriptor)bundle.getSerializable(PluginManagerProvider.QUERY_BY_CLASS_NAME_RESULT); - localCache.put(clazzName, pluginDescriptor); - } - - return pluginDescriptor; - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @SuppressWarnings("unchecked") - public static Collection getPlugins() { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_QUERY_ALL, null, null); - return (Collection)bundle.getSerializable(PluginManagerProvider.QUERY_ALL_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static PluginDescriptor getPluginDescriptorByPluginId(String pluginId) { - - PluginDescriptor pluginDescriptor = localCache.get(pluginId); - - if (pluginDescriptor == null) { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_QUERY_BY_ID, pluginId, null); - pluginDescriptor = (PluginDescriptor)bundle.getSerializable(PluginManagerProvider.QUERY_BY_ID_RESULT); - localCache.put(pluginId, pluginDescriptor); - } - - return pluginDescriptor; - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static int installPlugin(String srcFile) { - localCache.clear(); - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_INSTALL, srcFile, null); - return bundle.getInt(PluginManagerProvider.INSTALL_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static synchronized void remove(String pluginId) { - localCache.clear(); - PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_REMOVE, pluginId, null); - } - - /** - * 清除列表并不能清除已经加载到内存当中的class,因为class一旦加载后后无法卸载 - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static synchronized void removeAll() { - localCache.clear(); - PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_REMOVE_ALL, null, null); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static PluginDescriptor getPluginDescriptorByFragmentId(String clazzId) { - - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_QUERY_BY_FRAGMENT_ID, clazzId, null); - return (PluginDescriptor)bundle.getSerializable(PluginManagerProvider.QUERY_BY_FRAGMENT_ID_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static String bindStubReceiver() { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_BIND_RECEIVER, null, null); - return bundle.getString(PluginManagerProvider.BIND_RECEIVER_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static String bindStubActivity(String pluginActivityClassName, int launchMode) { - Bundle arg = new Bundle(); - arg.putInt("launchMode", launchMode); - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_BIND_ACTIVITY, - pluginActivityClassName, arg); - return bundle.getString(PluginManagerProvider.BIND_ACTIVITY_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static boolean isExact(String name, int type) { - Bundle arg = new Bundle(); - arg.putInt("type", type); - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_IS_EXACT, - name, arg); - return bundle.getBoolean(PluginManagerProvider.IS_EXACT_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static void unBindLaunchModeStubActivity(String activityName, String className) { - Bundle arg = new Bundle(); - arg.putString("className", className); - PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_UNBIND_ACTIVITY, - activityName, arg); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static String getBindedPluginServiceName(String stubServiceName) { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_GET_BINDED_SERVICE, - stubServiceName, null); - return bundle.getString(PluginManagerProvider.GET_BINDED_SERVICE_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static String bindStubService(String pluginServiceClassName) { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_BIND_SERVICE, - pluginServiceClassName, null); - return bundle.getString(PluginManagerProvider.BIND_SERVICE_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static void unBindStubService(String pluginServiceName) { - PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_UNBIND_SERVICE, - pluginServiceName, null); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static boolean isStubActivity(String className) { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_IS_STUB_ACTIVITY, - className, null); - return bundle.getBoolean(PluginManagerProvider.IS_STUB_ACTIVITY_RESULT); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static String dumpServiceInfo() { - Bundle bundle = PluginLoader.getApplication().getContentResolver().call(PluginManagerProvider.buildUri(), - PluginManagerProvider.ACTION_DUMP_SERVICE_INFO, - null, null); - return bundle.getString(PluginManagerProvider.DUMP_SERVICE_INFO_RESULT); - } -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginManagerImpl.java b/PluginCore/src/com/plugin/core/manager/PluginManagerImpl.java deleted file mode 100644 index 717468c1..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginManagerImpl.java +++ /dev/null @@ -1,414 +0,0 @@ -package com.plugin.core.manager; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.os.Build; -import android.text.TextUtils; -import android.util.Base64; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLauncher; -import com.plugin.core.PluginCreator; -import com.plugin.core.PluginLoader; -import com.plugin.core.PluginManifestParser; -import com.plugin.core.localservice.LocalServiceManager; -import com.plugin.core.multidex.PluginMultiDexExtractor; -import com.plugin.util.FileUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.PackageVerifyer; -import com.plugin.util.ProcessUtil; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -class PluginManagerImpl { - - private static final boolean NEED_VERIFY_CERT = true; - private static final int SUCCESS = 0; - private static final int SRC_FILE_NOT_FOUND = 1; - private static final int COPY_FILE_FAIL = 2; - private static final int SIGNATURES_INVALIDATE = 3; - private static final int VERIFY_SIGNATURES_FAIL = 4; - private static final int PARSE_MANIFEST_FAIL = 5; - private static final int FAIL_BECAUSE_HAS_LOADED = 6; - private static final int INSTALL_FAIL = 7; - - private static final String INSTALLED_KEY = "plugins.list"; - - private static final String PENDING_KEY = "plugins.pending"; - - private final Hashtable sInstalledPlugins = new Hashtable(); - - private final Hashtable sPendingPlugins = new Hashtable(); - - private PluginCallback changeListener = new PluginCallbackImpl(); - - PluginManagerImpl() { - if (!ProcessUtil.isPluginProcess()) { - throw new IllegalAccessError("本类仅在插件进程使用"); - } - } - - /** - * 插件的安装目录, 插件apk将来会被放在这个目录下面 - */ - private String genInstallPath(String pluginId, String pluginVersoin) { - return getPluginRootDir() + "/" + pluginId + "/" + pluginVersoin + "/base-1.apk"; - } - - private String getPluginRootDir() { - return PluginLoader.getApplication().getDir("plugin_dir", Context.MODE_PRIVATE).getAbsolutePath(); - } - - @SuppressWarnings("unchecked") - synchronized void loadInstalledPlugins() { - if (sInstalledPlugins.size() == 0) { - Hashtable installedPlugin = readPlugins(INSTALLED_KEY); - if (installedPlugin != null) { - sInstalledPlugins.putAll(installedPlugin); - } - - //把pending合并到install - Hashtable pendingPlugin = readPlugins(PENDING_KEY); - if (pendingPlugin != null) { - Iterator> itr = pendingPlugin.entrySet().iterator(); - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - //删除旧版 - remove(entry.getKey()); - } - - //保存新版 - sInstalledPlugins.putAll(pendingPlugin); - savePlugins(INSTALLED_KEY, sInstalledPlugins); - - //清除pending - getSharedPreference().edit().remove(PENDING_KEY).commit(); - } - - } - } - - private boolean addOrReplace(PluginDescriptor pluginDescriptor) { - sInstalledPlugins.put(pluginDescriptor.getPackageName(), pluginDescriptor); - return savePlugins(INSTALLED_KEY, sInstalledPlugins); - } - - private boolean pending(PluginDescriptor pluginDescriptor) { - sPendingPlugins.put(pluginDescriptor.getPackageName(), pluginDescriptor); - return savePlugins(PENDING_KEY, sPendingPlugins); - } - - synchronized boolean removeAll() { - sInstalledPlugins.clear(); - boolean isSuccess = savePlugins(INSTALLED_KEY, sInstalledPlugins); - - FileUtil.deleteAll(new File(getPluginRootDir())); - - changeListener.onPluginRemoveAll(); - - return isSuccess; - } - - synchronized boolean remove(String pluginId) { - - PluginDescriptor old = sInstalledPlugins.remove(pluginId); - - PluginLauncher.instance().stopPlugin(pluginId, old); - - boolean result = false; - if (old != null) { - result = savePlugins(INSTALLED_KEY, sInstalledPlugins); - boolean deleteSuccess = FileUtil.deleteAll(new File(old.getInstalledPath()).getParentFile()); - LogUtil.d("delete old", result, deleteSuccess, old.getInstalledPath(), old.getPackageName()); - } - - changeListener.onPluginRemoved(pluginId); - - return result; - } - - Collection getPlugins() { - return sInstalledPlugins.values(); - } - - /** - * for Fragment - * - * @param clazzId - * @return - */ - PluginDescriptor getPluginDescriptorByFragmenetId(String clazzId) { - Iterator itr = sInstalledPlugins.values().iterator(); - while (itr.hasNext()) { - PluginDescriptor descriptor = itr.next(); - if (descriptor.containsFragment(clazzId)) { - return descriptor; - } - } - return null; - } - - PluginDescriptor getPluginDescriptorByPluginId(String pluginId) { - PluginDescriptor pluginDescriptor = sInstalledPlugins.get(pluginId); - if (pluginDescriptor != null && pluginDescriptor.isEnabled()) { - return pluginDescriptor; - } - return null; - } - - PluginDescriptor getPluginDescriptorByClassName(String clazzName) { - Iterator itr = sInstalledPlugins.values().iterator(); - while (itr.hasNext()) { - PluginDescriptor descriptor = itr.next(); - if (descriptor.containsName(clazzName)) { - return descriptor; - } - } - return null; - } - - /** - * 安装一个插件 - * - * @param srcPluginFile - * @return - */ - synchronized int installPlugin(String srcPluginFile) { - LogUtil.e("开始安装插件", srcPluginFile); - if (TextUtils.isEmpty(srcPluginFile) || !new File(srcPluginFile).exists()) { - return SRC_FILE_NOT_FOUND; - } - - //第0步,先将apk复制到宿主程序私有目录,防止在安装过程中文件被篡改 - if (!srcPluginFile.startsWith(PluginLoader.getApplication().getCacheDir().getAbsolutePath())) { - String tempFilePath = PluginLoader.getApplication().getCacheDir().getAbsolutePath() - + File.separator + System.currentTimeMillis() + ".apk"; - if (FileUtil.copyFile(srcPluginFile, tempFilePath)) { - srcPluginFile = tempFilePath; - } else { - LogUtil.e("复制插件文件失败", srcPluginFile, tempFilePath); - return COPY_FILE_FAIL; - } - } - - // 第1步,验证插件APK签名,如果被篡改过,将获取不到证书 - //sApplication.getPackageManager().getPackageArchiveInfo(srcPluginFile, PackageManager.GET_SIGNATURES); - Signature[] pluginSignatures = PackageVerifyer.collectCertificates(srcPluginFile, false); - boolean isDebugable = (0 != (PluginLoader.getApplication().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)); - if (pluginSignatures == null) { - LogUtil.e("插件签名验证失败", srcPluginFile); - new File(srcPluginFile).delete(); - return SIGNATURES_INVALIDATE; - } else if (NEED_VERIFY_CERT && !isDebugable) { - //可选步骤,验证插件APK证书是否和宿主程序证书相同。 - //证书中存放的是公钥和算法信息,而公钥和私钥是1对1的 - //公钥相同意味着是同一个作者发布的程序 - Signature[] mainSignatures = null; - try { - PackageInfo pkgInfo = PluginLoader.getApplication().getPackageManager().getPackageInfo(PluginLoader.getApplication().getPackageName(), PackageManager.GET_SIGNATURES); - mainSignatures = pkgInfo.signatures; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - if (!PackageVerifyer.isSignaturesSame(mainSignatures, pluginSignatures)) { - LogUtil.e("插件证书和宿主证书不一致", srcPluginFile); - new File(srcPluginFile).delete(); - return VERIFY_SIGNATURES_FAIL; - } - } - - // 第2步,解析Manifest,获得插件详情 - PluginDescriptor pluginDescriptor = PluginManifestParser.parseManifest(srcPluginFile); - if (pluginDescriptor == null || TextUtils.isEmpty(pluginDescriptor.getPackageName())) { - LogUtil.e("解析插件Manifest文件失败", srcPluginFile); - new File(srcPluginFile).delete(); - return PARSE_MANIFEST_FAIL; - } - - PackageInfo packageInfo = PluginLoader.getApplication().getPackageManager().getPackageArchiveInfo(srcPluginFile, PackageManager.GET_GIDS); - if (packageInfo != null) { - pluginDescriptor.setApplicationTheme(packageInfo.applicationInfo.theme); - pluginDescriptor.setApplicationIcon(packageInfo.applicationInfo.icon); - pluginDescriptor.setApplicationLogo(packageInfo.applicationInfo.logo); - } - - // 第3步,检查插件是否已经存在,若存在删除旧的 - PluginDescriptor oldPluginDescriptor = getPluginDescriptorByPluginId(pluginDescriptor.getPackageName()); - if (oldPluginDescriptor != null) { - LogUtil.e("已安装过,安装路径为", oldPluginDescriptor.getInstalledPath(), oldPluginDescriptor.getVersion(), pluginDescriptor.getVersion()); - - //检查插件是否已经加载 - if (PluginLauncher.instance().isRunning(oldPluginDescriptor.getPackageName())) { - if (!oldPluginDescriptor.getVersion().equals(pluginDescriptor.getVersion())) { - LogUtil.e("旧版插件已经加载, 且新版插件和旧版插件版本不同,直接删除旧版,进行热更新"); - remove(oldPluginDescriptor.getPackageName()); - } else { - LogUtil.e("旧版插件已经加载, 且新版插件和旧版插件版本相同,拒绝安装"); - new File(srcPluginFile).delete(); - return FAIL_BECAUSE_HAS_LOADED; - } - } else { - LogUtil.e("旧版插件还未加载,忽略版本,直接删除旧版,尝试安装新版"); - remove(oldPluginDescriptor.getPackageName()); - } - } - - // 第4步骤,复制插件到插件目录 - String destApkPath = genInstallPath(pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); - boolean isCopySuccess = FileUtil.copyFile(srcPluginFile, destApkPath); - - if (!isCopySuccess) { - - LogUtil.e("复制插件到安装目录失败", srcPluginFile); - //删掉临时文件 - new File(srcPluginFile).delete(); - return COPY_FILE_FAIL; - } else { - - //第5步,先解压so到临时目录,再从临时目录复制到插件so目录。 在构造插件Dexclassloader的时候,会使用这个so目录作为参数 - File apkParent = new File(destApkPath).getParentFile(); - File tempSoDir = new File(apkParent, "temp"); - Set soList = FileUtil.unZipSo(srcPluginFile, tempSoDir); - if (soList != null) { - for (String soName : soList) { - FileUtil.copySo(tempSoDir, soName, apkParent.getAbsolutePath()); - } - //删掉临时文件 - FileUtil.deleteAll(tempSoDir); - } - - try { - ArrayList multiDexFiles = PluginMultiDexExtractor.performExtractions(new File(destApkPath), new File(apkParent, "secondDexes")); - pluginDescriptor.setMuliDexList(multiDexFiles); - } catch (IOException e) { - e.printStackTrace(); - } - - // 第6步 添加到已安装插件列表 - pluginDescriptor.setInstalledPath(destApkPath); - boolean isInstallSuccess = false; - - isInstallSuccess = addOrReplace(pluginDescriptor); - - //删掉临时文件 - new File(srcPluginFile).delete(); - - if (!isInstallSuccess) { - LogUtil.e("安装插件失败", srcPluginFile); - - new File(destApkPath).delete(); - - return INSTALL_FAIL; - } else { - //通过创建classloader来触发dexopt,但不加载 - LogUtil.d("正在进行DEXOPT...", pluginDescriptor.getInstalledPath()); - //ActivityThread.getPackageManager().performDexOptIfNeeded() - FileUtil.deleteAll(new File(apkParent, "dalvik-cache")); - PluginCreator.createPluginClassLoader(pluginDescriptor.getInstalledPath(), pluginDescriptor.isStandalone(), null, null); - LogUtil.d("DEXOPT完毕"); - - LocalServiceManager.registerService(pluginDescriptor); - - changeListener.onPluginInstalled(pluginDescriptor.getPackageName(), pluginDescriptor.getVersion()); - LogUtil.e("安装插件成功", destApkPath); - - //打印一下目录结构 - FileUtil.printAll(new File(PluginLoader.getApplication().getApplicationInfo().dataDir)); - - return SUCCESS; - } - } - } - - private static SharedPreferences getSharedPreference() { - SharedPreferences sp = PluginLoader.getApplication().getSharedPreferences("plugins.installed", - Build.VERSION.SDK_INT < 11 ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | 0x0004); - return sp; - } - - private synchronized boolean savePlugins(String key, Hashtable plugins) { - - ObjectOutputStream objectOutputStream = null; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try { - objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); - objectOutputStream.writeObject(plugins); - objectOutputStream.flush(); - - byte[] data = byteArrayOutputStream.toByteArray(); - String list = Base64.encodeToString(data, Base64.DEFAULT); - - getSharedPreference().edit().putString(key, list).commit(); - return true; - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (objectOutputStream != null) { - try { - objectOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (byteArrayOutputStream != null) { - try { - byteArrayOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return false; - } - - @SuppressWarnings("unchecked") - private synchronized Hashtable readPlugins(String key) { - String list = getSharedPreference().getString(key, ""); - Serializable object = null; - if (!TextUtils.isEmpty(list)) { - ByteArrayInputStream byteArrayInputStream = null; - ObjectInputStream objectInputStream = null; - try { - byteArrayInputStream = new ByteArrayInputStream(Base64.decode(list, Base64.DEFAULT)); - objectInputStream = new ObjectInputStream(byteArrayInputStream); - object = (Serializable) objectInputStream.readObject(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (objectInputStream != null) { - try { - objectInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (byteArrayInputStream != null) { - try { - byteArrayInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - return (Hashtable) object; - } - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/manager/PluginManagerProvider.java b/PluginCore/src/com/plugin/core/manager/PluginManagerProvider.java deleted file mode 100644 index 5378ff61..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginManagerProvider.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.plugin.core.manager; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLoader; -import com.plugin.util.LogUtil; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * Created by cailiming on 16/3/11. - * - * 利用ContentProvider实现同步跨进程调用 - * - */ -public class PluginManagerProvider extends ContentProvider { - - private static Uri CONTENT_URI; - - public static final String ACTION_INSTALL = "install"; - public static final String INSTALL_RESULT = "install_result"; - - public static final String ACTION_REMOVE = "remove"; - public static final String REMOVE_RESULT = "remove_result"; - - public static final String ACTION_REMOVE_ALL = "remove_all"; - public static final String REMOVE_ALL_RESULT = "remove_all_result"; - - public static final String ACTION_QUERY_BY_ID = "query_by_id"; - public static final String QUERY_BY_ID_RESULT = "query_by_id_result"; - - public static final String ACTION_QUERY_BY_CLASS_NAME = "query_by_class_name"; - public static final String QUERY_BY_CLASS_NAME_RESULT = "query_by_class_name_result"; - - public static final String ACTION_QUERY_BY_FRAGMENT_ID = "query_by_fragment_id"; - public static final String QUERY_BY_FRAGMENT_ID_RESULT = "query_by_fragment_id_result"; - - public static final String ACTION_QUERY_ALL = "query_all"; - public static final String QUERY_ALL_RESULT = "query_all_result"; - - public static final String ACTION_BIND_ACTIVITY = "bind_activity"; - public static final String BIND_ACTIVITY_RESULT = "bind_activity_result"; - - public static final String ACTION_UNBIND_ACTIVITY = "unbind_activity"; - public static final String UNBIND_ACTIVITY_RESULT = "unbind_activity_result"; - - public static final String ACTION_BIND_SERVICE = "bind_service"; - public static final String BIND_SERVICE_RESULT = "bind_service_result"; - - public static final String ACTION_GET_BINDED_SERVICE = "get_binded_service"; - public static final String GET_BINDED_SERVICE_RESULT = "get_binded_service_result"; - - public static final String ACTION_UNBIND_SERVICE = "unbind_service"; - public static final String UNBIND_SERVICE_RESULT = "unbind_service_result"; - - public static final String ACTION_BIND_RECEIVER = "bind_receiver"; - public static final String BIND_RECEIVER_RESULT = "bind_receiver_result"; - - public static final String ACTION_IS_EXACT = "is_exact"; - public static final String IS_EXACT_RESULT = "is_exact_result"; - - public static final String ACTION_IS_STUB_ACTIVITY = "is_stub_activity"; - public static final String IS_STUB_ACTIVITY_RESULT = "is_stub_activity_result"; - - public static final String ACTION_DUMP_SERVICE_INFO = "dump_service_info"; - public static final String DUMP_SERVICE_INFO_RESULT = "dump_service_info_result"; - - private Object mLockObject = new Object(); - - private PluginManagerImpl manager; - - public static Uri buildUri() { - if (CONTENT_URI == null) { - CONTENT_URI = Uri.parse("content://"+ PluginLoader.getApplication().getPackageName() + ".manager.pluginmanager" + "/call"); - } - return CONTENT_URI; - } - - @Override - public boolean onCreate() { - manager = new PluginManagerImpl(); - manager.loadInstalledPlugins(); - return false; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - //doNothing - return null; - } - - @Override - public String getType(Uri uri) { - //doNothing - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - //doNothing - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - //doNothing - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - //doNothing - return 0; - } - - @Override - public Bundle call(String method, String arg, Bundle extras) { - - if (Build.VERSION.SDK_INT >= 19) { - LogUtil.d("callingPackage = ", getCallingPackage()); - } - - LogUtil.d("Thead : id = " + Thread.currentThread().getId() - + ", name = " + Thread.currentThread().getName() - + ", method = " + method - + ", arg = " + arg); - - synchronized (mLockObject) { - Bundle bundle = new Bundle(); - if (ACTION_INSTALL.equals(method)) { - - int result = manager.installPlugin(arg); - bundle.putInt(INSTALL_RESULT, result); - - return bundle; - - } else if (ACTION_REMOVE.equals(method)) { - - boolean success = manager.remove(arg); - bundle.putBoolean(REMOVE_RESULT, success); - - return bundle; - - } else if (ACTION_REMOVE_ALL.equals(method)) { - - boolean success = manager.removeAll(); - bundle.putBoolean(REMOVE_ALL_RESULT, success); - - return bundle; - - } else if (ACTION_QUERY_BY_ID.equals(method)) { - - PluginDescriptor pluginDescriptor = manager.getPluginDescriptorByPluginId(arg); - bundle.putSerializable(QUERY_BY_ID_RESULT, pluginDescriptor); - - return bundle; - - } else if (ACTION_QUERY_BY_CLASS_NAME.equals(method)) { - - PluginDescriptor pluginDescriptor = manager.getPluginDescriptorByClassName(arg); - bundle.putSerializable(QUERY_BY_CLASS_NAME_RESULT, pluginDescriptor); - - return bundle; - - } else if (ACTION_QUERY_BY_FRAGMENT_ID.equals(method)) { - - PluginDescriptor pluginDescriptor = manager.getPluginDescriptorByFragmenetId(arg); - bundle.putSerializable(QUERY_BY_FRAGMENT_ID_RESULT, pluginDescriptor); - - return bundle; - - } else if (ACTION_QUERY_ALL.equals(method)) { - - Collection pluginDescriptorList = manager.getPlugins(); - ArrayList result = new ArrayList(pluginDescriptorList.size()); - result.addAll(pluginDescriptorList); - bundle.putSerializable(QUERY_ALL_RESULT, result); - - return bundle; - - } else if (ACTION_BIND_ACTIVITY.equals(method)) { - - bundle.putString(BIND_ACTIVITY_RESULT, - PluginStubBinding.bindStubActivity(arg, extras.getInt("launchMode"))); - - return bundle; - - } else if (ACTION_UNBIND_ACTIVITY.equals(method)) { - - PluginStubBinding.unBindLaunchModeStubActivity(arg, extras.getString("className")); - - } else if (ACTION_BIND_SERVICE.equals(method)) { - bundle.putString(BIND_SERVICE_RESULT, PluginStubBinding.bindStubService(arg)); - - return bundle; - - } else if (ACTION_GET_BINDED_SERVICE.equals(method)) { - bundle.putString(GET_BINDED_SERVICE_RESULT, PluginStubBinding.getBindedPluginServiceName(arg)); - - return bundle; - - } else if (ACTION_UNBIND_SERVICE.equals(method)) { - - PluginStubBinding.unBindStubService(arg); - - } else if (ACTION_BIND_RECEIVER.equals(method)) { - bundle.putString(BIND_RECEIVER_RESULT, PluginStubBinding.bindStubReceiver()); - - return bundle; - - } else if (ACTION_IS_EXACT.equals(method)) { - bundle.putBoolean(IS_EXACT_RESULT, PluginStubBinding.isExact(arg, extras.getInt("type"))); - - return bundle; - - } else if (ACTION_IS_STUB_ACTIVITY.equals(method)) { - bundle.putBoolean(IS_STUB_ACTIVITY_RESULT, PluginStubBinding.isStubActivity(arg)); - - return bundle; - - } else if (ACTION_DUMP_SERVICE_INFO.equals(method)) { - bundle.putString(DUMP_SERVICE_INFO_RESULT, PluginStubBinding.dumpServieInfo()); - return bundle; - } - } - return null; - } -} diff --git a/PluginCore/src/com/plugin/core/manager/PluginStubBinding.java b/PluginCore/src/com/plugin/core/manager/PluginStubBinding.java deleted file mode 100644 index f49cb624..00000000 --- a/PluginCore/src/com/plugin/core/manager/PluginStubBinding.java +++ /dev/null @@ -1,449 +0,0 @@ -package com.plugin.core.manager; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.text.TextUtils; -import android.util.Base64; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLoader; -import com.plugin.util.LogUtil; -import com.plugin.util.ProcessUtil; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * 插件组件动态绑定到宿主的虚拟stub组件 - */ -class PluginStubBinding { - - /** - * key:stub Activity Name - * value:plugin Activity Name - */ - private static HashMap singleTaskActivityMapping = new HashMap(); - private static HashMap singleTopActivityMapping = new HashMap(); - private static HashMap singleInstanceActivityMapping = new HashMap(); - private static String standardActivity = null; - private static String receiver = null; - /** - * key:stub Service Name - * value:plugin Service Name - */ - private static HashMap serviceMapping = new HashMap(); - - private static Set mExcatStubSet; - - private static boolean isPoolInited = false; - - private static String buildDefaultAction() { - return PluginLoader.getApplication().getPackageName() + ".STUB_DEFAULT"; - } - - private static String buildExactAction() { - return PluginLoader.getApplication().getPackageName() + ".STUB_EXACT"; - } - - private static void initPool() { - - if(!ProcessUtil.isPluginProcess()) { - throw new IllegalAccessError("此类只能在插件所在进程使用"); - } - - if (isPoolInited) { - return; - } - - loadStubActivity(); - - loadStubService(); - - loadStubExactly(); - - loadStubReceiver(); - - isPoolInited = true; - } - - private static void loadStubActivity() { - Intent launchModeIntent = new Intent(); - launchModeIntent.setAction(buildDefaultAction()); - launchModeIntent.setPackage(PluginLoader.getApplication().getPackageName()); - - List list = PluginLoader.getApplication().getPackageManager().queryIntentActivities(launchModeIntent, PackageManager.MATCH_DEFAULT_ONLY); - - if (list != null && list.size() >0) { - for (ResolveInfo resolveInfo: - list) { - if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) { - - singleTaskActivityMapping.put(resolveInfo.activityInfo.name, null); - - } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) { - - singleTopActivityMapping.put(resolveInfo.activityInfo.name, null); - - } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) { - - singleInstanceActivityMapping.put(resolveInfo.activityInfo.name, null); - - } else if (resolveInfo.activityInfo.launchMode == ActivityInfo.LAUNCH_MULTIPLE) { - - standardActivity = resolveInfo.activityInfo.name; - - } - - } - } - - } - - private static void loadStubService() { - Intent launchModeIntent = new Intent(); - launchModeIntent.setAction(buildDefaultAction()); - launchModeIntent.setPackage(PluginLoader.getApplication().getPackageName()); - - List list = PluginLoader.getApplication().getPackageManager().queryIntentServices(launchModeIntent, PackageManager.MATCH_DEFAULT_ONLY); - - if (list != null && list.size() >0) { - for (ResolveInfo resolveInfo: - list) { - serviceMapping.put(resolveInfo.serviceInfo.name, null); - } - HashMap mapping = restore(); - if (mapping != null) { - serviceMapping.putAll(mapping); - } - //只有service需要固化 - save(serviceMapping); - } - } - - private static void loadStubExactly() { - Intent exactStub = new Intent(); - exactStub.setAction(buildExactAction()); - exactStub.setPackage(PluginLoader.getApplication().getPackageName()); - - //精确匹配的activity - List resolveInfos = PluginLoader.getApplication().getPackageManager().queryIntentActivities(exactStub, PackageManager.MATCH_DEFAULT_ONLY); - - if (resolveInfos != null && resolveInfos.size() > 0) { - if (mExcatStubSet == null) { - mExcatStubSet = new HashSet(); - } - for(ResolveInfo info:resolveInfos) { - mExcatStubSet.add(info.activityInfo.name); - } - } - - //精确匹配的service - resolveInfos = PluginLoader.getApplication().getPackageManager().queryIntentServices(exactStub, PackageManager.MATCH_DEFAULT_ONLY); - - if (resolveInfos != null && resolveInfos.size() > 0) { - if (mExcatStubSet == null) { - mExcatStubSet = new HashSet(); - } - for(ResolveInfo info:resolveInfos) { - mExcatStubSet.add(info.serviceInfo.name); - } - } - - } - - private static void loadStubReceiver() { - Intent exactStub = new Intent(); - exactStub.setAction(buildDefaultAction()); - exactStub.setPackage(PluginLoader.getApplication().getPackageName()); - - List resolveInfos = PluginLoader.getApplication().getPackageManager().queryBroadcastReceivers(exactStub, PackageManager.MATCH_DEFAULT_ONLY); - - if (resolveInfos != null && resolveInfos.size() >0) { - receiver = resolveInfos.get(0).activityInfo.name; - } - - } - - public static String bindStubReceiver() { - initPool(); - return receiver; - } - - public static String bindStubActivity(String pluginActivityClassName, int launchMode) { - - initPool(); - - if (isExact(pluginActivityClassName, PluginDescriptor.ACTIVITY)) { - return pluginActivityClassName; - } - - - HashMap bindingMapping = null; - - if (launchMode == ActivityInfo.LAUNCH_MULTIPLE) { - - return standardActivity; - - } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) { - - bindingMapping = singleTaskActivityMapping; - - } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) { - - bindingMapping = singleTopActivityMapping; - - } else if (launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) { - - bindingMapping = singleInstanceActivityMapping; - - } - - if (bindingMapping != null) { - - Iterator> itr = bindingMapping.entrySet().iterator(); - String idleStubActivityName = null; - - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - if (entry.getValue() == null) { - if (idleStubActivityName == null) { - idleStubActivityName = entry.getKey(); - //这里找到空闲的stubactivity以后,还需继续遍历,用来检查是否pluginActivityClassName已经绑定过了 - } - } else if (pluginActivityClassName.equals(entry.getValue())) { - //已绑定过,直接返回 - return entry.getKey(); - } - } - - //没有绑定到StubActivity,而且还有空余的stubActivity,进行绑定 - if (idleStubActivityName != null) { - bindingMapping.put(idleStubActivityName, pluginActivityClassName); - return idleStubActivityName; - } - - } - - return standardActivity; - } - - public static boolean isExact(String name, int type) { - initPool(); - - if (mExcatStubSet != null && mExcatStubSet.size() > 0) { - return mExcatStubSet.contains(name); - } - - return false; - } - - public static void unBindLaunchModeStubActivity(String stubActivityName, String pluginActivityName) { - - LogUtil.d("unBindLaunchModeStubActivity", stubActivityName, pluginActivityName); - - if (pluginActivityName.equals(singleTaskActivityMapping.get(stubActivityName))) { - - LogUtil.d("unBindLaunchModeStubActivity", stubActivityName, pluginActivityName); - singleTaskActivityMapping.put(stubActivityName, null); - - } else if (pluginActivityName.equals(singleInstanceActivityMapping.get(stubActivityName))) { - - LogUtil.d("unBindLaunchModeStubActivity", stubActivityName, pluginActivityName); - singleInstanceActivityMapping.put(stubActivityName, null); - - } else { - //对于standard和singleTop的launchmode,不做处理。 - } - } - - public static String getBindedPluginServiceName(String stubServiceName) { - - initPool(); - - if (isExact(stubServiceName, PluginDescriptor.SERVICE)) { - return stubServiceName; - } - - Iterator> itr = serviceMapping.entrySet().iterator(); - - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - - if (entry.getKey().equals(stubServiceName)) { - return entry.getValue(); - } - } - - //没有找到,尝试重磁盘恢复 - HashMap mapping = restore(); - if (mapping != null) { - itr = mapping.entrySet().iterator(); - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - - if (entry.getKey().equals(stubServiceName)) { - serviceMapping.put(stubServiceName, entry.getValue()); - save(serviceMapping); - return entry.getValue(); - } - } - } - - return null; - } - - public static String bindStubService(String pluginServiceClassName) { - - initPool(); - - if (isExact(pluginServiceClassName, PluginDescriptor.SERVICE)) { - return pluginServiceClassName; - } - - Iterator> itr = serviceMapping.entrySet().iterator(); - - String idleStubServiceName = null; - - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - if (entry.getValue() == null) { - if (idleStubServiceName == null) { - idleStubServiceName = entry.getKey(); - //这里找到空闲的idleStubServiceName以后,还需继续遍历,用来检查是否pluginServiceClassName已经绑定过了 - } - } else if (pluginServiceClassName.equals(entry.getValue())) { - //已经绑定过,直接返回 - LogUtil.d("已经绑定过", entry.getKey(), pluginServiceClassName); - return entry.getKey(); - } - } - - //没有绑定到StubService,而且还有空余的StubService,进行绑定 - if (idleStubServiceName != null) { - LogUtil.d("添加绑定", idleStubServiceName, pluginServiceClassName); - serviceMapping.put(idleStubServiceName, pluginServiceClassName); - //对serviceMapping持久化是因为如果service处于运行状态时app发生了crash,系统会自动恢复之前的service,此时插件映射信息查不到的话会再次crash - save(serviceMapping); - return idleStubServiceName; - } - - //绑定失败 - return null; - } - - public static void unBindStubService(String pluginServiceName) { - Iterator> itr = serviceMapping.entrySet().iterator(); - while (itr.hasNext()) { - Map.Entry entry = itr.next(); - if (pluginServiceName.equals(entry.getValue())) { - //如果存在绑定关系,解绑 - LogUtil.d("回收绑定", entry.getKey(), entry.getValue()); - serviceMapping.put(entry.getKey(), null); - save(serviceMapping); - break; - } - } - } - - public static String dumpServieInfo() { - return serviceMapping.toString(); - } - - private static boolean save(HashMap mapping) { - - ObjectOutputStream objectOutputStream = null; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try { - objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); - objectOutputStream.writeObject(mapping); - objectOutputStream.flush(); - - byte[] data = byteArrayOutputStream.toByteArray(); - String list = Base64.encodeToString(data, Base64.DEFAULT); - - PluginLoader.getApplication() - .getSharedPreferences("plugins.serviceMapping", Context.MODE_PRIVATE) - .edit().putString("plugins.serviceMapping.map", list).commit(); - - return true; - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (objectOutputStream != null) { - try { - objectOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (byteArrayOutputStream != null) { - try { - byteArrayOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return false; - } - - private static HashMap restore() { - String list = PluginLoader.getApplication() - .getSharedPreferences("plugins.serviceMapping", Context.MODE_PRIVATE) - .getString("plugins.serviceMapping.map", ""); - Serializable object = null; - if (!TextUtils.isEmpty(list)) { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( - Base64.decode(list, Base64.DEFAULT)); - ObjectInputStream objectInputStream = null; - try { - objectInputStream = new ObjectInputStream(byteArrayInputStream); - object = (Serializable) objectInputStream.readObject(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (objectInputStream != null) { - try { - objectInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (byteArrayInputStream != null) { - try { - byteArrayInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - if (object != null) { - - HashMap mapping = (HashMap) object; - return mapping; - } - return null; - } - - public static boolean isStubActivity(String className) { - initPool(); - - return isExact(className, PluginDescriptor.ACTIVITY) || className.equals(standardActivity) || singleTaskActivityMapping.containsKey(className) - || singleTopActivityMapping.containsKey(className) - || singleInstanceActivityMapping.containsKey(className); - } -} diff --git a/PluginCore/src/com/plugin/core/multidex/PluginMultiDexExtractor.java b/PluginCore/src/com/plugin/core/multidex/PluginMultiDexExtractor.java deleted file mode 100644 index 8c914942..00000000 --- a/PluginCore/src/com/plugin/core/multidex/PluginMultiDexExtractor.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.plugin.core.multidex; - -import android.util.Log; - -import java.io.BufferedOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileFilter; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -/** - * Exposes application secondary dex files as files in the application data - * directory. - */ -public final class PluginMultiDexExtractor { - - private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + - "secondary-dexes"; - - private static final String TAG = "PluginMultiDexExtractor"; - - /** - * We look for additional dex files named {@code classes2.dex}, - * {@code classes3.dex}, etc. - */ - private static final String DEX_PREFIX = "classes"; - private static final String DEX_SUFFIX = ".dex"; - - private static final String EXTRACTED_NAME_EXT = ".classes"; - private static final String EXTRACTED_SUFFIX = ".zip"; - private static final int MAX_EXTRACT_ATTEMPTS = 3; - - - /** - * Size of reading buffers. - */ - private static final int BUFFER_SIZE = 0x4000; - - public static ArrayList performExtractions(File sourceApk, File dexDir) - throws IOException { - - final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; - - // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that - // contains a secondary dex file in there is not consistent with the latest apk. Otherwise, - // multi-process race conditions can cause a crash loop where one process deletes the zip - // while another had created it. - prepareDexDir(dexDir, extractedFilePrefix); - - ArrayList files = new ArrayList(); - - final ZipFile apk = new ZipFile(sourceApk); - try { - - int secondaryNumber = 2; - - ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); - while (dexFile != null) { - String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; - File extractedFile = new File(dexDir, fileName); - files.add(extractedFile.getAbsolutePath()); - - Log.i(TAG, "Extraction is needed for file " + extractedFile); - int numAttempts = 0; - boolean isExtractionSuccessful = false; - while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { - numAttempts++; - - // Create a zip file (extractedFile) containing only the secondary dex file - // (dexFile) from the apk. - extract(apk, dexFile, extractedFile, extractedFilePrefix); - - // Verify that the extracted file is indeed a zip file. - isExtractionSuccessful = verifyZipFile(extractedFile); - - // Log the sha1 of the extracted zip file - Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + - " - length " + extractedFile.getAbsolutePath() + ": " + - extractedFile.length()); - if (!isExtractionSuccessful) { - // Delete the extracted file - extractedFile.delete(); - if (extractedFile.exists()) { - Log.w(TAG, "Failed to delete corrupted secondary dex '" + - extractedFile.getPath() + "'"); - } - } - } - if (!isExtractionSuccessful) { - throw new IOException("Could not create zip file " + - extractedFile.getAbsolutePath() + " for secondary dex (" + - secondaryNumber + ")"); - } - secondaryNumber++; - dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); - } - } finally { - try { - apk.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close resource", e); - } - } - - return files; - } - - /** - * This removes any files that do not have the correct prefix. - */ - private static void prepareDexDir(File dexDir, final String extractedFilePrefix) - throws IOException { - /* mkdirs() has some bugs, especially before jb-mr1 and we have only a maximum of one parent - * to create, lets stick to mkdir(). - */ - File cache = dexDir.getParentFile(); - mkdirChecked(cache); - mkdirChecked(dexDir); - - // Clean possible old files - FileFilter filter = new FileFilter() { - - @Override - public boolean accept(File pathname) { - return !pathname.getName().startsWith(extractedFilePrefix); - } - }; - File[] files = dexDir.listFiles(filter); - if (files == null) { - Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); - return; - } - for (File oldFile : files) { - Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + - oldFile.length()); - if (!oldFile.delete()) { - Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); - } else { - Log.i(TAG, "Deleted old file " + oldFile.getPath()); - } - } - } - - private static void mkdirChecked(File dir) throws IOException { - dir.mkdir(); - if (!dir.isDirectory()) { - File parent = dir.getParentFile(); - if (parent == null) { - Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null."); - } else { - Log.e(TAG, "Failed to create dir " + dir.getPath() + - ". parent file is a dir " + parent.isDirectory() + - ", a file " + parent.isFile() + - ", exists " + parent.exists() + - ", readable " + parent.canRead() + - ", writable " + parent.canWrite()); - } - throw new IOException("Failed to create cache directory " + dir.getPath()); - } - } - - private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, - String extractedFilePrefix) throws IOException, FileNotFoundException { - - InputStream in = apk.getInputStream(dexFile); - ZipOutputStream out = null; - File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, - extractTo.getParentFile()); - Log.i(TAG, "Extracting " + tmp.getPath()); - try { - out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); - try { - ZipEntry classesDex = new ZipEntry("classes.dex"); - // keep zip entry time since it is the criteria used by Dalvik - classesDex.setTime(dexFile.getTime()); - out.putNextEntry(classesDex); - - byte[] buffer = new byte[BUFFER_SIZE]; - int length = in.read(buffer); - while (length != -1) { - out.write(buffer, 0, length); - length = in.read(buffer); - } - out.closeEntry(); - } finally { - out.close(); - } - Log.i(TAG, "Renaming to " + extractTo.getPath()); - if (!tmp.renameTo(extractTo)) { - throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + - "\" to \"" + extractTo.getAbsolutePath() + "\""); - } - } finally { - closeQuietly(in); - tmp.delete(); // return status ignored - } - } - - /** - * Returns whether the file is a valid zip file. - */ - private static boolean verifyZipFile(File file) { - try { - ZipFile zipFile = new ZipFile(file); - try { - zipFile.close(); - return true; - } catch (IOException e) { - Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); - } - } catch (ZipException ex) { - Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); - } catch (IOException ex) { - Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); - } - return false; - } - - /** - * Closes the given {@code Closeable}. Suppresses any IO exceptions. - */ - private static void closeQuietly(Closeable closeable) { - try { - closeable.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close resource", e); - } - } - -} diff --git a/PluginCore/src/com/plugin/core/proxy/MethodProxy.java b/PluginCore/src/com/plugin/core/proxy/MethodProxy.java deleted file mode 100644 index 9d1bece9..00000000 --- a/PluginCore/src/com/plugin/core/proxy/MethodProxy.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.plugin.core.proxy; - - - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -/** - * Created by cailiming on 16/1/15. - */ -public abstract class MethodProxy extends MethodDelegate { - - protected static Map sMethods = new HashMap(5); - - protected MethodDelegate findMethodDelegate(String methodName, Object[] args) { - return sMethods.get(methodName); - } - - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - MethodDelegate deleate = findMethodDelegate(method.getName(), args); - if (deleate != null) { - return deleate.beforeInvoke(target, method, args); - } - return super.beforeInvoke(target, method, args); - } - - @Override - public Object afterInvoke(Object target, Method method, Object[] args, Object before, Object invokeResult) { - MethodDelegate deleate = findMethodDelegate(method.getName(), args); - if (deleate != null) { - return deleate.afterInvoke(target, method, args, before, invokeResult); - } - return super.afterInvoke(target, method, args, before, invokeResult); - } - - -} diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidAppIActivityManager.java b/PluginCore/src/com/plugin/core/systemservice/AndroidAppIActivityManager.java deleted file mode 100644 index 462eb427..00000000 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidAppIActivityManager.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.plugin.core.systemservice; - -import android.content.Intent; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.proxy.MethodDelegate; -import com.plugin.core.proxy.MethodProxy; -import com.plugin.core.proxy.ProxyUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.NotificationHelper; -import com.plugin.util.RefInvoker; - -import java.lang.reflect.Method; - -/** - * Created by cailiming on 16/1/15. - */ -public class AndroidAppIActivityManager extends MethodProxy { - - private AndroidAppIActivityManager() { - } - - public static void installProxy() { - LogUtil.d("安装ActivityManagerProxy"); - Object androidAppActivityManagerProxy = RefInvoker.invokeStaticMethod("android.app.ActivityManagerNative", "getDefault", (Class[])null, (Object[])null); - Object androidAppIActivityManagerStubProxyProxy = ProxyUtil.createProxy(androidAppActivityManagerProxy, new AndroidAppIActivityManager()); - Object singleton = RefInvoker.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault"); - //如果是IActivityManager - if (singleton.getClass().isAssignableFrom(androidAppIActivityManagerStubProxyProxy.getClass())) { - RefInvoker.setStaticOjbect("android.app.ActivityManagerNative", "gDefault", androidAppIActivityManagerStubProxyProxy); - } else {//否则是包装过的单例 - RefInvoker.setFieldObject(singleton, "android.util.Singleton", "mInstance", androidAppIActivityManagerStubProxyProxy); - } - LogUtil.d("安装完成"); - } - - static { - sMethods.put("getRunningAppProcesses", new getRunningAppProcesses()); - sMethods.put("killBackgroundProcesses", new killBackgroundProcesses()); - sMethods.put("getServices", new getServices()); - sMethods.put("getIntentSender", new getIntentSender()); - } - - //public List getRunningAppProcesses() - public static class getRunningAppProcesses extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - //TODO 需要时再说 - return super.beforeInvoke(target, method, args); - } - } - - //public void killBackgroundProcesses(String packageName) - public static class killBackgroundProcesses extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - //TODO 需要时再说 - return super.beforeInvoke(target, method, args); - } - } - - //public List getRunningServices(int maxNum) - public static class getServices extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - //TODO 需要时再说 - return super.beforeInvoke(target, method, args); - } - } - - public static class getIntentSender extends MethodDelegate { - - public static final int INTENT_SENDER_BROADCAST = 1; - public static final int INTENT_SENDER_ACTIVITY = 2; - public static final int INTENT_SENDER_ACTIVITY_RESULT = 3; - public static final int INTENT_SENDER_SERVICE = 4; - - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - int type = (int)args[0]; - if (type != INTENT_SENDER_ACTIVITY_RESULT) { - for (int i = 0; i < args.length; i++) { - if (args[i] != null && args[i].getClass().isAssignableFrom(Intent[].class)) { - Intent[] intents = (Intent[])args[i]; - if (type == INTENT_SENDER_BROADCAST) { - type = PluginDescriptor.BROADCAST; - } else if (type == INTENT_SENDER_ACTIVITY) { - type = PluginDescriptor.ACTIVITY; - } else if (type == INTENT_SENDER_SERVICE) { - type = PluginDescriptor.SERVICE; - } - for(int j = 0; j < intents.length; j++) { - intents[j] = NotificationHelper.resolveNotificationIntent(intents[j], type); - } - break; - } - } - } - - return super.beforeInvoke(target, method, args); - } - } -} diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidAppINotificationManager.java b/PluginCore/src/com/plugin/core/systemservice/AndroidAppINotificationManager.java deleted file mode 100644 index a51475bd..00000000 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidAppINotificationManager.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.plugin.core.systemservice; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.os.Build; -import android.widget.RemoteViews; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginIntentResolver; -import com.plugin.core.PluginLoader; -import com.plugin.core.PluginPublicXmlConst; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.core.proxy.MethodDelegate; -import com.plugin.core.proxy.MethodProxy; -import com.plugin.core.proxy.ProxyUtil; -import com.plugin.util.FileUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.io.File; -import java.lang.reflect.Method; - -/** - * Created by cailiming on 16/1/15. - */ -public class AndroidAppINotificationManager extends MethodProxy { - - private AndroidAppINotificationManager() { - } - - public static void installProxy() { - LogUtil.d("安装NotificationManagerProxy"); - Object androidAppINotificationStubProxy = RefInvoker.invokeStaticMethod(NotificationManager.class.getName(), "getService", (Class[])null, (Object[])null); - Object androidAppINotificationStubProxyProxy = ProxyUtil.createProxy(androidAppINotificationStubProxy, new AndroidAppINotificationManager()); - RefInvoker.setStaticOjbect(NotificationManager.class.getName(), "sService", androidAppINotificationStubProxyProxy); - LogUtil.d("安装完成"); - } - - static { - sMethods.put("enqueueNotification", new enqueueNotification()); - sMethods.put("enqueueNotificationWithTag", new enqueueNotificationWithTag()); - sMethods.put("enqueueNotificationWithTagPriority", new enqueueNotificationWithTagPriority()); - } - - public static class enqueueNotification extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - for(Object obj: args) { - if (obj instanceof Notification) { - resolveRemoteViews((Notification)obj); - break; - } - } - return super.beforeInvoke(target, method, args); - } - } - - public static class enqueueNotificationWithTag extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - for(Object obj: args) { - if (obj instanceof Notification) { - resolveRemoteViews((Notification)obj); - break; - } - } - return super.beforeInvoke(target, method, args); - } - } - - public static class enqueueNotificationWithTagPriority extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - for(Object obj: args) { - if (obj instanceof Notification) { - resolveRemoteViews((Notification)obj); - break; - } - } - return super.beforeInvoke(target, method, args); - } - } - - private static void resolveRemoteViews(Notification notification) { - if (Build.VERSION.SDK_INT >= 21) { - - int layoutId = 0; - if (notification.contentView != null) { - layoutId = (int)RefInvoker.getFieldObject(notification.contentView, RemoteViews.class, "mLayoutId"); - } - if (layoutId == 0) { - if (notification.bigContentView != null) { - layoutId = (int)RefInvoker.getFieldObject(notification.bigContentView, RemoteViews.class, "mLayoutId"); - } - } - if (layoutId != 0) { - //检查资源布局资源Id是否属于宿主 - if (PluginPublicXmlConst.resourceMap.get(layoutId>>16) == null) { - //资源是来自插件 - if (notification.contentIntent != null) { - Intent intent = (Intent)RefInvoker.invokeMethod(notification.contentIntent, PendingIntent.class.getName(), "getIntent", (Class[]) null, (Object[]) null); - if (intent.getAction() != null && intent.getAction().contains(PluginIntentResolver.CLASS_SEPARATOR)) { - String className = intent.getAction().split(PluginIntentResolver.CLASS_SEPARATOR)[0]; - //通过重新构造ApplicationInfo来附加插件资源 - PluginDescriptor pd = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pd != null) { - ApplicationInfo applicationInfo = PluginLoader.getApplication().getApplicationInfo(); - ApplicationInfo newInfo = new ApplicationInfo();//重新构造一个,而不是修改原本的 - newInfo.packageName = applicationInfo.packageName; - newInfo.sourceDir = applicationInfo.sourceDir; - newInfo.dataDir = applicationInfo.dataDir; - //要确保publicSourceDir这个路径可以被SystemUI应用读取, - newInfo.publicSourceDir = getNotificationResourcePath(pd.getInstalledPath(), PluginLoader.getApplication().getExternalCacheDir().getAbsolutePath() + "/notification_res.apk"); - if (notification.tickerView != null) { - RefInvoker.setFieldObject(notification.tickerView, RemoteViews.class.getName(), "mApplication", newInfo); - } - if (notification.contentView != null) { - RefInvoker.setFieldObject(notification.contentView, RemoteViews.class.getName(), "mApplication", newInfo); - } - if (notification.bigContentView != null) { - RefInvoker.setFieldObject(notification.bigContentView, RemoteViews.class.getName(), "mApplication", newInfo); - } - if (notification.headsUpContentView != null) { - RefInvoker.setFieldObject(notification.headsUpContentView, RemoteViews.class.getName(), "mApplication", newInfo); - } - } - } - } - } - } - } - } - - private static String getNotificationResourcePath(String pluginInstalledPath, String worldReadablePath) { - LogUtil.d("正在为通知栏准备插件资源。。。这里现在暂时是同步复制,注意大文件卡顿!!"); - File worldReadableFile = new File(worldReadablePath); - - if (FileUtil.copyFile(pluginInstalledPath, worldReadableFile.getAbsolutePath())) { - LogUtil.d("通知栏插件资源准备完成,请确保此路径SystemUi有读权限", worldReadableFile.getAbsolutePath()); - return worldReadableFile.getAbsolutePath(); - } else { - LogUtil.d("不应该到这里来,直接返回这个路径SystemUi没有权限读取"); - return pluginInstalledPath; - } - } - -} diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidAppIPackageManager.java b/PluginCore/src/com/plugin/core/systemservice/AndroidAppIPackageManager.java deleted file mode 100644 index 9be82133..00000000 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidAppIPackageManager.java +++ /dev/null @@ -1,360 +0,0 @@ -package com.plugin.core.systemservice; - -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; - -import com.plugin.content.PluginActivityInfo; -import com.plugin.content.PluginDescriptor; -import com.plugin.content.PluginProviderInfo; -import com.plugin.core.PluginLoader; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.core.manager.PluginManagerProvider; -import com.plugin.core.proxy.MethodDelegate; -import com.plugin.core.proxy.MethodProxy; -import com.plugin.core.proxy.ProxyUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; -import com.plugin.util.ResourceUtil; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Created by cailiming on 16/1/15. - */ -public class AndroidAppIPackageManager extends MethodProxy { - - public static void installProxy(PackageManager manager) { - LogUtil.d("安装PackageManagerProxy"); - Object androidAppIPackageManagerStubProxy = RefInvoker.getStaticFieldObject("android.app.ActivityThread", "sPackageManager"); - Object androidAppIPackageManagerStubProxyProxy = ProxyUtil.createProxy(androidAppIPackageManagerStubProxy, new AndroidAppIPackageManager()); - RefInvoker.setStaticOjbect("android.app.ActivityThread", "sPackageManager", androidAppIPackageManagerStubProxyProxy); - RefInvoker.setFieldObject(manager, "android.app.ApplicationPackageManager", "mPM", androidAppIPackageManagerStubProxyProxy); - LogUtil.d("安装完成"); - } - - static { - sMethods.put("getInstalledPackages", new getInstalledPackages()); - sMethods.put("getPackageInfo", new getPackageInfo()); - sMethods.put("getApplicationInfo", new getApplicationInfo()); - sMethods.put("getActivityInfo", new getActivityInfo()); - sMethods.put("getReceiverInfo", new getReceiverInfo()); - sMethods.put("getServiceInfo", new getServiceInfo()); - sMethods.put("getProviderInfo", new getProviderInfo()); - sMethods.put("queryIntentActivities", new queryIntentActivities()); - sMethods.put("queryIntentServices", new queryIntentServices()); - sMethods.put("resolveActivity", new resolveActivity()); - sMethods.put("resolveActivityAsUser", new resolveActivityAsUser()); - sMethods.put("resolveService", new resolveService()); - sMethods.put("getComponentEnabledSetting", new getComponentEnabledSetting()); - } - - public static class getPackageInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - String packageName = (String)args[0]; - LogUtil.d("beforeInvoke", method.getName(), packageName); - if (!packageName.equals(PluginLoader.getApplication().getPackageName())) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); - if (pluginDescriptor != null) { - return PluginLoader.getApplication().getPackageManager().getPackageArchiveInfo(pluginDescriptor.getInstalledPath(), (int) args[1]); - } - } - return super.beforeInvoke(target, method, args); - } - } - - public static class getInstalledPackages extends MethodDelegate { - - @Override - public Object afterInvoke(Object target, Method method, Object[] args, Object beforeResult, Object invokeResult) { - LogUtil.d("afterInvoke", method.getName()); - - List result = (List )RefInvoker.invokeMethod(invokeResult, "android.content.pm.ParceledListSlice", "getList", (Class[])null, (Object[])null); - - Collection plugins = PluginManagerHelper.getPlugins(); - if (plugins != null) { - if (result == null) { - result = new ArrayList(); - } - for(PluginDescriptor pluginDescriptor:plugins) { - PackageInfo info = PluginLoader.getApplication().getPackageManager().getPackageArchiveInfo(pluginDescriptor.getInstalledPath(), (int) args[0]); - result.add(info); - } - } - - return invokeResult; - } - } - - public static class queryIntentActivities extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); - if (classNames != null && classNames.size() > 0) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - List result = new ArrayList<>(); - ResolveInfo info = new ResolveInfo(); - result.add(info); - info.activityInfo = getActivityInfo(pluginDescriptor, classNames.get(0)); - return result; - } - return super.beforeInvoke(target, method, args); - } - - } - - public static class getApplicationInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - String packageName = (String)args[0]; - LogUtil.d("beforeInvoke", method.getName(), packageName); - if (!packageName.equals(PluginLoader.getApplication().getPackageName())) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByPluginId(packageName); - if (pluginDescriptor != null) { - return getApplicationInfo(pluginDescriptor); - } - } - return super.beforeInvoke(target, method, args); - } - } - - public static class getActivityInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - String className = ((ComponentName)args[0]).getClassName(); - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pluginDescriptor != null) { - return getActivityInfo(pluginDescriptor, className); - } - return super.beforeInvoke(target, method, args); - } - - } - - public static class getReceiverInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - String className = ((ComponentName)args[0]).getClassName(); - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pluginDescriptor != null) { - return getActivityInfo(pluginDescriptor, className); - } - return super.beforeInvoke(target, method, args); - } - - } - - public static class getServiceInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - String className = ((ComponentName)args[0]).getClassName(); - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pluginDescriptor != null) { - return getServiceInfo(pluginDescriptor, className); - } - - return super.beforeInvoke(target, method, args); - } - } - - public static class getProviderInfo extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - String className = ((ComponentName)args[0]).getClassName(); - if (!className.equals(PluginManagerProvider.class.getName())) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className); - if (pluginDescriptor != null) { - PluginProviderInfo info = pluginDescriptor.getProviderInfos().get(className); - ProviderInfo providerInfo = new ProviderInfo(); - providerInfo.name = info.getName(); - providerInfo.packageName = getPackageName(pluginDescriptor); - providerInfo.icon = pluginDescriptor.getApplicationIcon(); - providerInfo.metaData = pluginDescriptor.getMetaData(); - providerInfo.enabled = true; - providerInfo.exported = info.isExported(); - providerInfo.applicationInfo = getApplicationInfo(pluginDescriptor); - providerInfo.authority = info.getAuthority(); - return providerInfo; - } - } - return super.beforeInvoke(target, method, args); - } - } - - public static class queryIntentServices extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.SERVICE); - if (classNames != null && classNames.size() > 0) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - List result = new ArrayList<>(); - ResolveInfo info = new ResolveInfo(); - result.add(info); - info.serviceInfo = getServiceInfo(pluginDescriptor, classNames.get(0)); - return result; - } - return super.beforeInvoke(target, method, args); - } - } - - public static class resolveIntent extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); - if (classNames != null && classNames.size() > 0) { - List result = new ArrayList<>(); - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - ResolveInfo info = new ResolveInfo(); - result.add(info); - info.activityInfo = getActivityInfo(pluginDescriptor, classNames.get(0)); - return result; - } - return super.beforeInvoke(target, method, args); - } - } - - public static class resolveService extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.SERVICE); - if (classNames != null && classNames.size() > 0) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - ResolveInfo info = new ResolveInfo(); - info.serviceInfo = getServiceInfo(pluginDescriptor, classNames.get(0)); - return info; - } - return super.beforeInvoke(target, method, args); - } - } - - //public abstract ResolveInfo resolveActivity(Intent intent, int flags); - public static class resolveActivity extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); - if (classNames != null && classNames.size() > 0) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - ResolveInfo info = new ResolveInfo(); - info.activityInfo = getActivityInfo(pluginDescriptor, classNames.get(0)); - return info; - } - return super.beforeInvoke(target, method, args); - } - } - - //public abstract ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId); - public static class resolveActivityAsUser extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.d("beforeInvoke", method.getName()); - ArrayList classNames = PluginLoader.matchPlugin((Intent) args[0], PluginDescriptor.ACTIVITY); - if (classNames != null && classNames.size() > 0) { - PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(classNames.get(0)); - ResolveInfo info = new ResolveInfo(); - info.activityInfo = getActivityInfo(pluginDescriptor, classNames.get(0)); - return info; - } - return super.beforeInvoke(target, method, args); - } - } - - public static class getComponentEnabledSetting extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - Object arg0 = args[0]; - if (arg0 instanceof ComponentName) { - ComponentName mComponentName = ((ComponentName) args[0]); - - LogUtil.d("beforeInvoke", method.getName(), mComponentName.getPackageName(), mComponentName.getClassName()); - - if ("com.htc.android.htcsetupwizard".equalsIgnoreCase(mComponentName.getPackageName())) { - return PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - } - } else { - LogUtil.d("beforeInvoke", method.getName(), arg0); - } - - return super.beforeInvoke(target, method, args); - } - } - - private static ApplicationInfo getApplicationInfo(PluginDescriptor pluginDescriptor) { - ApplicationInfo info = new ApplicationInfo(); - info.packageName = getPackageName(pluginDescriptor); - info.metaData = pluginDescriptor.getMetaData(); - info.name = pluginDescriptor.getApplicationName(); - info.className = pluginDescriptor.getApplicationName(); - info.enabled = true; - info.processName = null;//需要时再添加 - info.sourceDir = pluginDescriptor.getInstalledPath(); - //info.uid == Process.myUid(); - info.publicSourceDir = pluginDescriptor.getInstalledPath(); - info.taskAffinity = null;//需要时再加上 - info.dataDir = null;//需要时再添加 - info.theme = pluginDescriptor.getApplicationTheme(); - info.flags = info.flags | ApplicationInfo.FLAG_HAS_CODE; - //需要时再添加 - //info.nativeLibraryDir = new File(pluginDescriptor.getInstalledPath()).getParentFile().getAbsolutePath() + "/lib"; - return info; - } - - private static ActivityInfo getActivityInfo(PluginDescriptor pluginDescriptor, String className) { - ActivityInfo activityInfo = new ActivityInfo(); - activityInfo.name = className; - activityInfo.packageName = getPackageName(pluginDescriptor); - activityInfo.icon = pluginDescriptor.getApplicationIcon(); - activityInfo.metaData = pluginDescriptor.getMetaData(); - activityInfo.enabled = true; - activityInfo.exported = false; - activityInfo.applicationInfo = getApplicationInfo(pluginDescriptor); - activityInfo.taskAffinity = null;//需要时再加上 - //activityInfo.targetActivity = - - if (pluginDescriptor.getType(className) == PluginDescriptor.ACTIVITY) { - PluginActivityInfo detail = pluginDescriptor.getActivityInfos().get(className); - activityInfo.launchMode = Integer.valueOf(detail.getLaunchMode()); - activityInfo.theme = ResourceUtil.getResourceId(detail.getTheme()); - if (detail.getUiOptions() != null) { - activityInfo.uiOptions = Integer.parseInt(detail.getUiOptions().replace("0x", ""), 16); - } - } - return activityInfo; - } - - private static ServiceInfo getServiceInfo(PluginDescriptor pluginDescriptor, String className) { - ServiceInfo serviceInfo = new ServiceInfo(); - serviceInfo.name = className; - serviceInfo.packageName = getPackageName(pluginDescriptor); - serviceInfo.icon = pluginDescriptor.getApplicationIcon(); - serviceInfo.metaData = pluginDescriptor.getMetaData(); - serviceInfo.enabled = true; - serviceInfo.exported = false; - serviceInfo.applicationInfo = getApplicationInfo(pluginDescriptor); - return serviceInfo; - } - - private static String getPackageName(PluginDescriptor pluginDescriptor) { - //这里的packageName可能需要使用宿主的packageName, - return pluginDescriptor.getPackageName(); - } - -} diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidWebkitWebViewFactoryProvider.java b/PluginCore/src/com/plugin/core/systemservice/AndroidWebkitWebViewFactoryProvider.java deleted file mode 100644 index e4baf711..00000000 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidWebkitWebViewFactoryProvider.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.plugin.core.systemservice; - -import android.app.Activity; -import android.content.Context; -import android.os.Build; -import android.webkit.WebView; - -import com.plugin.core.proxy.MethodDelegate; -import com.plugin.core.proxy.MethodProxy; -import com.plugin.core.proxy.ProxyUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Created by cailiming on 16/1/28. - */ -public class AndroidWebkitWebViewFactoryProvider extends MethodProxy { - - public static void installProxy() { - if (Build.VERSION.SDK_INT >= 19) { - LogUtil.d("安装WebViewFactoryProviderProxy"); - //在4。4及以上,这里的WebViewFactoryProvider的实际类型是 - // com.android.webview.chromium.WebViewChromiumFactoryProvider implements WebViewFactoryProvider - Object webViewFactoryProvider = RefInvoker.invokeMethod(null, "android.webkit.WebViewFactory", "getProvider", (Class[]) null, (Object[]) null); - Object webViewFactoryProviderProxy = ProxyUtil.createProxy(webViewFactoryProvider, new AndroidWebkitWebViewFactoryProvider()); - RefInvoker.setStaticOjbect("android.webkit.WebViewFactory", "sProviderInstance", webViewFactoryProviderProxy); - LogUtil.d("安装完成"); - } - } - - static { - sMethods.put("createWebView", new createWebView()); - } - - public static class createWebView extends MethodDelegate { - - @Override - public Object afterInvoke(Object target, Method method, Object[] args, Object beforeInvoke, final Object invokeResult) { - //这里invokeResult的实际类型是 - // com.android.webview.chromium.WebViewChromium implements WebViewProvider - //所以这里可以再次进行Proxy - final WebView webView = (WebView) args[0]; - fixWebViewAsset(webView.getContext()); - return super.afterInvoke(target, method, args, beforeInvoke, invokeResult); -// return ProxyUtil.createProxy(invokeResult, new MethodDelegate() { -// -// @Override -// public Object beforeInvoke(Object target, Method method, Object[] args) { -// fixWebViewAsset(webView.getContext()); -// return super.beforeInvoke(target, method, args); -// } -// -// }); - } - } - - public static void switchWebViewContext(Context pluginActivity) { - WebView wb = new WebView(pluginActivity); - wb.loadUrl(""); - } - - private static void fixWebViewAsset(Context context) { - try { - if (sContentMain == null) { - Object provider = RefInvoker.invokeMethod(null, "android.webkit.WebViewFactory", "getProvider", (Class[]) null, (Object[]) null); - if (provider != null) { - ClassLoader cl = provider.getClass().getClassLoader(); - - try { - sContentMain = Class.forName("org.chromium.content.app.ContentMain", true, cl); - } catch (ClassNotFoundException e) { - } - - if (sContentMain == null) { - try { - sContentMain = Class.forName("com.android.org.chromium.content.app.ContentMain", true, cl); - } catch (ClassNotFoundException e) { - } - } - - if (sContentMain == null) { - throw new ClassNotFoundException(String.format("Can not found class %s or %s in classloader %s", "org.chromium.content.app.ContentMain", "com.android.org.chromium.content.app.ContentMain", cl)); - } - } - } - if (sContentMain != null) { - RefInvoker.invokeMethod(null, sContentMain, "initApplicationContext", new Class[]{Context.class}, new Object[]{context.getApplicationContext()}); - } - } catch (Exception e) { - LogUtil.printException("createWebview", e); - } - } - - private static Class sContentMain; - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/core/systemservice/AndroidWidgetToast.java b/PluginCore/src/com/plugin/core/systemservice/AndroidWidgetToast.java deleted file mode 100644 index 652ecb59..00000000 --- a/PluginCore/src/com/plugin/core/systemservice/AndroidWidgetToast.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.plugin.core.systemservice; - -import android.app.NotificationManager; -import android.widget.Toast; - -import com.plugin.core.PluginLoader; -import com.plugin.core.proxy.MethodDelegate; -import com.plugin.core.proxy.MethodProxy; -import com.plugin.core.proxy.ProxyUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.RefInvoker; - -import java.lang.reflect.Method; - -/** - * Created by cailiming on 16/1/15. - */ -public class AndroidWidgetToast extends MethodProxy { - - private AndroidWidgetToast() { - } - - public static void installProxy() { - LogUtil.d("安装NotificationManagerProxy"); - Object androidAppINotificationStubProxy = RefInvoker.invokeStaticMethod(Toast.class.getName(), "getService", (Class[])null, (Object[])null); - Object androidAppINotificationStubProxyProxy = ProxyUtil.createProxy(androidAppINotificationStubProxy, new AndroidWidgetToast()); - RefInvoker.setStaticOjbect(NotificationManager.class.getName(), "sService", androidAppINotificationStubProxyProxy); - LogUtil.d("安装完成"); - } - - static { - sMethods.put("enqueueToast", new enqueueToast()); - sMethods.put("cancelToast", new cancelToast()); - } - - public static class enqueueToast extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - args[0] = PluginLoader.getApplication().getPackageName(); - return super.beforeInvoke(target, method, args); - } - } - - public static class cancelToast extends MethodDelegate { - @Override - public Object beforeInvoke(Object target, Method method, Object[] args) { - LogUtil.e("beforeInvoke", method.getName()); - args[0] = PluginLoader.getApplication().getPackageName(); - return super.beforeInvoke(target, method, args); - } - } - -} diff --git a/PluginCore/src/com/plugin/util/ProcessUtil.java b/PluginCore/src/com/plugin/util/ProcessUtil.java deleted file mode 100644 index 23cbad72..00000000 --- a/PluginCore/src/com/plugin/util/ProcessUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.plugin.util; - -import android.app.ActivityManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.os.Build; - -import com.plugin.core.PluginLoader; -import com.plugin.core.manager.PluginManagerProvider; - -public class ProcessUtil { - - private static Boolean isPluginProcess; - - public static boolean isPluginProcess() { - - if (isPluginProcess == null) { - Context context = PluginLoader.getApplication(); - String processName = getCurProcessName(context); - String pluginProcessName = getPluginProcessName(context); - - isPluginProcess = processName.equals(pluginProcessName); - } - return isPluginProcess; - } - - private static String getCurProcessName(Context context) { - ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - for (ActivityManager.RunningAppProcessInfo appProcess : activityManager.getRunningAppProcesses()) { - if (appProcess.pid == android.os.Process.myPid()) { - return appProcess.processName; - } - } - return ""; - } - - private static String getPluginProcessName(Context context) { - try { - if (Build.VERSION.SDK_INT >= 9) { - ProviderInfo pinfo = context.getPackageManager().getProviderInfo(new ComponentName(context, PluginManagerProvider.class), 0); - return pinfo.processName; - } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return ""; - } -} diff --git a/PluginCore/src/com/plugin/util/RefInvoker.java b/PluginCore/src/com/plugin/util/RefInvoker.java deleted file mode 100644 index 610472fc..00000000 --- a/PluginCore/src/com/plugin/util/RefInvoker.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.plugin.util; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -@SuppressWarnings("unchecked") -public class RefInvoker { - - @SuppressWarnings("rawtypes") - public static Object invokeStaticMethod(String className, String methodName, Class[] paramTypes, - Object[] paramValues) { - - return invokeMethod(null, className, methodName, paramTypes, paramValues); - } - - @SuppressWarnings("rawtypes") - public static Object invokeMethod(Object target, String className, String methodName, Class[] paramTypes, - Object[] paramValues) { - - try { - Class clazz = Class.forName(className); - return invokeMethod(target, clazz, methodName, paramTypes, paramValues); - }catch (ClassNotFoundException e) { - e.printStackTrace(); - } - return null; - } - - public static Object invokeMethod(Object target, Class clazz, String methodName, Class[] paramTypes, - Object[] paramValues) { - try { - //LogUtil.e("Method", methodName); - Method method = clazz.getDeclaredMethod(methodName, paramTypes); - if (!method.isAccessible()) { - method.setAccessible(true); - } - return method.invoke(target, paramValues); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - return null; - } - - @SuppressWarnings("rawtypes") - public static Object getFieldObject(Object target, Class clazz, String fieldName) { - try { - Field field = clazz.getDeclaredField(fieldName); - if (!field.isAccessible()) { - field.setAccessible(true); - } - return field.get(target); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchFieldException e) { - // try supper for Miui, Miui has a class named MiuiPhoneWindow - try { - Field field = clazz.getSuperclass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(target); - } catch (Exception superE) { - e.printStackTrace(); - superE.printStackTrace(); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - return null; - - } - - @SuppressWarnings("rawtypes") - public static Object getFieldObject(Object target, String className, String fieldName) { - Class clazz = null; - try { - clazz = Class.forName(className); - return getFieldObject(target, clazz, fieldName); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - return null; - } - - public static Object getStaticFieldObject(String className, String fieldName) { - - return getFieldObject(null, className, fieldName); - } - - @SuppressWarnings("rawtypes") - public static void setFieldObject(Object target, String className, String fieldName, Object fieldValue) { - Class clazz = null; - try { - clazz = Class.forName(className); - setFieldObject(target, clazz, fieldName, fieldValue); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - - public static void setFieldObject(Object target, Class clazz, String fieldName, Object fieldValue) { - try { - Field field = clazz.getDeclaredField(fieldName); - if (!field.isAccessible()) { - field.setAccessible(true); - } - field.set(target, fieldValue); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchFieldException e) { - // try supper for Miui, Miui has a class named MiuiPhoneWindow - try { - Field field = clazz.getSuperclass().getDeclaredField(fieldName); - if (!field.isAccessible()) { - field.setAccessible(true); - } - field.set(target, fieldValue); - } catch (Exception superE) { - e.printStackTrace(); - superE.printStackTrace(); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - - public static void setStaticOjbect(String className, String fieldName, Object fieldValue) { - setFieldObject(null, className, fieldName, fieldValue); - } - - public static Method findMethod(Object object, String methodName, Class[] clazzes) { - try { - return object.getClass().getDeclaredMethod(methodName, clazzes); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - return null; - } - - public static Method findMethod(Object object, String methodName, Object[] args) { - if (args == null) { - try { - return object.getClass().getDeclaredMethod(methodName, (Class[])null); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - return null; - } else { - Method[] methods = object.getClass().getDeclaredMethods(); - boolean isFound = false; - Method method = null; - for(Method m: methods) { - if (m.getName().equals(methodName)) { - Class[] types = m.getParameterTypes(); - if (types.length == args.length) { - isFound = true; - for(int i = 0; i < args.length; i++) { - if (!(types[i] == args[i].getClass() || (types[i].isPrimitive() && primitiveToWrapper(types[i]) == args[i].getClass()))) { - isFound = false; - break; - } - } - if (isFound) { - method = m; - break; - } - } - } - } - return method; - } - } - - private static final Map, Class> primitiveWrapperMap = new HashMap, Class>(); - - static { - primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); - primitiveWrapperMap.put(Byte.TYPE, Byte.class); - primitiveWrapperMap.put(Character.TYPE, Character.class); - primitiveWrapperMap.put(Short.TYPE, Short.class); - primitiveWrapperMap.put(Integer.TYPE, Integer.class); - primitiveWrapperMap.put(Long.TYPE, Long.class); - primitiveWrapperMap.put(Double.TYPE, Double.class); - primitiveWrapperMap.put(Float.TYPE, Float.class); - primitiveWrapperMap.put(Void.TYPE, Void.TYPE); - } - - static Class primitiveToWrapper(final Class cls) { - Class convertedClass = cls; - if (cls != null && cls.isPrimitive()) { - convertedClass = primitiveWrapperMap.get(cls); - } - return convertedClass; - } - -} \ No newline at end of file diff --git a/PluginCore/src/com/plugin/util/ResourceUtil.java b/PluginCore/src/com/plugin/util/ResourceUtil.java deleted file mode 100644 index 7b494fc4..00000000 --- a/PluginCore/src/com/plugin/util/ResourceUtil.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.plugin.util; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Bundle; - -import com.plugin.content.PluginDescriptor; -import com.plugin.core.PluginLoader; -import com.plugin.core.PluginPublicXmlConst; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Created by cailiming - */ -public class ResourceUtil { - - public static String getString(String value, Context pluginContext) { - String idHex = null; - if (value != null && value.startsWith("@") && value.length() == 9) { - idHex = value.replace("@", ""); - - } else if (value != null && value.startsWith("@android:") && value.length() == 17) { - idHex = value.replace("@android:", ""); - } - - if (idHex != null) { - try { - int id = Integer.parseInt(idHex, 16); - //此时context可能还没有初始化 - if (pluginContext != null) { - String des = pluginContext.getString(id); - return des; - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - return value; - } - - public static Boolean getBoolean(String value, Context pluginContext) { - String idHex = null; - if (value != null && value.startsWith("@") && value.length() == 9) { - idHex = value.replace("@", ""); - - } else if (value != null && value.startsWith("@android:") && value.length() == 17) { - idHex = value.replace("@android:", ""); - } - - if (idHex != null) { - try { - int id = Integer.parseInt(idHex, 16); - //此时context可能还没有初始化 - if (pluginContext != null) { - return pluginContext.getResources().getBoolean(id); - } - } catch (Exception e) { - e.printStackTrace(); - } - } else if (value != null) { - return Boolean.parseBoolean(value); - } - - return null; - } - - public static int getResourceId(String value) { - String idHex = null; - if (value != null && value.startsWith("@") && value.length() == 9) { - idHex = value.replace("@", ""); - - } else if (value != null && value.startsWith("@android:") && value.length() == 17) { - idHex = value.replace("@android:", ""); - } - if (idHex != null) { - try { - int id = Integer.parseInt(idHex, 16); - return id; - } catch (Exception e) { - e.printStackTrace(); - } - } - return 0; - } - - public static String getLabel(PluginDescriptor pd) { - PackageManager pm = PluginLoader.getApplication().getPackageManager(); - PackageInfo info = pm.getPackageArchiveInfo(pd.getInstalledPath(), PackageManager.GET_ACTIVITIES); - if (info != null) { - ApplicationInfo appInfo = info.applicationInfo; - appInfo.sourceDir = pd.getInstalledPath(); - appInfo.publicSourceDir = pd.getInstalledPath(); - String label = null; - try { - if (!isMainResId(appInfo.labelRes)){ - label = pm.getApplicationLabel(appInfo).toString(); - } - } catch (Resources.NotFoundException e) { - } - if (label == null || label.equals(pd.getPackageName())) { - //可能设置的lable是来自宿主的资源 - if (pd.getDescription() != null) { - int id = ResourceUtil.getResourceId(pd.getDescription()); - if (id != 0) { - //再宿主中查一次 - try { - label = PluginLoader.getApplication().getResources().getString(id); - } catch (Resources.NotFoundException e) { - } - } - } - } - if (label != null) { - return label; - } - } - return pd.getDescription(); - } - - public static Bundle getApplicationMetaData(String apkPath) { - //暂时只查询Applicatoin节点下的meta信息,其他组件节点下的meta先不管 - PackageInfo info = PluginLoader.getApplication().getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_META_DATA); - if (info.applicationInfo != null) { - return info.applicationInfo.metaData; - } - return null; - } - - @TargetApi(Build.VERSION_CODES.GINGERBREAD) - public static Drawable getLogo(PluginDescriptor pd) { - PackageManager pm = PluginLoader.getApplication().getPackageManager(); - PackageInfo info = pm.getPackageArchiveInfo(pd.getInstalledPath(), PackageManager.GET_ACTIVITIES); - if (info != null) { - ApplicationInfo appInfo = info.applicationInfo; - appInfo.sourceDir = pd.getInstalledPath(); - appInfo.publicSourceDir = pd.getInstalledPath(); - Drawable logo = pm.getApplicationLogo(appInfo); - return logo; - } - return null; - } - - public static boolean isMainResId(int resid) { - //如果使用的使openatlasextention - //默认宿主的资源id以0x7f3X开头 - return PluginPublicXmlConst.resourceMap.get(resid>>16) != null; - } - - public static void rewriteRValues(ClassLoader cl, String packageName, int id) { - final Class rClazz; - try { - rClazz = cl.loadClass(packageName + ".R"); - } catch (ClassNotFoundException e) { - LogUtil.d("No resource references to update in package " + packageName); - return; - } - - final Method callback; - try { - callback = rClazz.getMethod("onResourcesLoaded", int.class); - } catch (NoSuchMethodException e) { - // No rewriting to be done. - return; - } - - Throwable cause; - try { - callback.invoke(null, id); - return; - } catch (IllegalAccessException e) { - cause = e; - } catch (InvocationTargetException e) { - cause = e.getCause(); - } - - throw new RuntimeException("Failed to rewrite resource references for " + packageName, - cause); - } -} diff --git a/PluginHelloWorld/build.gradle b/PluginHelloWorld/build.gradle deleted file mode 100644 index ab0eb976..00000000 --- a/PluginHelloWorld/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - applicationId "com.example.pluginhelloworld" - minSdkVersion 14 - targetSdkVersion 22 - versionCode 2 - versionName "1.0.1" - - //for 1.5.0- - //generatedDensities = [] - - //for 2.0.0+ - //vectorDrawables.useSupportLibrary = true; - } - - lintOptions { - abortOnError false - } - - aaptOptions { - //for 1.5.0- Flag that tells aapt to keep attribute ids - //additionalParameters "--no-version-vectors" - } - -} - -dependencies { - compile 'com.android.support:support-v4:22.1.1' - compile 'com.android.support:appcompat-v7:22.1.1' -} - -build.doLast { - //将编译好的插件apk复制到Main工程的assets目录下, 便于测试 - copy { - println 'copy plugin apk to assets... ' + buildDir.absolutePath + '/outputs/apk/PluginHelloWorld-debug.apk' - from(buildDir.absolutePath + '/outputs/apk/') { - include 'PluginHelloWorld-debug.apk' - } - into(project(':PluginMain').getProjectDir().absolutePath + '/assets/') - } -} \ No newline at end of file diff --git a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java b/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java deleted file mode 100644 index 7f7e3289..00000000 --- a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.pluginhelloworld; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -import com.example.hellojni.HelloJni; - -/** - * 独立插件测试demo - */ -public class WelcomeActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ActionBar actionBar = getSupportActionBar(); - actionBar.setTitle("这是App首屏"); - actionBar.setSubtitle("这是副标题"); - actionBar.setLogo(R.drawable.ic_launcher); - actionBar.setIcon(R.drawable.ic_launcher); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP - | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); - - - try { - ApplicationInfo info = getPackageManager().getApplicationInfo("com.example.pluginhelloworld", PackageManager.GET_META_DATA); - String hellowMeta = (String)info.metaData.get("hello_meta"); - Toast.makeText(this, hellowMeta, Toast.LENGTH_SHORT).show(); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - - Log.e("xxx1", "activity_welcome ID= " + R.layout.activity_welcome); - Log.e("xxx2", getResources().getResourceEntryName(R.layout.activity_welcome)); - Log.e("xxx3", getResources().getString(R.string.app_name)); - Log.e("xxx4", getPackageName() + ", " + getText(R.string.app_name)); - Log.e("xxx5", getResources().getString(android.R.string.httpErrorBadUrl)); - Log.e("xxx6", getResources().getString(getResources().getIdentifier("app_name", "string", "com.example.pluginhelloworld"))); - Log.e("xxx7", getResources().getString(getResources().getIdentifier("app_name", "string", getPackageName()))); - - setContentView(R.layout.activity_welcome); - - findViewById(R.id.test_s_btn).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - Toast.makeText(WelcomeActivity.this, "测试JNI:3 + 4 = " + HelloJni.calculate(3, 4), Toast.LENGTH_LONG).show(); - } - }); - - findViewById(R.id.test_switch_btn).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(WelcomeActivity.this, MainActivity.class)); - } - }); - - findViewById(R.id.test_transparent_btn).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(WelcomeActivity.this, TransparentActivity.class)); - } - }); - - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_welcome, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } -} diff --git a/PluginHelloWorld/src/main/jniLibs/armeabi-v7a/libhello-jni.so b/PluginHelloWorld/src/main/jniLibs/armeabi-v7a/libhello-jni.so deleted file mode 100644 index ab6ca970..00000000 Binary files a/PluginHelloWorld/src/main/jniLibs/armeabi-v7a/libhello-jni.so and /dev/null differ diff --git a/PluginHelloWorld/src/main/res/drawable/ic_launcher.png b/PluginHelloWorld/src/main/res/drawable/ic_launcher.png deleted file mode 100644 index d4fb7cd9..00000000 Binary files a/PluginHelloWorld/src/main/res/drawable/ic_launcher.png and /dev/null differ diff --git a/PluginHelloWorld/src/main/res/values/styles.xml b/PluginHelloWorld/src/main/res/values/styles.xml deleted file mode 100644 index e84bc5ec..00000000 --- a/PluginHelloWorld/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/PluginMain/.gitignore b/PluginMain/.gitignore deleted file mode 100644 index 1ec69fab..00000000 --- a/PluginMain/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -gen/ \ No newline at end of file diff --git a/PluginMain/AndroidManifest.xml b/PluginMain/AndroidManifest.xml deleted file mode 100644 index b5c9fc5a..00000000 --- a/PluginMain/AndroidManifest.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/PluginMain/PluginMain-debug.apk b/PluginMain/PluginMain-debug.apk deleted file mode 100644 index 693853d7..00000000 Binary files a/PluginMain/PluginMain-debug.apk and /dev/null differ diff --git a/PluginMain/assets/PluginBase-debug.apk b/PluginMain/assets/PluginBase-debug.apk deleted file mode 100644 index 7cbd6f93..00000000 Binary files a/PluginMain/assets/PluginBase-debug.apk and /dev/null differ diff --git a/PluginMain/assets/PluginHelloWorld-debug.apk b/PluginMain/assets/PluginHelloWorld-debug.apk deleted file mode 100644 index c1afff88..00000000 Binary files a/PluginMain/assets/PluginHelloWorld-debug.apk and /dev/null differ diff --git a/PluginMain/assets/PluginTest-debug.apk b/PluginMain/assets/PluginTest-debug.apk deleted file mode 100644 index 3f08ed33..00000000 Binary files a/PluginMain/assets/PluginTest-debug.apk and /dev/null differ diff --git a/PluginMain/assets/wxsdklibrary-debug.apk b/PluginMain/assets/wxsdklibrary-debug.apk deleted file mode 100644 index 77738048..00000000 Binary files a/PluginMain/assets/wxsdklibrary-debug.apk and /dev/null differ diff --git a/PluginMain/build.gradle b/PluginMain/build.gradle deleted file mode 100644 index acc7b306..00000000 --- a/PluginMain/build.gradle +++ /dev/null @@ -1,403 +0,0 @@ -import org.xml.sax.Attributes -import org.xml.sax.SAXException -import org.xml.sax.helpers.DefaultHandler - -import javax.xml.parsers.ParserConfigurationException -import javax.xml.parsers.SAXParser -import javax.xml.parsers.SAXParserFactory -import java.io.*; -import java.util.HashSet; -import java.util.Stack; - -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - aaptOptions { - additionalParameters "-P", project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml" - } - - packagingOptions { - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } - - defaultConfig { - applicationId "com.example.pluginmain" - minSdkVersion 8 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } - - lintOptions { - checkReleaseBuilds false - abortOnError false - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - jniLibs.srcDirs = ['libs'] - renderscript.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] - } - - // Move the tests to tests/java, tests/res, etc... - instrumentTest.setRoot('tests') - - // Move the build types to build-types/ - // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... - // This moves them out of them default location under src//... which would - // conflict with src/ being used by the main source set. - // Adding new build types or product flavors should be accompanied - // by a similar customization. - debug.setRoot('build-types/debug') - release.setRoot('build-types/release') - } - - signingConfigs { - debug { - storeFile file("limpoxe.keystore") - storePassword "123456" - keyAlias "limpoxe" - keyPassword "123456" - } - - release { - storeFile file("limpoxe.keystore") - storePassword "123456" - keyAlias "limpoxe" - keyPassword "123456" - } - } - - buildTypes { - debug { - debuggable true - signingConfig signingConfigs.release - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - release { - debuggable true - signingConfig signingConfigs.release - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - //compile 'com.android.support:appcompat-v7:23.0.0' - compile fileTree(dir: 'libs', include: ['*.jar']) - compile project(':PluginCore') - compile project(':PluginShareLib') -} - - -tasks.whenTaskAdded { task -> - if (task.name.startsWith("merge") && task.name.endsWith("Resources") && !task.name.contains("AndroidTest")) { - - task.doLast { - copy { - from(project.getProjectDir()) { - include 'public.xml' - } - String dirName = task.name.replace("merge", "").replace("Resources", ""); - //这里最好再增加一个buildType判断,而不是写死Debug和Release两种 - if (dirName.equals("Debug") || dirName.equals("Release")) { - into("${project.buildDir}/intermediates/res/merged/" + dirName.toLowerCase() + "/values/") - } else if (dirName.endsWith("Debug")) { - into("${project.buildDir}/intermediates/res/merged/" + dirName.replace("Debug", "") + "/debug/values/") - } else if (dirName.endsWith("Release")) { - into("${project.buildDir}/intermediates/res/merged/" + dirName.replace("Release", "") + "/release/values/") - } - } - } - //task.enabled = false - } - - if (task.name.startsWith("process") && task.name.endsWith("Resources") && !task.name.contains("AndroidTest")) { - - task.doLast { - copy { - String name = task.name.replace("process", "").replace("Resources", ""); - //这里最好再增加一个buildType判断,而不是写死Debug和Release两种 - if (name.equals("Debug") || name.equals("Release")) { - name = "resources-" + name.toLowerCase() + ".ap_"; - } else if (name.endsWith("Debug")) { - name = "resources-" + name.replace("Debug", "") + "-debug.ap_"; - } else if (name.endsWith("Release")) { - name = "resources-" + name.replace("Release", "") + "-release.ap_"; - } - - from("${project.buildDir}/intermediates/res/") { - include name - } - if (task.name.endsWith("DebugResources")) { - into("${project.buildDir}/outputs/") - rename { String fileName -> - 'resources-debug.ap_' - } - } else if (task.name.endsWith("ReleaseResources")) { - into("${project.buildDir}/outputs/") - rename { String fileName -> - 'resources-release.ap_' - } - } - } - } - - //task.enabled = false - } -} - -build.doLast { - - //导出主题patch - createThemePatch(); -} - -//导出主题patch -def createThemePatch() { - File patchDir = new File(project.buildDir.absolutePath + "/outputs/theme_patch"); - patchDir.mkdirs(); - - String parenDir = "${project.buildDir}/intermediates/res/merged/debug/"; - XmlHandler.exportThemeStyle(new File(parenDir + "values/values.xml"), - new File(patchDir, "patch_values.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v11/values-v11.xml"), - new File(patchDir, "patch_values-v11.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v12/values-v12.xml"), - new File(patchDir, "patch_values-v12.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v13/values-v13.xml"), - new File(patchDir, "patch_values-v13.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v14/values-v14.xml"), - new File(patchDir, "patch_values-v14.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v17/values-v17.xml"), - new File(patchDir, "patch_values-v17.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v18/values-18.xml"), - new File(patchDir, "patch_values-v18.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v21/values-v21.xml"), - new File(patchDir, "patch_values-v21.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v22/values-v22.xml"), - new File(patchDir, "patch_values-v22.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(parenDir + "values-v23/values-v23.xml"), - new File(patchDir, "patch_values-v23.xml"), android.defaultConfig.applicationId) - - XmlHandler.exportThemeStyle(new File(project.buildDir.absolutePath + "/outputs/generated_exported_all_resouces.xml"), - new File(patchDir, "public_theme.xml"), android.defaultConfig.applicationId) -} - -//http://unclechen.github.io/2015/10/25/Gradle%E5%AE%9E%E8%B7%B5%E4%B9%8B%E6%89%93%E5%8C%85jar+Log%E5%BC%80%E5%85%B3%E8%87%AA%E5%8A%A8%E5%85%B3%E9%97%AD/ -//自定义混淆配置 -//def androidSDKDir = plugins.getPlugin('com.android.library').sdkHandler.getSdkFolder() -//def androidJarDir = androidSDKDir.toString() + '/platforms/' + "${android.compileSdkVersion}" + '/android.jar' - -//task proguardMyLib(type: proguard.gradle.ProGuardTask, dependsOn: ['jarMyLib']) { -// injars('build/libs/my-lib.jar') -// outjars('build/libs/my-pro-lib.jar') -// libraryjars(androidJarDir) -// configuration 'proguard-rules.pro' -//} - -/////////////////////// -/////////////////////// -/////////////////////// -/////////////////////// -public class XmlHandler extends DefaultHandler { - - public static void exportThemeStyle(File srcFile, File destFile, String packageName) { - try { - SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); - saxParser.parse(new FileInputStream(srcFile), new XmlHandler(destFile, packageName)); - } catch (ParserConfigurationException e) { - System.out.println(e.getMessage()); - } catch (SAXException e) { - System.out.println(e.getMessage()); - } catch (FileNotFoundException e) { - System.out.println(e.getMessage()); - } catch (IOException e) { - System.out.println(e.getMessage()); - } - - } - - //////////////// - //////////////// - //////////////// - - File destFile; - String packageName; - Stack stack = new Stack(); - BufferedWriter outXmlStream = null; - - HashSet attrSets = new HashSet<>(); - - public XmlHandler(File destFile, String packageName) { - this.destFile = destFile; - this.packageName = packageName; - } - - public void startDocument() throws SAXException { - try { - outXmlStream = new BufferedWriter(new FileWriter(destFile)); - outXmlStream.write(""); - outXmlStream.write("\n"); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - - if (qName.equals("bool") || qName.equals("color") || qName.equals("dimen") || qName.equals("eat-comment") - || qName.equals("integer") || qName.equals("string")) { - stack.add(new Node(attributes.getValue("name"), false, true)); - return; - } - - boolean skip = false; - if (!qName.equals("declare-styleable")) { - String space = "\n"; - for (int i = 0; i < stack.size(); i++) { - space = space + " "; - } - String tag = space + "<" + qName; - for (int i = 0; i < attributes.getLength(); i++) { - tag = tag + " " + attributes.getQName(i) + "=\""+ attributes.getValue(i) + "\""; - } - tag = tag + ">"; - try { - if (qName.equals("attr") && (attributes.getValue("name").startsWith("android:") || !attrSets.add(attributes.getValue("name"))) - || (qName.equals("public") && (!attributes.getValue("type").equals("attr") || attributes.getValue("name").startsWith("public_static_final_")))) { - //skip - skip = true; - } else { - outXmlStream.write(tag); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - if (!stack.empty()) { - Node top = stack.peek(); - top.hasChild = true; - } - stack.add(new Node(attributes.getValue("name"), false, skip)); - } - - public void endElement(String uri, String localName, String qName) throws SAXException { - - Node node = stack.pop(); - if (node.skip) { - return; - } - - if (!qName.equals("declare-styleable")) { - String space = ""; - if (node.hasChild) { - space = "\n"; - for (int i = 0; i < stack.size(); i++) { - space = space + " "; - } - } - try { - outXmlStream.write(space + ""); - } catch (IOException e) { - e.printStackTrace(); - } - } - - } - - public void characters(char[] ch, int start, int length) throws SAXException { - Node node = stack.peek(); - if (node.skip) { - return; - } - - String text = new String(ch, start, length); - text = text.replaceAll("[\n ]", ""); - if (text.length() > 0) { - try { - if (text.startsWith("@color")) { - text = text.replace("@color", "@*" + packageName +":color"); - - } else if (text.startsWith("@dimen")) { - text = text.replace("@dimen", "@*" + packageName +":dimen"); - - } else if (text.startsWith("@string")) { - text = text.replace("@string", "@*" + packageName +":string"); - - } else if (text.startsWith("@bool")) { - text = text.replace("@bool", "@*" + packageName +":bool"); - - } else if (text.startsWith("@integer")) { - text = text.replace("@integer", "@*" + packageName +":integer"); - - } else if (text.startsWith("@layout")) { - text = text.replace("@layout", "@*" + packageName +":layout"); - - } else if (text.startsWith("@anim")) { - text = text.replace("@anim", "@*" + packageName +":anim"); - - } else if (text.startsWith("@id")) { - text = text.replace("@id", "@*" + packageName +":id"); - - } else if (text.startsWith("@drawable")) { - text = text.replace("@drawable", "@*" + packageName +":drawable"); - - //} else if (text.startsWith("?attr")) { - // text = text.replace("?attr", "?*" + packageName +":attr"); - } - - outXmlStream.write(text); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - public void endDocument() throws SAXException { - try { - outXmlStream.flush(); - outXmlStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static class Node { - String name = null; - boolean hasChild = false; - boolean skip = false; - - public Node(String name, boolean hasChild, boolean skip) { - this.name = name; - this.hasChild = hasChild; - this.skip = skip; - } - } - -} \ No newline at end of file diff --git a/PluginMain/res/drawable-xhdpi/ic_launcher.png b/PluginMain/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index d4fb7cd9..00000000 Binary files a/PluginMain/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/PluginMain/res/layout/fragment_activity.xml b/PluginMain/res/layout/fragment_activity.xml deleted file mode 100644 index f0353d6a..00000000 --- a/PluginMain/res/layout/fragment_activity.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/PluginMain/res/values/styles.xml b/PluginMain/res/values/styles.xml deleted file mode 100644 index f11f7450..00000000 --- a/PluginMain/res/values/styles.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/PluginMain/src/com/example/pluginmain/MainActivity.java b/PluginMain/src/com/example/pluginmain/MainActivity.java deleted file mode 100644 index 1bccfac9..00000000 --- a/PluginMain/src/com/example/pluginmain/MainActivity.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.example.pluginmain; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.Iterator; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.support.v7.app.AppCompatActivity; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout.LayoutParams; -import android.widget.Toast; - -import com.example.pluginsharelib.SharePOJO; -import com.plugin.content.PluginDescriptor; -import com.plugin.core.annotation.ComponentContainer; -import com.plugin.core.manager.PluginCallback; -import com.plugin.core.manager.PluginManagerHelper; -import com.plugin.util.FileUtil; -import com.plugin.util.LogUtil; -import com.plugin.util.ResourceUtil; - -/** - * 添加这个注解@ComponentContainer是为了控制宿主的当前Activity是否需要支持控件级插件 - * - * 控件级插件功能默认是关闭的。控件级插件和主题换肤功能不能共存。关闭控件级插件。页面换肤功能刚能生效 - * - */ -@ComponentContainer -public class MainActivity extends AppCompatActivity { - - private ViewGroup mList; - private Button install; - boolean isInstalled = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.main_activity); - - setTitle("插件列表"); - - initView(); - - listAll(); - - // 监听插件安装 安装新插件后刷新当前页面 - registerReceiver(pluginInstallEvent, new IntentFilter(PluginCallback.ACTION_PLUGIN_CHANGED)); - - //测试利用Action打开在宿主中唤起插件receiver - Intent intent = new Intent("test.rst2");//两个Receive都配置了这个aciton,这里可以同时唤起两个Receiver - intent.putExtra("testParam", "testParam"); - sendBroadcast(intent); - - //测试通过宿主service唤起插件service - startService(new Intent(this, MainService.class)); - } - - private void initView() { - mList = (ViewGroup) findViewById(R.id.list); - install = (Button) findViewById(R.id.install); - - final Handler handler = new Handler(); - - install.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - if (!isInstalled) { - isInstalled = true; - - try { - String[] files = getAssets().list(""); - for (String apk : files) { - if (apk.endsWith(".apk")) { - copyAndInstall(apk); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } else { - Toast.makeText(MainActivity.this, "点1次就可以啦!", Toast.LENGTH_LONG).show(); - } - } - }); - } - - private void copyAndInstall(String name) { - try { - InputStream assestInput = getAssets().open(name); - String dest = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + name; - if (FileUtil.copyFile(assestInput, dest)) { - PluginManagerHelper.installPlugin(dest); - } else { - assestInput = getAssets().open(name); - dest = getCacheDir().getAbsolutePath() + "/" + name; - if (FileUtil.copyFile(assestInput, dest)) { - PluginManagerHelper.installPlugin(dest); - } else { - Toast.makeText(MainActivity.this, "解压Apk失败" + dest, Toast.LENGTH_LONG).show(); - } - } - } catch (IOException e) { - e.printStackTrace(); - Toast.makeText(MainActivity.this, "安装失败", Toast.LENGTH_LONG).show(); - } - } - - private void listAll() { - ViewGroup root = mList; - root.removeAllViews(); - // 列出所有已经安装的插件 - Collection plugins = PluginManagerHelper.getPlugins(); - Iterator itr = plugins.iterator(); - while (itr.hasNext()) { - final PluginDescriptor pluginDescriptor = itr.next(); - Button button = new Button(this); - button.setPadding(10, 25, 10, 25); - LayoutParams layoutParam = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - layoutParam.topMargin = 25; - layoutParam.bottomMargin = 25; - layoutParam.gravity = Gravity.LEFT; - root.addView(button, layoutParam); - - LogUtil.d("插件名称:", ResourceUtil.getLabel(pluginDescriptor)); - - button.setText("打开插件:" + pluginDescriptor.getPackageName()); - button.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - Intent launchIntent = getPackageManager().getLaunchIntentForPackage(pluginDescriptor.getPackageName()); - if (launchIntent == null) { - Toast.makeText(MainActivity.this, "插件" + pluginDescriptor.getPackageName() + "没有配置Launcher", Toast.LENGTH_SHORT).show(); - //没有找到Launcher,打开插件详情 - Intent intent = new Intent(MainActivity.this, DetailActivity.class); - intent.putExtra("plugin_id", pluginDescriptor.getPackageName()); - startActivity(intent); - } else { - //打开插件的Launcher界面 - if (!pluginDescriptor.isStandalone()) { - //测试向非独立插件传宿主中定义的VO对象 - launchIntent.putExtra("paramVO", new SharePOJO("宿主传过来的测试VO")); - } - startActivity(launchIntent); - } - - //也可以直接构造Intent,指定打开插件中的某个Activity - //Intent intent = new Intent("test.abc"); - //startActivity(intent); - } - }); - } - - if (plugins.size() >0) { - Button button = new Button(this); - button.setPadding(10, 25, 10, 25); - LayoutParams layoutParam = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - layoutParam.topMargin = 25; - layoutParam.bottomMargin = 25; - layoutParam.gravity = Gravity.LEFT; - root.addView(button, layoutParam); - button.setText("打开皮肤测试"); - button.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, TestSkinActivity.class); - startActivity(intent); - } - }); - - button = new Button(this); - button.setPadding(10, 25, 10, 25); - layoutParam = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - layoutParam.topMargin = 25; - layoutParam.bottomMargin = 25; - layoutParam.gravity = Gravity.LEFT; - root.addView(button, layoutParam); - button.setText("打开控件级插件测试"); - button.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, TestViewActivity.class); - startActivity(intent); - } - }); - } - - - } - - @Override - protected void onDestroy() { - super.onDestroy(); - unregisterReceiver(pluginInstallEvent); - }; - - private final BroadcastReceiver pluginInstallEvent = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Toast.makeText(MainActivity.this, - "插件" + intent.getStringExtra("id") + " "+ intent.getStringExtra("type") + "完成", - Toast.LENGTH_SHORT).show(); - listAll(); - }; - }; - -} diff --git a/PluginShareLib/AndroidManifest.xml b/PluginShareLib/AndroidManifest.xml deleted file mode 100644 index e49d7156..00000000 --- a/PluginShareLib/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/PluginShareLib/build.gradle b/PluginShareLib/build.gradle deleted file mode 100644 index 76d3f277..00000000 --- a/PluginShareLib/build.gradle +++ /dev/null @@ -1,88 +0,0 @@ - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 8 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - renderscript.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] - } - - // Move the tests to tests/java, tests/res, etc... - instrumentTest.setRoot('tests') - - // Move the build types to build-types/ - // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... - // This moves them out of them default location under src//... which would - // conflict with src/ being used by the main source set. - // Adding new build types or product flavors should be accompanied - // by a similar customization. - debug.setRoot('build-types/debug') - release.setRoot('build-types/release') - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - compile 'com.android.support:support-v4:22.1.1' - compile 'com.android.support:appcompat-v7:22.1.1' - compile fileTree(dir: 'libs', include: ['*.jar']) -} - -build.doLast { - - //测试自定义task, 观察编译log里面是否有输出 - helloTask.execute() - - //导出R的Jar包 - exportJar.execute() - -} - -task exportJar(type: Jar) { - //指定生成的jar名 - baseName 'rClasses' - from buildDir.absolutePath + '/intermediates/classes/debug/' - include '**/R.class' - include '**/R$*.class' - //打包到jar后的目录结构 - //into("com/xxx/xxx") - destinationDir = file(buildDir.absolutePath + '/outputs/') -} - - -// 自定义task的用法 -task helloTask(type: HelloGradleTask) { - helloStr = 'hello from ovrride greeting ' + android.sourceSets.main.manifest.srcFile -} -// 自定义task的用法 -class HelloGradleTask extends DefaultTask { - - def String helloStr = 'hello from Default HelloGradleTask ' - - @TaskAction - def hello() { - println helloStr - } -} diff --git a/PluginTest/build.gradle b/PluginTest/build.gradle deleted file mode 100644 index d7b2d762..00000000 --- a/PluginTest/build.gradle +++ /dev/null @@ -1,203 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - aaptOptions { - //将宿主资源添加到编译时,仅参与编译,不参与打包 - additionalParameters '-I', project(':PluginMain').getBuildDir().absolutePath + "/outputs/resources-debug.ap_" - } - - packagingOptions { - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } - - defaultConfig { - applicationId "com.example.plugintest" - minSdkVersion 11 - targetSdkVersion 22 - versionCode 1 - versionName "1.0.1" - } - - lintOptions { - checkReleaseBuilds false - abortOnError false - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - jniLibs.srcDirs = ['libs'] - renderscript.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] - } - - // Move the tests to tests/java, tests/res, etc... - instrumentTest.setRoot('tests') - - // Move the build types to build-types/ - // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... - // This moves them out of them default location under src//... which would - // conflict with src/ being used by the main source set. - // Adding new build types or product flavors should be accompanied - // by a similar customization. - debug.setRoot('build-types/debug') - release.setRoot('build-types/release') - } - - buildTypes { - release { - versionNameSuffix '_' + getDate() - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - - debug { - versionNameSuffix '_' + getDate() - } - } - -} - -dependencies { - - compile fileTree(dir: 'libs', include: ['*.jar']) - - //依赖库的src - provided files(project(':PluginCore').getBuildDir().absolutePath + '/outputs/PluginCore.jar') - provided files(project(':PluginBase').getBuildDir().absolutePath + '/outputs/PluginBase.jar') - - //依赖库的R - provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/outputs/rClasses.jar') - //依赖库的src - provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/intermediates/bundles/debug/classes.jar') - //依赖库的依赖裤(当supportV4在依赖库V的libs目录下时) - //provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/intermediates/bundles/debug/libs/android-support-v4.jar') - //依赖库的依赖裤(当supportV4通过仓库依赖时) - provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/intermediates/exploded-aar/com.android.support/support-v4/22.1.1/jars/classes.jar') - provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/intermediates/exploded-aar/com.android.support/support-v4/22.1.1/jars/libs/internal_impl-22.1.1.jar') - provided files(project(':PluginShareLib').getBuildDir().absolutePath + '/intermediates/exploded-aar/com.android.support/appcompat-v7/22.1.1/jars/classes.jar') - - //如果插件需要依赖一些私有库,可放开下面的注释,并修改为私有库的名称 - //compile project(':PluginPrivateLib1') - //compile project(':PluginPrivateLib2') - //compile file('xxx/xxx.jar'); -} - -build.doLast { - //将编译好的插件apk复制到Main工程的assets目录下, 便于测试 - copy { - println 'copy plugin apk to assets... ' + buildDir.absolutePath + '/outputs/apk/PluginTest-debug.apk' - from(buildDir.absolutePath + '/outputs/apk/') { - include 'PluginTest-debug.apk' - } - into(project(':PluginMain').getProjectDir().absolutePath + '/assets/') - } -} - -tasks.whenTaskAdded { task -> - if (task.name.startsWith("merge") && task.name.endsWith("Resources")) { - - task.doLast { - copy { - from(project.getProjectDir()) { - include 'public*.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values/") - } - - //太蠢了 这里最好改成循环遍历 - //将宿主工程导出的主题也复制过来 - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'public*.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v11.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v11/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v12.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v12/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v13.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v13/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v14.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v14/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v17.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v17/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v18.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v18/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v21.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v21/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v22.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v22/") - } - - copy { - from(project(':PluginMain').getBuildDir().absolutePath + "/outputs/theme_patch") { - include 'patch_values-v23.xml' - } - into("${project.buildDir}/intermediates/res/merged/" + task.name.replace("merge", "").replace("Resources", "").toLowerCase() + "/values-v23/") - } - } - //task.enabled = false - } -} - -def getDate() { - def date = new Date() - def formattedDate = date.format('yyyy_MM_dd_HH_mm_ss') - return formattedDate -} diff --git a/PluginTest/libs/armeabi-v7a/libhello-jni.so b/PluginTest/libs/armeabi-v7a/libhello-jni.so deleted file mode 100644 index ab6ca970..00000000 Binary files a/PluginTest/libs/armeabi-v7a/libhello-jni.so and /dev/null differ diff --git a/PluginTest/res/values/strings.xml b/PluginTest/res/values/strings.xml deleted file mode 100644 index dd0d8aed..00000000 --- a/PluginTest/res/values/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - 来着插件文字1 - 来着插件文字2 - 来着插件文字3 - 来着插件文字4 - 非独立插件demo - diff --git a/PluginTest/res/values/styles.xml b/PluginTest/res/values/styles.xml deleted file mode 100644 index 24ba97a0..00000000 --- a/PluginTest/res/values/styles.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PluginTest/src/com/example/plugintest/PluginTestApplication.java b/PluginTest/src/com/example/plugintest/PluginTestApplication.java deleted file mode 100644 index 17b74bad..00000000 --- a/PluginTest/src/com/example/plugintest/PluginTestApplication.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.plugintest; - -import android.app.Application; -import android.content.Context; - -import com.plugin.util.LogUtil; - -public class PluginTestApplication extends Application { - - @Override - public void onCreate() { - super.onCreate(); - - Context ctx = getApplicationContext(); - LogUtil.d("ctx", ctx); - } - - - -} diff --git a/PluginTest/src/com/example/plugintest/activity/LauncherActivity.java b/PluginTest/src/com/example/plugintest/activity/LauncherActivity.java deleted file mode 100644 index d3eba97d..00000000 --- a/PluginTest/src/com/example/plugintest/activity/LauncherActivity.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.example.plugintest.activity; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.view.Menu; -import android.view.View; - -import com.example.pluginsharelib.SharePOJO; -import com.example.plugintest.R; -import com.example.plugintest.receiver.PluginTestReceiver2; -import com.example.plugintest.service.PluginTestService; - -public class LauncherActivity extends AppCompatActivity implements View.OnClickListener { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.plugin_launcher); - - ActionBar actionBar = getSupportActionBar(); - actionBar.setTitle("这是插件首屏"); - actionBar.setSubtitle("这是副标题"); - actionBar.setLogo(R.drawable.ic_launcher); - actionBar.setIcon(R.drawable.ic_launcher); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP - | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); - - findViewById( R.id.onClickHellowrld).setOnClickListener(this); - findViewById( R.id.onClickPluginNormalFragment).setOnClickListener(this); - findViewById( R.id.onClickPluginSpecFragment).setOnClickListener(this); - findViewById( R.id.onClickPluginForDialogActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginForOppoAndVivoActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginNotInManifestActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginFragmentTestActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginSingleTaskActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginTestActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginTestOpenPluginActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginTestTabActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginWebViewActivity).setOnClickListener(this); - findViewById( R.id.onClickTransparentActivity).setOnClickListener(this); - findViewById( R.id.onClickPluginTestReceiver).setOnClickListener(this); - findViewById( R.id.onClickPluginTestReceiver2).setOnClickListener(this); - findViewById( R.id.onClickPluginTestService).setOnClickListener(this); - findViewById( R.id.onClickPluginTestService2).setOnClickListener(this); - } - - private static void startFragmentInHostActivity(Context context, String targetId) { - Intent pluginActivity = new Intent(); - pluginActivity.setClassName(context, "com.example.pluginmain.TestFragmentActivity"); - pluginActivity.putExtra("PluginDispatcher.fragmentId", targetId); - pluginActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(pluginActivity); - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.onClickHellowrld: - onClickHellowrld(v); - break; - case R.id.onClickPluginNormalFragment: - onClickPluginNormalFragment(v); - break; - case R.id.onClickPluginSpecFragment: - onClickPluginSpecFragment(v); - break; - case R.id.onClickPluginForDialogActivity: - onClickPluginForDialogActivity(v); - break; - case R.id.onClickPluginForOppoAndVivoActivity: - onClickPluginForOppoAndVivoActivity(v); - break; - case R.id.onClickPluginNotInManifestActivity: - onClickPluginNotInManifestActivity(v); - break; - case R.id.onClickPluginFragmentTestActivity: - onClickPluginFragmentTestActivity(v); - break; - case R.id.onClickPluginSingleTaskActivity: - onClickPluginSingleTaskActivity(v); - break; - case R.id.onClickPluginTestActivity: - onClickPluginTestActivity(v); - break; - case R.id.onClickPluginTestOpenPluginActivity: - onClickPluginTestOpenPluginActivity(v); - break; - case R.id.onClickPluginTestTabActivity: - onClickPluginTestTabActivity(v); - break; - case R.id.onClickPluginWebViewActivity: - onClickPluginWebViewActivity(v); - break; - case R.id.onClickTransparentActivity: - onClickTransparentActivity(v); - break; - case R.id.onClickPluginTestReceiver: - onClickPluginTestReceiver(v); - break; - case R.id.onClickPluginTestReceiver2: - onClickPluginTestReceiver2(v); - break; - case R.id.onClickPluginTestService: - onClickPluginTestService(v); - break; - case R.id.onClickPluginTestService2: - onClickPluginTestService2(v); - break; - } - } - - public void onClickHellowrld(View v) { - Intent intent = getPackageManager().getLaunchIntentForPackage("com.example.pluginhelloworld"); - intent.putExtra("testParam", "testParam"); - startActivity(intent); - } - - public void onClickPluginNormalFragment(View v) { - startFragmentInHostActivity(this, "some_id_for_fragment1"); - } - - public void onClickPluginSpecFragment(View v) { - startFragmentInHostActivity(this, "some_id_for_fragment2"); - } - - public void onClickPluginForDialogActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginForDialogActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginForOppoAndVivoActivity(View v) { - //利用Action打开 - Intent intent = new Intent("test.ijk"); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginNotInManifestActivity(View v) { - //利用scheme打开 - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - intent.setData(Uri.parse("testscheme://testhost")); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - - } - - public void onClickPluginFragmentTestActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginFragmentTestActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginSingleTaskActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginSingleTaskActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginTestActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginTestActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - - } - - public void onClickPluginTestOpenPluginActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginTestOpenPluginActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginTestTabActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginTestTabActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginWebViewActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginWebViewActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickTransparentActivity(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, TransparentActivity.class.getName()); - intent.putExtra("testParam", "testParam"); - intent.putExtra("paramVO", new SharePOJO("测试VO")); - startActivity(intent); - } - - public void onClickPluginTestReceiver(View v) { - //利用Action打开 - Intent intent = new Intent("test.rst2");//两个Receive都配置了这个aciton,这里可以同时唤起两个Receiver - intent.putExtra("testParam", "testParam"); - sendBroadcast(intent); - } - - public void onClickPluginTestReceiver2(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginTestReceiver2.class.getName()); - intent.putExtra("testParam", "testParam"); - sendBroadcast(intent); - } - - public void onClickPluginTestService(View v) { - //利用className打开 - Intent intent = new Intent(); - intent.setClassName(this, PluginTestService.class.getName()); - intent.putExtra("testParam", "testParam"); - startService(intent); - //stopService(intent); - } - - public void onClickPluginTestService2(View v) { - //利用Action打开 - Intent intent = new Intent("test.lmn2"); - intent.putExtra("testParam", "testParam"); - startService(intent); - //stopService(intent); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add("cc"); - return super.onCreateOptionsMenu(menu); - } -} \ No newline at end of file diff --git a/PluginTest/src/com/example/plugintest/vo/ParamVO.java b/PluginTest/src/com/example/plugintest/vo/ParamVO.java deleted file mode 100644 index 35e89a61..00000000 --- a/PluginTest/src/com/example/plugintest/vo/ParamVO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.plugintest.vo; - -import java.io.Serializable; - -/** - * Created by cailiming on 15/9/22. - */ -public class ParamVO implements Serializable { - public String name; - - @Override - public String toString() { - return "name:" + name; - } -} diff --git a/README.md b/README.md index ee85dd93..b89838d2 100644 --- a/README.md +++ b/README.md @@ -1,286 +1,253 @@ # Android-Plugin-Framework - -此项目是Android插件开发框架完整源码及示例。用来通过动态加载的方式在宿主程序中运行插件APK。 - - -# 已支持的功能: - 1、插件apk无需安装,由宿主程序动态加载运行。 - - 2、支持fragment、activity、service、receiver、contentprovider、so、application、notification。 - - 3、支持插件自定义控件、宿主自定控件。 - - 4、开发插件apk和开发普通apk时代码编写方式无区别。对插件apk和宿主程序来说,插件框架完全透明,开发插件apk时无约定、无规范约束。 - - 5、插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理 - - 6、支持插件引用宿主程序的依赖库、插件资源、宿主资源、以及插件依赖插件。 - - 7、支持插件使用宿主主题、系统主题、插件自身主题以及style、轻松支持皮肤切换 - - 8、支持非独立插件和独立插件(非独立插件指自己编译的需要依赖宿主中的公共类和资源的插件,不可独立安装运行。独立插件又分为两种: - 一种是自己编译的不需要依赖宿主中的类和资源的插件,可独立安装运行;一种是第三方发布的apk,如从应用市场下载的apk,可独立安装 - 运行,这种只做了简单支持。) - - 9、支持插件Activity的4个LaunchMode - - 10、支持插件资源文件中直接通过@xxx方式引用共享依赖库中的资源 - - 11、支持插件发送notification时在RemoteViews携带插件自定义的布局资源(只支持5.x及以上) - -# 暂不支持的功能: - - 1、插件Activity切换动画不支持使用插件自己的资源。 - - 2、不支持插件申请权限,权限必须预埋到宿主中。 - - 3、不支持第三方app试图唤起插件中的组件时直接使用插件app的Intent。即插件app不能认为自己是一个正常安装的app。 - 第三方app要唤起插件中的静态组件时必须由宿主程序进行桥接,方法请参看wxsdklibrary工程的用法 - - 4、不支持android.app.NativeActivity - -# 开发注意事项 - - 1、非独立插件开发需要解决插件资源id和宿主资源id重复产生的冲突问题。 - - 解决冲突的方式有如下两种: - - a)通过在宿主中添加一个public.xml文件来解决资源id冲突(master分支采用的方案) - - b)通过定制过的aapt在编译插件时指定id范围来解决冲突(For-gradle-with-aapt分支采用的方案) - 此方案需要替换sdk原生的aapt,且要区分多平台,buildTools版本更新后需同步升级aapt。 - 定制的aapt由 openAtlasExtention@github 项目提供,目前的版本是基于22.0.1,将项目中的BuildTools替换 - 到本地Android Sdk中相应版本的BuildTools中, - 并指定gradle的buildTools version为对应版本即可。 - - 2、非独立插件中的class不能同时存在于宿主和插件程序中,因此其引用的公共库仅参与编译,不参与打包,参看demo中的gradle脚本。 - - 3、若插件中包含so,则需要在宿主中添加一个占位的so文件。占位so可随意创建,随意命名,关键在于so所在的cpuarch目录要正确。 - 在pluginMain工程。pluginMain工程中的libstub.so其实只是一个txt文件。 - - 需要占位so的原因是,宿主在安装时,系统会扫描宿主中的so的(根据文件夹判断)类型,决定宿主在哪种cpu模式下运行、并保持到系统设置里面。 - (系统源码可查看com.android.server.pm.PackageManagerService.setBundledAppAbisAndRoots()方法) - 例如32、64、还是x86等等。如果宿主中不包含so,系统默认会选择一个最适合当前设备的模式。 - 那么问题来了,如果系统默认选择的模式,和将来下载安装的插件中的so支持的模式不匹配,则会出现so异常。 - - 因此需要提前通知系统,宿主需要在哪些cpu模式下运行。提前通知的方式即内置占位so。 - - 4、插件依赖插件时,被插件依赖的插件暂不支持包含资源(技术上可行,但为了降低复杂度不做支持) - - 5、在插件中调用getPackageName方法返回的是宿主的包名,不是插件包名。 - - 6、插件默认是在插件进程中运行,如需切到宿主进程,仅需将core工程的Manifest中配置的所有组件去都去掉掉process属性即可。 - PluginMain工程下有几个插件进程的demo也需要去掉process属性 - - 7、将配置插件为非独立插件、为插件配置依赖插件的方法。 - 插件框架识别一个插件是否为独立插件,是根据插件的Manifest文件中的android:sharedUserId属性。 - 将android:sharedUserId属性设置为宿主的packageName,则表示为非独立插件。不配置或配置为其他值表示为独立插件 - - 插件如果依赖其他基础插件,需要在插件Manifest中配置如下信息 - - name是被依赖的插件的packageName - - 8、框架中对非独立插件做了签名校验。如果宿主是release模式,要求插件的签名和宿主的签名一致才允许安装。 - -# 目录结构说明: - - 1、PluginCore工程是插件库核心工程,用于提供对插件功能的支持。 - - 2、PluginMain是用来测试的宿主程序Demo工程。 - - 3、PluginShareLib是用来测试非独立插件的公共依赖库Demo工程。 - - 4、PluginTest是用来测试的非独立插件Demo工程。 - - 5、PluginHelloWorld是用来测试的独立插件Demo工程。 - - 6、PluginBase是用来测试的被PluginTest插件依赖的插件Demo工程(此插件被PluginTest、wxsdklibrary两个插件依赖),此插件不包含资源。 - - 7、wxsdklibrary是用来测试的非独立插件Demo工程。 - -# demo安装说明: - - 1、宿主程序demo工程的assets目录下已包含了编译好的独立插件demo apk和非独立插件demo apk。 - - 2、宿主程序demo工程根目录下已包含一个已经编译好的宿主demo,可直接安装运行。 - - 3、宿主程序demo工程源码可直接编译安装运行。 - - 4、插件demo工程: - - 1、若使用master分支: - 直接编译即可,无特别要求。 - - 2、若使用For-gradle-with-aapt分支: - 将openAtlasExtention@github项目提供的BuildTools替换自己的Sdk中相应版本的BuildTools。剩下的步骤照常即可。 - - 3、若使用For-eclipse-ide分支: - 需要使用ant编译,关注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml,若编译失败,请升级androidSDK。 - - 4、编译方法 - - a)如果是命令行中: - cd Android-Plugin-Framework - ./gradlew clean - ./gradlew build - - b)如果是studio中: - 打开studio右侧gradle面板区,点clean、点build - - 由于编译脚本依赖build.doLast, 使用其他编译方法可能不会触发build.doLast导致编译失败 - 所以使用其他编译方法前请务必仔细阅读build.gradle,了解编译过程后自行调整编译脚本。 - - 待插件编译完成后,插件的编译脚本会自动将插件demo的apk复制到PlugiMain/assets目录下(复制脚本参看插件工程的build.gradle),然后重新打包安装PluginMain。 - 或者也可将插件apk复制到sdcard,然后在宿主程序中调用PluginLoader.installPlugin("插件apk绝对路径")进行安装。 - -# 实现原理简介: - 1、插件apk的class - - 通过构造插件apk的Dexclassloader来加载插件apk中的类。 - DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通。 - 若是独立插件,将parent设置为宿主程序的classloader的parent,可隔离宿主class和插件class,此时宿主和插件可包含同名的class。 - - 2、插件apk的Resource - - 直接构造插件apk的AssetManager和Resouce对象即可,需要注意的是, - 通过addAssetsPath方法添加资源的时候,需要同时添加插件程序的资源文件和宿主程序的资源, - 以及其依赖的资源。这样可以将Resource合并到一个Context里面去,解决资源访问时需要切换上下文的问题。 - - 3、插件apk中的资源id冲突 - - 完成上述第二点以后,宿主程序资源id和插件程序id可能有重复而参数冲突。 - 我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN - PP段,是用来标记apk的,默认情况下系统资源PP是01,应用程序的PP是07 - TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值 - 不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能 - 会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。 - NNNN则是某种资源类型的资源id,默认从1开始,依次累加。 - - 那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资 - 源id达到分组的效果,从而避免重复。例如将宿主程序的layout资源的TT固定为33,将插件程序 - 资源的layout的TT值固定为03(也可不对插件程序的资源id做任何处理,使其使用编译出来的原生的值), 即可解决资源id重复的问题了。 - - 固定资源id的TT值的办法也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以 - 什么TT值开头即可。具体public.xml如何编写,可参考PluginMain/public.xml,是用来固定宿主程序资源id范围的。 - - - 还有一个方法是通过定制过的aapt在编译时指定插件的PP段的值来实现分组: - 参考openAtlasExtention@github项目提供的重写过的aapt指定PP段来实现id分组,代码见For-gradle-with-aapt分支 - - 4、插件apk的Context和LayoutInfalter - - 构造一个Context对象即可,具体的Context实现请参考PluginCore/src/com/plugin/core/PluginContextTheme.java - 关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法,再从构造粗来的context中获取LayoutInfalter - - 6、插件代码无约定无规范约束。 - - 要做到这一点,主要有几点: - 1、上诉第4步骤, - 2、在classloader树中插入自己的Classloader,在loadclass时进行映射 - 3、替换ActivityThread的的Instrumentation对象和Handle CallBack对象,用来拦截组件的创建过程。 - 4、利用反射修改成员变量,注入Context。利用反射调用隐藏方法。 - - 7、插件中Activity等不在宿主manifest中注册即拥有完整生命周期的方法。 +README: [中文](https://github.com/limpoxe/Android-Plugin-Framework/blob/master/README.md) + +Android-Plugin-Framework是一个Android插件化框架,用于通过动态加载的方式免安装运行插件apk + +### 最新版本: 'com.github.limpoxe:Android-Plugin-Framework:0.0.74@aar' + +### 此项目主要目标是为了运行非独立插件,而不是任意第三方app。 + +尽管此框架支持独立插件,但目标并不是为了支持任意三方app,不同于平行空间或应用分身之类的产品。 +非独立插件相比任意三方app来说,可以预见到其使用了哪些系统api和特性,而且所有行为都是可以预测的。而任意三方app是不可预测的。 +框架的做法是按需hook,即需要用到哪些系统特性和api,就对哪些特性和api提供支持。这种做法对开发非独立插件和二方独立插件而言完全足够。 +目前已经添加了对常用特性和api的支持,如需使用的api还未支持请联系作者。 + +### FEATURE +- 框架透明, 插件开发与普通apk开发无异,无约定约束 +- 支持非独立插件和独立插件(非任意三方) +- 支持四大组件/Application/Fragment/Accessibility/LaunchMode/so +- 支持插件Theme/Style,宿主Theme/Style,轻松支持基于主题属性的皮肤切换 +- 支持插件发送Notification/时在RemoteViews中携带插件中的资源(只支持5.x及以上, 且不支持miui8) +- 支持插件热更新:即在插件模块已经被唤起的情况先安装新版本插件,无需重启插件进程(前提是插件高度内敛,宿主```不主动```持有插件中的任何对象) +- 支持全局服务:即插件向容器注册一个服务,其他所有插件已经宿主都获取并调用此服务 +- 支持DataBinding(仅限独立插件) +- 支持插件WebView加载插件本地HTML文件 +- 支持插件Fragment/View内嵌宿主Activity中 +- 支持FileProvider +- 支持2.3-12.0 + +### LIMIT +- 不支持插件Activity转场动画使用插件中的动画资源 +- 不支持插件Manifest中申请权限,所有权限必须预埋到宿主Manifest中 +- 不支持第三方app试图唤起插件中的组件时直接使用插件组件的Intent。 + 第三方app要唤起插件中的静态组件,例如Activity/service/Provider,必须由宿主程序进行桥接,即此组件需同时预埋到宿主和插件的Manifest中 +- 不支持android.app.NativeActivity +- 不支持当一个插件依赖另一个插件时,被插件依赖的包含资源 +- 不支持插件中的webview弹出```原生Chrome组件``` + 例如通过html的标签设置时间选择器。 + 说明:是否能支持原生组件取决于系统中使用WebView的实现。 + 如果是使用的Android System Webview,则可以支持。因为它packageId是以0x3f开头; + 如果是使用的Chrome Webview,则不支持。因为它packageId是以0x7f开头,会和插件冲突。 + 这是采用Public.xml进行资源分组的缺陷。 +- 可能不支持对插件或者宿主进行加壳加固处理,未尝试 + +# HOW TO USE +``` + buildscript { + dependencies { + //gradle-7.5-all + classpath "com.android.tools.build:gradle:7.4.2" + } + } - 由于Activity等是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期。 - 通过反射代理方式实现的实际是伪生命周期,并非完整生命周期。要实现插件组件免注册有2个方法。 - - 前提:宿主中预注册几个组件。预注册的组件可实际存在也可不存在。 - - a、替换classloader。适用于所有组件。 - App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml, - 如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload - 加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果, - 即可将预注册的A组件替换为未注册的插件中的B组件,从而实现插件中的组件 - 完全被系统接管,而拥有完整生命周期。其他组件同理。 - + allprojects { + repositories { + ... + maven { url = 'https://jitpack.io' } + } + } +``` +### 宿主侧 +1、 新建一个工程,作为宿主工程 + +2、 在宿主工程的build.gradle文件下添加如下3个配置 +``` + //插件脚本 + apply from: "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_2_0/host.gradle" + + android { + defaultConfig { + //这个配置不可省略 + applicationId 宿主app包名 + } + } +``` + +``` + dependencies { + //请务必使用@aar结尾,以中断依赖传递 + implementation('com.github.limpoxe:Android-Plugin-Framework:latest.release@aar') + //可选,用于支持插件全局函数式服务,不使用全局函数式服务不需要添加此依赖 + //implementation('com.limpoxe.support:android-servicemanager:1.0.5@aar') + } +``` + +``` + fairy { + //可选配置,用于指定插件进程名。默认插件进程为单独的进程,进程名为":plugin" + //若设置为空串或者null即是使用宿主进程作为插件进程 + //pluginProcess = "" + //pluginProcess = null + //pluginProcess = ":xxx" + } +``` + +3、 在宿主工程中新建一个类继承自Application类, 并配置到AndroidManifest.xml中并重写这个类的下面2个方法 +``` + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + + //框架日志开关, 默认false + FairyGlobal.setLogEnable(true); + + //首次加载插件会创建插件对象,比较耗时,通过弹出loading页来过渡。 + //这个方法是设置首次加载插件时, 定制loading页面的UI, 不传即默认没有loading页 + //在宿主中创建任意一个layout传进去即可 + //注意:首次唤起插件组件时,如果是通过startActivityForResult唤起的,如果配置了loading页, + //则实际是先打开了loading页,再转到目标页面,此时会忽略ForResult的结果。这种情况下应该禁用loading页配置 + FairyGlobal.setLoadingResId(R.layout.loading); + + //是否支持插件中使用本地html, 默认false + FairyGlobal.setLocalHtmlenable(true); + + //初始化框架 + PluginLoader.initLoader(this); + } +``` + +``` + @Override + public Context getBaseContext() { + return PluginLoader.fixBaseContextForReceiver(super.getBaseContext()); + } +``` + +4、在宿主工程中通过下面3个方法进行最基本的插件操作 +``` + 安装: PluginManagerHelper.installPlugin( SDcard上插件apk的路径 ); + 卸载: PluginManagerHelper.remove( 插件packageName ); + 列表: PluginManagerHelper.getPlugins(); +``` - b、替换Instrumention。 - 这种方式仅适用于Activity。通过修改Instrumentation进行拦截,可以利用Intent传递参数。 - 如果是Receiver和Service,利用Handler Callback进行拦截,再配合Classloader在loadclass时进行映射 - - - 8、通过activity代理方式实现加载插件中的activity是如何实现的 - - 要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式, - 替换代理Activiyt的context即可。 - 另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。 - - 更新:activity代理方式已放弃,不再支持,要了解实现可以查看历史版本 - - 9、插件编译问题。 - - 如果插件和宿主共享依赖库,常见的如supportv4,那么编译插件的时候不可将共享库编译到插件当中, - 包括共享库的代码以及R文件,只需在编译时添加到classpath中,且插件中如果要使用共享依赖库中的资源, - 需要使用共享库的R文件来进行引用。这几点在PluginTest示例工程中有体现。 - - 更新:已接入gradle,通过provided方式即可,具体可参考PluginShareLib和PluginTest的build.gradle文件 - - 10、插件Fragment - 插件UI可通过fragment或者activity来实现 +5、通过构造一个插件组件Intent打开插件 + + 例如打开插件的Launcher界面 +``` + Intent launchIntent = getPackageManager().getLaunchIntentForPackage( 插件packageName ); + startActivity(launchIntent); +``` + 宿主编译完成后,会在outputs/distrubites目录下生成一个名为host.bar的基线包,作为编译插件的基线。 + 以上所有内容及更多详情可以参考Demo + +### 插件侧   +独立插件: + + 新建一个工程, 作为插件工程,无需任何其他配置,编译出来即可当插件apk安装到宿主中。 + +非独立插件: + +1、新建一个工程, 作为插件工程。 + +2、在build.gradle中添加如下2个配置 +``` + //插件脚本 + apply from: "https://raw.githubusercontent.com/limpoxe/Android-Plugin-Framework/master/FairyPlugin/agp7_2_0/plugin.gradle" + + android { + defaultConfig { + //这个配置不可省略 + applicationId 插件app包名 + } + } - 如果是fragment实现的插件,又分为3种: - 1、fragment运行在宿主中的普通Activity中 - 2、fragment运行在宿主中的特定Activity中 - 3、fragment运行在插件中的Activity中 - - 对第2种和第3种,fragmet的开发方式和正常开发方式没有任何区别 - - 对第1种,fragmeng中凡是要使用context的地方,都需要使用通过PluginLoader.getDefaultPluginContext(FragmentClass)或者 - 通过context.createPackageContext(插件包名)获取的插件context, - 那么这种fragment对其运行容器没有特殊要求 - - 第1种Activity和第2种Activity,两者在代码上没有任何区别。主要是插件框架在运行时需要区分注入的Context的类型。 - - demo中都有例子。 - - 11、插件主题 - - 重要实现原理仍然基于上述第2、3点。 - - 12、插件Activity的LaunchMode - - 要实现插件Activity的LaunchMode,需要在宿主程序中预埋若干个(standard只需1个)相应launchMode的Activity(预注 - 册的组件可实际存在也可不存在),在运行时进行动态映射选择。core工程的manifest中配置 - - 13、对多Service的支持 - - Service的启动模式类似于Activity的singleInstance,因此为了支持插件多service,采用了和上述第12像类似的做法。 - -# 需要注意的问题 - - 1、项目插件化后,特别需要注意的是宿主程序混淆问题。公共库混淆后,可能会导致非独立插件程序运行时出现classnotfound,原因很好理解。 - 所以公共库一定要排除混淆或者使用稳定的mapping混淆。 - - 2、android sdk中的build tools版本较低时也无法编译public.xml文件,因此如果采用public.xml的方式,应使用较新版本的buildtools。 - - 3、本项目除master分支外,其他分支不会更新维护。 - -# 更新纪录: - - 2016-02-24: 1、添加插件进程 - 2、添加插件MultiDex支持 - - 2016-02-18: 增加对插件使用宿主主题的支持,例如supportV7主题 - - 2016-01-27: 1、修复几个bug - 2、增加对插件Activity透明主题支持 - - 2016-01-16: 对系统服务增加了一层Proxy,以支持拦截系统服务的方法调用 - - 2016-01-01: 1、添加对插件依赖插件的支持 - 2、添加localservice - - 2015-12-27: 添加控件插件支持。可在宿主或插件布局文件中直接嵌入其他插件中定义的控件 - - 2015-12-05: 1、修复插件so在多cpu平台下模式选择错误的问题 - 2、添加对基于主题style和自定义属性的换肤功能 - - 2015-11-22: 1、gradle插件1.3.0以上版本不支持public.xml文件也无法识别public-padding节点的文件的问题已解决, - 因此master分支切回到利用public.xml分组的实现 - 2、支持插件资源文件直接通过@package:type/name方式引用宿主资源 - -联系作者: + dependencies { + //***这是demo中的示例,请根据自己的实际情况修改,作用是指向插件依赖的宿主基线包*** + //支持文件、maven坐标等写法 + //baselinePatch 'xxx:xxx:xxx@bar' + //debugBaselinePatch 'xxx:xxx:xxx@bar' + //releaseBaselinePatch 'xxx:xxx:xxx@bar' + baselinePatch files(project(':Samples:PluginMain').getBuildDir().absolutePath + '/distributions/host.bar') + } + + ``` + + 完成以上2步后即可编译出非独立插件,以上所有内容及更多详情可以参考Demo + +### Demo编译方法 + + a)如果是命令行中(编译前需先打开settings.gradle文件中的buildSamples开关): + +``` + cd Android-Plugin-Framework + + ./gradlew clean + + ./gradlew :Samples:PluginMain:assembleF1Debug + + ./gradlew :Samples:PluginTesBase:assembleF1Debug + + ./gradlew assembleF1Debug + + ./gradlew :Samples:PluginMain:assembleF1Debug + + 说明:由于框架、demo宿主和各个demo插件都在同一个工程下,依赖关系互相影响,因此需要一定的编译顺序才能正常运行 + + 第一个assembleF1Debug是为了编译宿主,产生基线bar文件 + + 第二个assembleF1Debug是为了编译基础插件,产生插件apk和jar,基础插件jar会被其他插件依赖 + + 第三个assembleF1Debug是为了编译所有插件,产生插件apk + + 第四个assembleF1Debug是为了重新编译宿主,将前面编译生成的插件apk内置到宿主的assets目录中,作为内置插件使用 + + 接下来将宿主的out目录下的apk安装到设备上即可 + +``` + + + b)如果是studio中: + + 打开studio右侧gradle面板区,点clean、点assembleDebug。不要使用菜单栏的菜单编译。 + + 重要: + + 1、由于编译脚本依赖Task.doLast, 使用其他编译方法(如菜单编译)可能不会触发Task.doLast导致编译失败 + + 2、必须先编译宿主,再编译非独立插件。(这也是使用菜单栏编译会失败的原因之一) + 原因很简单,既然是非独立插件,肯定是需要引用宿主的类和资源的。所以编译非独立插件时会用到编译宿主时的输出物 + + 3、由于宿主和插件在同一个工程中,点击assembleDebug时编译顺序不可控,会导致每次clean后,首次assembleDebug会失败,此时重新编译即可 + 可能需要执行3次assembleDebug, + 第一次是编译宿主,产生bar文件, + 第二次是依赖bar编译插件,产生插件文件 + 第三次是重新编译宿主,将插件文件内置到宿主assets中 + 所以如果使用其他编译方法,请务必仔细阅读build.gradle,了解编译过程和依赖关系后可以自行调整编译脚本,否则可能会失败。 + + 4、Demo中使用了arm平台的so,若在x86平台上测试Demo可能会有so异常,请自行适配so。 + +   待插件编译完成后,即可通过宿主在运行时下载插件apk或者将插件apk复制到sdcard调用PluginManagerHelper.installPlugin("插件apk绝对路径")进行插件安装。 + + 通常插件会内置一个版本到宿主中随宿主一起发布,则需要将插件配置到宿主的assets目录下,再编译一次宿主(即上述3中的第三次编译)。 + 配置方法如下: + + dependencies { + //支持坐标依赖 + //innerPlugin 'xxx:xxx:xxx@apk' + innerPlugin '/xx/xx/xx/xx.apk' + } + + + 增加这个配置以后,宿主在打包时会将这个依赖的插件apk打包到宿主的assets目录中 + +## 其他 +1. [使用指南](https://github.com/limpoxe/Android-Plugin-Framework/wiki/%E5%85%B6%E4%BB%96%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97) +2. [原理简介](https://github.com/limpoxe/Android-Plugin-Framework/wiki/%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B) +3. [使用Public.xml的坑和填坑](https://github.com/limpoxe/Android-Plugin-Framework/wiki/%E4%BD%BF%E7%94%A8Public.xml%E7%9A%84%E5%9D%91%E5%92%8C%E5%A1%AB%E5%9D%91). +4. [更新记录](https://github.com/limpoxe/Android-Plugin-Framework/wiki/%E6%9B%B4%E6%96%B0%E8%AE%B0%E5%BD%95) + +## 联系作者: Q:15871365851,添加时请注明插件开发 - Q群:207397154、116993004,添加前请务必仔细阅读此ReadMe!请务必仔细阅读Demo! + + Q群:116993004,重要:添加前请务必仔细阅读此ReadMe!请务必仔细阅读Demo! diff --git a/wxsdklibrary/.gitignore b/Samples/PluginHelloWorld/.gitignore similarity index 100% rename from wxsdklibrary/.gitignore rename to Samples/PluginHelloWorld/.gitignore diff --git a/PluginHelloWorld/README.md b/Samples/PluginHelloWorld/README.md similarity index 100% rename from PluginHelloWorld/README.md rename to Samples/PluginHelloWorld/README.md diff --git a/Samples/PluginHelloWorld/build.gradle b/Samples/PluginHelloWorld/build.gradle new file mode 100644 index 00000000..1907fe47 --- /dev/null +++ b/Samples/PluginHelloWorld/build.gradle @@ -0,0 +1,76 @@ +apply plugin: 'com.android.application' + +repositories { + maven { url = 'https://maven.aliyun.com/repository/google' } +} + +android { + compileSdkVersion COMPILE_SDK_VERSION + buildToolsVersion BUILD_TOOLS_VERSION + ndkVersion = NDK_VERSION + + namespace = 'com.example.pluginhelloworld' + + useLibrary 'org.apache.http.legacy' + + defaultConfig { + applicationId "com.example.pluginhelloworld" + minSdkVersion MIN_SDK_VERSION + targetSdkVersion TARGET_SDK_VERSION + versionCode 2 + versionName "1.0.1" + flavorDimensions "versionCode" + + externalNativeBuild { + cmake { + cppFlags "" + } + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version = CMAKE_VERSION + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { + tasks.lint.enabled = false + checkReleaseBuilds = false + abortOnError = false + textReport = false + htmlReport = false + xmlReport = false + } + //测试框架脚本是否支持Flavor配置 + productFlavors { + f1 { + // + } + + f2 { + // + } + } +} + +dependencies { + implementation "com.google.android.material:material:${MATERIAL_VERSION}" + implementation "androidx.appcompat:appcompat:${APPCOMPAT_VERSION}" + implementation "com.linkedin.dexmaker:dexmaker:2.28.1" +} + +//afterEvaluate { +// tasks.getByName("packageDebug").doLast { +// copy { +// println "复制插件" + apkPathList.get(0).absolutePath + "到宿主assets目录" +// from apkPathList.get(0).getParent() +// include project.name + '-debug.apk' +// into(project(':Samples:PluginMain').getProjectDir().absolutePath + '/src/main/assets/') +// } +// } +//} diff --git a/Samples/PluginHelloWorld/jitpack.yml b/Samples/PluginHelloWorld/jitpack.yml new file mode 100644 index 00000000..db342ca6 --- /dev/null +++ b/Samples/PluginHelloWorld/jitpack.yml @@ -0,0 +1,7 @@ +before_install: + - yes | sdkmanager "cmake;3.10.2.4988404" + - sdk install java 11.0.10-open + - sdk use java 11.0.10-open + +jdk: + - openjdk11 \ No newline at end of file diff --git a/wxsdklibrary/proguard-rules.pro b/Samples/PluginHelloWorld/proguard-rules.pro similarity index 100% rename from wxsdklibrary/proguard-rules.pro rename to Samples/PluginHelloWorld/proguard-rules.pro diff --git a/PluginHelloWorld/src/main/AndroidManifest.xml b/Samples/PluginHelloWorld/src/main/AndroidManifest.xml similarity index 52% rename from PluginHelloWorld/src/main/AndroidManifest.xml rename to Samples/PluginHelloWorld/src/main/AndroidManifest.xml index 9611e542..a16a27f5 100644 --- a/PluginHelloWorld/src/main/AndroidManifest.xml +++ b/Samples/PluginHelloWorld/src/main/AndroidManifest.xml @@ -2,17 +2,27 @@ + + + + + android:requestLegacyExternalStorage="true" + android:theme="@style/AppTheme" > + + + android:label="@string/app_name" + android:exported="true"> @@ -29,6 +39,17 @@ android:theme="@android:style/Theme.Translucent.NoTitleBar" android:name=".TransparentActivity" > + + + + + diff --git a/Samples/PluginHelloWorld/src/main/assets/local_page_1.html b/Samples/PluginHelloWorld/src/main/assets/local_page_1.html new file mode 100644 index 00000000..3c06236b --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/assets/local_page_1.html @@ -0,0 +1,312 @@ + + + +GDT DEV guide.4.3 + + + + + + + + + + + + + + + + + + + +
错误码错误类型描述
3XX具体类型可联系TEST初始化错误TEST
+ + + diff --git a/Samples/PluginHelloWorld/src/main/cpp/CMakeLists.txt b/Samples/PluginHelloWorld/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..5684aac2 --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/cpp/CMakeLists.txt @@ -0,0 +1,44 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library( # Sets the name of the library. + hello-jni + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + hello-jni.cpp ) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + hello-jni + + # Links the target library to the log library + # included in the NDK. + ${log-lib} ) \ No newline at end of file diff --git a/Samples/PluginHelloWorld/src/main/cpp/hello-jni.cpp b/Samples/PluginHelloWorld/src/main/cpp/hello-jni.cpp new file mode 100644 index 00000000..ffe84a52 --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/cpp/hello-jni.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +extern "C" JNIEXPORT jint JNICALL +Java_com_example_pluginhelloworld_HelloJni_calculate( + JNIEnv* env, + jclass /* this */, + jint x, + jint y) { + int ret = x + y; + __android_log_print(ANDROID_LOG_INFO, "HelloJni", "pluginhelloworld %i + %i = %i", x, y, ret); + return ret; +} diff --git a/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloFileProvider.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloFileProvider.java new file mode 100644 index 00000000..b6437371 --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloFileProvider.java @@ -0,0 +1,22 @@ +package com.example.pluginhelloworld; + +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +import java.io.FileNotFoundException; + +public class HelloFileProvider extends FileProvider { + @Override + public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) + throws FileNotFoundException { + //插件中需要对外部app提供的intent,都需要通过宿主来桥接 + //本例中插件提供了一个contentprovider给外系统app:PackageInstaller调用。 + //所以插件中的这个contentprovider是被宿主桥接过来的 + //桥接时框架会给uri增加一个固定的前缀,这里需要将前缀移除掉,还原到原本期望的uri + Uri realUri = Uri.parse(uri.toString().replace("unsafe.proxy.", "")); + return super.openFile(realUri, mode); + } +} diff --git a/PluginHelloWorld/src/main/java/com/example/hellojni/HelloJni.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloJni.java similarity index 78% rename from PluginHelloWorld/src/main/java/com/example/hellojni/HelloJni.java rename to Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloJni.java index 512b0ba1..74ed0544 100644 --- a/PluginHelloWorld/src/main/java/com/example/hellojni/HelloJni.java +++ b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HelloJni.java @@ -1,4 +1,4 @@ -package com.example.hellojni; +package com.example.pluginhelloworld; public class HelloJni { diff --git a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HellowWorldApplication.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HellowWorldApplication.java similarity index 100% rename from PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HellowWorldApplication.java rename to Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/HellowWorldApplication.java diff --git a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java similarity index 73% rename from PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java rename to Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java index 99d33de2..53dfd94f 100644 --- a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java +++ b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/MainActivity.java @@ -1,12 +1,13 @@ package com.example.pluginhelloworld; -import android.app.Activity; +import android.net.http.AndroidHttpClient; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; /** * 独立插件测试demo @@ -25,6 +26,15 @@ public void onClick(View v) { MainActivity.this.finish(); } }); + + testUseLibray(); + } + + private void testUseLibray() { + AndroidHttpClient androidHttpClient = AndroidHttpClient.newInstance("test/test", getApplicationContext()); + ClassLoader classloader = androidHttpClient.getClass().getClassLoader(); + androidHttpClient.close(); + Log.e("MainActivity", "testUseLibray, classloader=" + classloader); } @Override diff --git a/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/Stock.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/Stock.java new file mode 100644 index 00000000..eb51f0d3 --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/Stock.java @@ -0,0 +1,11 @@ +package com.example.pluginhelloworld; + +import android.util.Log; + +public class Stock { + + public void testProxyMethod(String text) { + Log.e("Stock", "testProxyMethod " + text); + } + +} diff --git a/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/TransparentActivity.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/TransparentActivity.java similarity index 100% rename from PluginHelloWorld/src/main/java/com/example/pluginhelloworld/TransparentActivity.java rename to Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/TransparentActivity.java diff --git a/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java new file mode 100644 index 00000000..629d5993 --- /dev/null +++ b/Samples/PluginHelloWorld/src/main/java/com/example/pluginhelloworld/WelcomeActivity.java @@ -0,0 +1,190 @@ +package com.example.pluginhelloworld; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.FileProvider; + +import com.android.dx.stock.ProxyBuilder; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +/** + * 独立插件测试demo + */ +public class WelcomeActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_welcome); + try { + ApplicationInfo info = getPackageManager().getApplicationInfo("com.example.pluginhelloworld", PackageManager.GET_META_DATA); + String hellowMeta = (String)info.metaData.get("hello_meta"); + Toast.makeText(this, hellowMeta, Toast.LENGTH_SHORT).show(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + Log.e("xxx1", "activity_welcome ID= " + R.layout.activity_welcome); + Log.e("xxx2", getResources().getResourceEntryName(R.layout.activity_welcome)); + Log.e("xxx3", getResources().getString(R.string.app_name)); + Log.e("xxx4", getPackageName() + ", " + getText(R.string.app_name)); + Log.e("xxx5", getResources().getString(android.R.string.httpErrorBadUrl)); + Log.e("xxx6", getResources().getString(getResources().getIdentifier("app_name", "string", "com.example.pluginhelloworld"))); + Log.e("xxx7", getResources().getString(getResources().getIdentifier("app_name", "string", getPackageName()))); + + findViewById(R.id.test_dexmaker_btn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + Stock stock = ProxyBuilder.forClass(Stock.class) + .dexCache(getDir("dexmaker", Context.MODE_PRIVATE)) + .handler(new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Log.d("ProxyBuilder", "before " + method.getName()); + Object result = ProxyBuilder.callSuper(proxy, method, args); + Log.d("ProxyBuilder", "after " + method.getName()); + return result; + } + }) + .build(); + stock.testProxyMethod("test proxy method"); + Log.d("WelcomeActivity", "Real Stock Class : " + stock.getClass().getName()); + Log.d("WelcomeActivity", "Real Stock ClassLoader" + stock.getClass().getClassLoader()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + + findViewById(R.id.test_s_btn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + testFileProvider(); + Toast.makeText(WelcomeActivity.this, "测试JNI:3 + 4 = " + HelloJni.calculate(3, 4), Toast.LENGTH_LONG).show(); + } + }); + + findViewById(R.id.test_switch_btn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(WelcomeActivity.this, MainActivity.class)); + } + }); + + findViewById(R.id.test_transparent_btn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(WelcomeActivity.this, TransparentActivity.class)); + } + }); + + findViewById(R.id.test_installapk_btn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (new File("/sdcard/Download/xx.apk").exists()) { + testInstallApk(WelcomeActivity.this, "/sdcard/Download/xx.apk"); + } else { + Toast.makeText(WelcomeActivity.this, "apk file not exits!", Toast.LENGTH_LONG).show(); + } + } + }); + + WebView webView = (WebView) findViewById(R.id.webview); + webView.loadUrl("file:///android_asset/local_page_1.html"); + + Intent intent = new Intent("test.thirdparty.open"); + sendBroadcast(intent); + } + + + private static void testInstallApk(Context context, String apkPath) { + if (context == null || TextUtils.isEmpty(apkPath)) { + return; + } + File file = new File(apkPath); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (Build.VERSION.SDK_INT >= 24) { + Uri apkUri = FileProvider.getUriForFile(context, "x.y.z.fileprovider", file); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + } else { + intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); + } + context.startActivity(intent); + } + + private void testFileProvider() { + Intent intent = new Intent("com.android.camera.action.CROP"); + + //注意修改为自己设备上真实存在的地址 + File srcfile = new File("/storage/emulated/0/Pictures/Screenshots/1.png"); + + if(!srcfile.exists()) { + //oast.makeText(getApplicationContext(), "图片不存在:" + srcfile.getAbsolutePath(), Toast.LENGTH_LONG).show(); + return; + } + + Uri photoURI = FileProvider.getUriForFile(this, "x.y.z.fileprovider", srcfile); + + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(photoURI, "image/*"); + + intent.putExtra("crop", "true"); + intent.putExtra("outputX", 80); + intent.putExtra("outputY", 80); + intent.putExtra("return-data", false); + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + + File output = new File("/storage/emulated/0/Pictures/Screenshots/", System.currentTimeMillis() + "_crop.png"); + output.getParentFile().mkdirs(); + output.delete(); + + intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(output)); + + startActivityForResult(intent, 111); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_welcome, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/PluginHelloWorld/src/main/res/layout/activity_main.xml b/Samples/PluginHelloWorld/src/main/res/layout/activity_main.xml similarity index 100% rename from PluginHelloWorld/src/main/res/layout/activity_main.xml rename to Samples/PluginHelloWorld/src/main/res/layout/activity_main.xml diff --git a/PluginTest/res/layout/activity_transparent.xml b/Samples/PluginHelloWorld/src/main/res/layout/activity_transparent.xml similarity index 100% rename from PluginTest/res/layout/activity_transparent.xml rename to Samples/PluginHelloWorld/src/main/res/layout/activity_transparent.xml diff --git a/PluginHelloWorld/src/main/res/layout/activity_welcome.xml b/Samples/PluginHelloWorld/src/main/res/layout/activity_welcome.xml similarity index 65% rename from PluginHelloWorld/src/main/res/layout/activity_welcome.xml rename to Samples/PluginHelloWorld/src/main/res/layout/activity_welcome.xml index e2a6166a..173c533f 100644 --- a/PluginHelloWorld/src/main/res/layout/activity_welcome.xml +++ b/Samples/PluginHelloWorld/src/main/res/layout/activity_welcome.xml @@ -8,6 +8,14 @@ +