├── .gitignore ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── README_en.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── loader ├── .gitignore ├── build.gradle ├── libs │ ├── annotation.jar │ ├── hooklib-4.0.0.aar │ └── xposedcompat.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── android │ │ ├── app │ │ │ ├── ActivityThread.java │ │ │ ├── AndroidAppHelper.java │ │ │ ├── LoadedApk.java │ │ │ └── package-info.java │ │ └── content │ │ │ └── res │ │ │ ├── CompatibilityInfo.java │ │ │ ├── XResForwarder.java │ │ │ └── package-info.java │ ├── com │ │ ├── storm │ │ │ └── wind │ │ │ │ └── xposed │ │ │ │ ├── MainTestActivity.java │ │ │ │ └── XposedTestApplication.java │ │ └── wind │ │ │ ├── xpatch │ │ │ └── proxy │ │ │ │ └── XpatchProxyApplication.java │ │ │ └── xposed │ │ │ └── entry │ │ │ ├── SandHookInitialization.java │ │ │ ├── XposedHookLoadPackageInner.java │ │ │ ├── XposedModuleEntry.java │ │ │ ├── XposedModuleLoader.java │ │ │ ├── hooker │ │ │ └── PackageSignatureHooker.java │ │ │ └── util │ │ │ ├── FileUtils.java │ │ │ ├── NativeLibraryHelperCompat.java │ │ │ ├── PackageNameCache.java │ │ │ ├── PluginNativeLibExtractor.java │ │ │ ├── ReflectUtils.java │ │ │ ├── SharedPrefUtils.java │ │ │ ├── VMRuntime.java │ │ │ ├── XLog.java │ │ │ └── XpatchUtils.java │ └── de │ │ └── robv │ │ └── android │ │ └── xposed │ │ └── XposedHelper.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── settings.gradle └── xpatch ├── .gitignore ├── build.gradle ├── libs ├── ManifestEditor-aff5123.jar ├── apksigner.jar └── dex-tools-2.1-SNAPSHOT.jar └── src └── main ├── AndroidManifest.xml ├── assets ├── android.keystore ├── dex │ ├── sandhook │ │ └── classes.dex │ └── whale │ │ └── classes-1.0.dex ├── keystore ├── lib │ ├── arm64-v8a │ │ ├── libsandhook │ │ └── libwhale │ └── armeabi-v7a │ │ ├── libsandhook │ │ └── libwhale ├── win │ └── zipalign.exe ├── xposedmodule │ └── hook_apk_path_module.apk └── zipalign └── java ├── com └── storm │ └── wind │ └── xpatch │ ├── MainCommand.java │ ├── base │ └── BaseCommand.java │ ├── task │ ├── ApkModifyTask.java │ ├── BuildAndSignApkTask.java │ ├── SaveApkSignatureTask.java │ ├── SaveOriginalApkTask.java │ ├── SaveOriginalApplicationNameTask.java │ └── SoAndDexCopyTask.java │ └── util │ ├── ApkSignatureHelper.java │ ├── FileUtils.java │ ├── ManifestParser.java │ ├── ReflectUtils.java │ └── ShellCmdUtil.java └── wind ├── android ├── content │ └── res │ │ ├── AXmlResourceParser.java │ │ ├── ChunkUtil.java │ │ ├── IntReader.java │ │ ├── StringBlock.java │ │ └── XmlResourceParser.java └── util │ ├── AttributeSet.java │ └── TypedValue.java ├── test └── AXMLPrinter.java └── v1 ├── XmlPullParser.java ├── XmlPullParserException.java ├── XmlPullParserFactory.java └── XmlSerializer.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .idea 8 | 9 | .DS_Store 10 | /build 11 | /captures 12 | .externalNativeBuild 13 | /out 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Xpatch - Tool to work with android apk file, it can insert xposed loader into the apk file, and repackage the apk, then 2 | the repacked apk can load xposed module when started. 3 | 4 | Copyright (c) 2019 Wind 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [**English Version**](https://github.com/WindySha/Xpatch/blob/6ec0f3c16128dda46ab05bdd915d66ebbdaaf9fc/README_en.md) 2 | 3 | # Android App破解工具Xpatch的使用方法 4 | 5 | ## Xpatch概述 6 | Xpatch用来重新签名打包Apk文件,使重打包后的Apk能加载安装在系统里的Xposed插件,从而实现免Root Hook任意App。 7 | 8 | ## Xpatch基本原理 9 | Xpatch的原理是对Apk文件进行二次打包,重新签名,并生成一个新的apk文件。 10 | 在Apk二次打包过程中,插入加载Xposed插件的逻辑,这样,新的Apk文件就可以加载任意Xposed插件,从而实现免Root Hook任意App的Java代码。 11 | 12 | 1.0~1.4版本,Hook框架使用的是Lody的[whale](https://github.com/asLody/whale) 13 | 2.0版本开始,Hook框架底层使用的是ganyao114的[SandHook](https://github.com/ganyao114/SandHook)。 14 | 3.0版本开始,默认使用SandHook,同时,兼容切换为whale 15 | 16 | ## Xpatch工具包下载 17 | [下载最新的Xpatch Jar包][1] 18 | 或者进入Releases页面下载指定版本:[releases][2] 19 | 20 | ## Xpatch App版本(Xposed Tool)下载 21 | [XposedTool][16] [下载XposedTool Apk][15] 22 | 23 | ## Xpatch使用方法 24 | Xpatch项目最终生成物是一个Jar包,此Jar使用起来非常简单,只需要一行命令,一个接入xposed hook功能的apk就生成: 25 | ``` 26 | $ java -jar XpatchJar包路径 apk文件路径 27 | 28 | For example: 29 | $ java -jar ../xpatch.jar ../Test.apk 30 | ``` 31 | 32 | 这条命令之后,在原apk文件(Test.apk)相同的文件夹中,会生成一个名称为`Test-xposed-signed.apk`的新apk,这就是重新签名之后的支持xposed插件的apk。 33 | 34 | **Note:** 由于签名与原签名不一致,因此需要先卸载掉系统上已经安装的原apk,才能安装这个Xpatch后的apk 35 | 36 | 当然,也可以增加`-o`参数,指定新apk生成的路径: 37 | ``` 38 | $ java -jar ../xpatch.jar ../Test.apk -o ../new-Test.apk 39 | ``` 40 | 41 | 更多参数类型可以使用--help查看,或者不输入任何参数运行jar包: 42 | ``` 43 | $ java -jar ../xpatch.jar --h(可省略) 44 | ``` 45 | 这行命令之后得到结果(v1.0-v2.0): 46 | ``` 47 | Please choose one apk file you want to process. 48 | options: 49 | -f,--force force overwrite 50 | -h,--help Print this help message 51 | -k,--keep not delete the jar file that is changed by dex2jar 52 | and the apk zip files 53 | -l,--log show all the debug logs 54 | -o,--output output .apk file, default is $source_apk_dir/[file 55 | -name]-xposed-signed.apk 56 | ``` 57 | 58 | ## Xposed模块开关控制的两种方法 59 | ### 1. 手动修改sdcard文件控制模块开关 60 | 当新apk安装到系统之后,应用启动时,默认会加载所有已安装的Xposed插件(Xposed Module)。 61 | 62 | 一般情况下,Xposed插件中都会对包名过滤,有些Xposed插件有界面,并且在界面上可以设置开关,所以默认启用所有的Xposed插件的方式,大多数情形下都可行。 63 | 64 | 但在少数情况可能会出现问题,比如,同一个应用安装有多个Xposed插件(wechat插件就非常多),并且都没有独立的开关界面,同时启用这些插件可能会产生冲突。 65 | 66 | 为了解决此问题,当应用启动时,会查找系统中所有已安装的Xposed插件,并在文件目录下生成一个文件 67 | `mnt/sdcard/xposed_config/modules.list`,记录这些Xposed插件App。 68 | 比如: 69 | ``` 70 | com.blanke.mdwechat#MDWechat 71 | com.example.wx_plug_in3#畅玩微信 72 | liubaoyua.customtext#文本自定义 73 | ``` 74 | 记录的方式是:`插件app包名#插件app名称` 75 | 76 | 需要禁用某个插件,只需要修改此文件,在该插件包名前面增加一个`#`号即可。 77 | 78 | 比如,需要禁用`畅玩微信`和`文本自定义`两个插件,只需要修改该文本文件,增加一个`#`号即可: 79 | ``` 80 | com.blanke.mdwechat#MDWechat 81 | #com.example.wx_plug_in3#畅玩微信 82 | #liubaoyua.customtext#文本自定义 83 | ``` 84 | 如果需要禁用所有插件,只需在所有的包名前面增加`#`。 85 | 86 | **注意:** 87 | 有些App没有获取到sd卡文件读写权限,这会导致无法读取modules.list配置文件,此时会默认启用所有插件。这种情况下,需要手动打开app的文件读写权限。 88 | ### 2. 通过Xposed Tool App控制模块开关 89 | 下载并安装Xpatch App(Xposed Tool) 90 | [点我下载XposedTool Apk][15] 91 | 通过`Xposed模块管理`页面来控制模块开关。(原理跟方法1一致) 92 | ![Screenshot.png](https://upload-images.jianshu.io/upload_images/1639238-84d7a1dd814f314a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300) 93 | 94 | ## 可用的Xposed模块示例 95 | 96 | - [MDWechat][8] 97 | - [文本自定义][9] 98 | - **你自己编写的Xposed模块** 99 | 100 | 101 | ## 其他 102 | assets目录下的classes.dex是来加载设备上已安装的Xposed插件,其源代码也已经开源: 103 | [xposed_module_loader](https://github.com/WindySha/xposed_module_loader) 104 | 欢迎star and fork. 105 | 106 | ## 局限性 107 | Xpatch是基于apk二次打包实现的,而且使用到了dex2Jar工具,因此,也存在不少的局限性。大概有以下几点: 108 | 109 | 1. Hook框架默认使用的是SandHook,此框架存在一些不稳定性,在少数机型上hook可能会崩溃。 110 | 2. 对于校验了文件完整性的app,重打包后可能无法启动; 111 | 3. Xposed Hook框架暂时不支持Dalvik虚拟机。 112 | 4. 暂时不支持Xposed插件中的资源Hook。 113 | 114 | ## Technology Discussion 115 | **QQ Group: 741998508** 116 | or 117 | **Post comments under this article: [Xpatch: 免Root实现App加载Xposed插件的一种方案](https://windysha.github.io/2019/04/18/Xpatch-%E5%85%8DRoot%E5%AE%9E%E7%8E%B0App%E5%8A%A0%E8%BD%BDXposed%E6%8F%92%E4%BB%B6%E7%9A%84%E4%B8%80%E7%A7%8D%E6%96%B9%E6%A1%88/)** 118 | 119 | ## 功能更新 120 | 121 | ---- 122 | ### 1. 2019/4/15 updated 123 | 增加自动破解签名检验的功能,此功能默认开启,如果需要关闭可以增加`-c`即可,比如: 124 | ``` 125 | $ java -jar ../xpatch.jar ../Test.apk -c 126 | ``` 127 | 通过help(-h)可以查看到: 128 | >options: 129 | > -c,--crach disable craching the apk's signature. 130 | 131 | ### 2. 2019/4/25 updated 132 | 增加将Xposed modules打包到apk中的功能 133 | 通过help(-h)可以查看到: 134 | >-xm,--xposed-modules the xposed mpdule files to be packaged into the ap 135 | > k, multi files should be seperated by :(mac) or ;( 136 | > win) 137 | 138 | 使用方式为在命令后面增加`-xm path`即可,比如: 139 | ``` 140 | $ java -jar ../xpatch.jar ../source.apk -xm ../module1.apk 141 | ``` 142 | 假如需要将多个Xposed插件打包进去,在Mac中使用":",在Windows下使用";",隔开多个文件路径即可,比如: 143 | ``` 144 | mac 145 | $ java -jar ../xpatch.jar ../source.apk -xm ../module1.apk:../module2.apk 146 | 147 | windows 148 | $ java -jar ../xpatch.jar ../source.apk -xm ../module1.apk;../module2.apk 149 | ``` 150 | 151 | **注意:** 152 | 1. 多个Xposed modules使用`:`(mac)/`;`(win)分割; 153 | 2. 假如此module既被打包到apk中,又安装在设备上,则只会加载打包到apk中的module,不会加载安装的。 154 | 这里是通过包名区分是不是同一个module。 155 | 156 | ---- 157 | 158 | ### 3. 2020/02/09 updated (Version 3.0) 159 | 3.0版本增加了不少新功能,同时修复了一些用户反馈的Bug。 160 | 161 | 新增功能: 162 | 1. 支持android 10; 163 | 2. 支持更改植入的hook框架,默认使用Sandhook(支持android10),可更改为whale(暂不支持android10)(-w); 164 | 3. 默认使用修改Maniest文件方式,植入初始化代码,同时,支持更改为老版本中的修改dex文件的方式植入代码(-dex); 165 | 4. 支持修改apk包名(一般不建议使用,很多应用会校验包名,会导致无法使用) 166 | 5. 支持修改apk的version code; 167 | 6. 支持修改apk的version name; 168 | 7. 支持修改apk的debuggable为true或者false; 169 | 170 | Bug修复: 171 | 1. 修复Manifest文件中未定义ApplicationName类,导致无法实现Hook的问题; 172 | 2. 修复破解无so文件的apk时,出现无法找到so的问题; 173 | 3. 修复签名可能会失败的问题; 174 | 4. 修复dex文件数超过65536的问题; 175 | 176 | #### 新功能用法 177 | 在命令行中输入-h,可以看到3.0版本完整的帮助文档: 178 | `$ java -jar ../xpatch-3.0.jar -h` 179 | ``` 180 | options: 181 | -c,--crach disable craching the apk's signature. 182 | -d,--debuggable <0 or 1> set 1 to make the app debuggable = true, set 0 to 183 | make the app debuggable = false 184 | -dex,--dex insert code into the dex file, not modify manifest 185 | application name attribute 186 | -f,--force force overwrite 187 | -h,--help Print this help message 188 | -k,--keep not delete the jar file that is changed by dex2jar 189 | and the apk zip files 190 | -l,--log show all the debug logs 191 | -o,--output output .apk file, default is $source_apk_dir/[file 192 | -name]-xposed-signed.apk 193 | -pkg,--packageName modify the apk package name 194 | -vc,--versionCode set the app version code 195 | -vn,--versionName set the app version name 196 | -w,--whale Change hook framework to Lody's whale 197 | -xm,--xposed-modules 198 | the xposed module files to be packaged into the ap 199 | k, multi files should be seperated by :(mac) or ;( 200 | win) 201 | version: 3.0 202 | ``` 203 | 具体用法: 204 | 1. 修改Apk的debuggable = true: 205 | `$ java -jar ../xpatch-3.0.jar ../Test.apk -d 1` (false改为0) 206 | 2. 使用老版本的破解dex方法破解apk: 207 | `$ java -jar ../xpatch-3.0.jar ../Test.apk -dex` 208 | 3. 修改包名,版本号: 209 | `$ java -jar ../xpatch-3.0.jar ../Test.apk -pkg com.test.test -vc 1000 -vn 1.1.1` 210 | 2. 更改Hook框架为whale: 211 | `$ java -jar ../xpatch-3.0.jar ../Test.apk -w` 212 | 213 | ### 4. 2021/01/13 updated (Version 4.0) 214 | Update SandHook to the newest version and SandHook support the Android 11. 215 | 216 | ## Thanks 217 | 218 | - [Xposed][10] 219 | - [whale][11] 220 | - [dex2jar][12] 221 | - [AXMLPrinter2][13] 222 | - [SandHook](https://github.com/ganyao114/SandHook) 223 | - [xposed_module_loader](https://github.com/WindySha/xposed_module_loader) 224 | - [axml](https://github.com/Sable/axml) 225 | 226 | [1]: https://github.com/WindySha/Xpatch/releases/download/v3.0/xpatch-3.0.jar 227 | [2]: https://github.com/WindySha/Xpatch/releases 228 | [3]: https://ibotpeaches.github.io/Apktool/install/ 229 | [5]: https://github.com/asLody/whale 230 | [6]: https://repo.xposed.info/module/com.example.wx_plug_in3 231 | [7]: https://github.com/Gh0u1L5/WechatMagician/releases 232 | [8]: https://github.com/Blankeer/MDWechat 233 | [9]: https://repo.xposed.info/module/liubaoyua.customtext 234 | [10]: https://github.com/rovo89/Xposed 235 | [11]: https://github.com/asLody/whale 236 | [12]: https://github.com/pxb1988/dex2jar 237 | [13]: https://code.google.com/archive/p/android4me/downloads 238 | [14]: http://www.apache.org/licenses/LICENSE-2.0.html 239 | [15]: https://xposed-tool-app.oss-cn-beijing.aliyuncs.com/data/Xposed_Tool_2.0.3.apk 240 | [16]: https://github.com/WindySha/xposed-tool-app 241 | 242 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # What is Xpatch 2 | 3 | Xpatch is a jar tool which is used to repackage the apk file. Then, the new apk can load any Xposed modules installed in the android system. 4 | 5 | This is a way to use Xposed modules without root your device. 6 | 7 | It is easy way to modify one app using xposed module. And any apps changed by Xpatch can load every modules downloaded in the [Xposed Module Repository](https://repo.xposed.info/). 8 | # Benefits 9 | 1. Use xposed modules without your device; 10 | 2. Modify any apps without root your device. 11 | 12 | # How to use 13 | 1. Download the latest jar file from the [release page](https://github.com/WindySha/Xpatch/releases); 14 | 2. Run this command in the Windows/Mac console: 15 | ``` 16 | $ java -jar ../../xpatch.jar ../../source.apk 17 | ``` 18 | Then, a new apk named `source-xposed-signed.apk` in the same folder as `source.apk`. 19 | 20 | # More commands 21 | 1. You can specify the output apk path by add `-o` parameter, eg: 22 | ``` 23 | $ java -jar ../../xpatch.jar ../../source.apk -o ../../dst.apk 24 | ``` 25 | 2. Show all the building new apk logs, just add `-l`, eg: 26 | ``` 27 | $ java -jar ../../xpatch.jar ../../source.apk -l 28 | ``` 29 | 3. Not delete the build files, just add `-k`, eg: 30 | ``` 31 | $ java -jar ../../xpatch.jar ../../source.apk -k 32 | ``` 33 | 4. After the version 1.2, craching app signature verifying is added, if you won't need the function, just add '-c', eg: 34 | ``` 35 | $ java -jar ../../xpatch.jar ../../source.apk -c 36 | ``` 37 | 5. More command details can be found when no parameter is added, eg: 38 | ``` 39 | $ java -jar ../../xpatch.jar 40 | ``` 41 | # How to manage Xposed modules 42 | When the new apk is installed in the device, It will load all the Xposed modules installed in the device when it's process started. 43 | 44 | But you can manage the installed Xposed modules on/off state by a file in the storage. 45 | The file path is `mnt/sdcard/xposed_config/modules.list`. 46 | 47 | When the new app started, it will search all the installed Xposed modules and write the the module app name and the module application name into this file. (`mnt/sdcard/xposed_config/modules.list`) 48 | eg: 49 | ``` 50 | com.blanke.mdwechat#MDWechat 51 | 52 | liubaoyua.customtext#文本自定义 53 | ``` 54 | Each line of this file is Application Name#App Name. 55 | You can disable a Xposed module by add `#` before the Application Name, eg: 56 | ``` 57 | #com.blanke.mdwechat#MDWechat 58 | 59 | liubaoyua.customtext#文本自定义 60 | ``` 61 | This means the MDWechat Xposed module is disabled. 62 | 63 | ``` 64 | #com.blanke.mdwechat#MDWechat 65 | 66 | #liubaoyua.customtext#文本自定义 67 | ``` 68 | This means all Xposed modules are disabled. 69 | 70 | Note: The target app must have file system access permission. Otherwise this file will not be created, and all xposed modules are enabled. 71 | 72 | 73 | # Todo list 74 | 1. Support packaging the xposed modules into the source apk; 75 | 2. Support loading so library in the xposed modules; 76 | 3. Crach apk protections. 77 | 78 | # Issues 79 | 1. If the apk dex files are protected, dex2jar can not effect on the dexs, then this tool will not work; 80 | 2. The hook framework is using [whale](https://github.com/asLody/whale); this framework is not very stable, some hooks may fail; 81 | 3. Do not support Davlik VM; 82 | 4. Do not support resource hook; 83 | 84 | # Discuss 85 | You can discuss with me under this page. 86 | [Xpatch Comments](https://windysha.github.io/2019/04/18/Xpatch-%E5%85%8DRoot%E5%AE%9E%E7%8E%B0App%E5%8A%A0%E8%BD%BDXposed%E6%8F%92%E4%BB%B6%E7%9A%84%E4%B8%80%E7%A7%8D%E6%96%B9%E6%A1%88/) 87 | 88 | 89 | # Thanks to 90 | - [Xposed][10] 91 | - [whale][11] 92 | - [dex2jar][12] 93 | - [AXMLPrinter2][13] 94 | 95 | [10]: https://github.com/rovo89/Xposed 96 | [11]: https://github.com/asLody/whale 97 | [12]: https://github.com/pxb1988/dex2jar 98 | [13]: https://code.google.com/archive/p/android4me/downloads 99 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.0.1' 10 | } 11 | } 12 | 13 | task clean(type: Delete) { 14 | delete rootProject.buildDir 15 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=false 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 06 01:11:44 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /loader/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /loader/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdk 30 7 | buildToolsVersion "30.0.0" 8 | 9 | defaultConfig { 10 | applicationId "com.storm.wind.xposed" 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | versionCode 3 14 | versionName "3.0" 15 | 16 | multiDexEnabled false 17 | 18 | ndk { 19 | abiFilters 'armeabi-v7a', 'arm64-v8a' 20 | } 21 | } 22 | buildTypes { 23 | debug { 24 | debuggable true 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | release { 29 | debuggable false 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | 35 | lintOptions { 36 | abortOnError false 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | applicationVariants.all { variant -> 45 | variant.outputs.all { 46 | outputFileName = "${variant.getName()}-${variant.versionName}.apk" 47 | } 48 | } 49 | } 50 | 51 | task "copyDebugDex"(type: Copy) { 52 | outputs.upToDateWhen { false } // do not UP-TO_DATE 53 | dependsOn("assembleDebug") 54 | from "$buildDir/intermediates/dex/debug/mergeDexDebug/classes.dex" 55 | rename "(.*).dex", "classes.dex" 56 | into "$rootProject.projectDir/xpatch/src/main/assets/dex/sandhook" 57 | } 58 | 59 | task "copyDebugSo"(type: Copy) { 60 | outputs.upToDateWhen { false } // do not UP-TO_DATE 61 | dependsOn("assembleDebug") 62 | from "$buildDir/intermediates/merged_native_libs/debug/out/lib" 63 | rename '(.*).so', '$1' 64 | into "$rootProject.projectDir/xpatch/src/main/assets/lib" 65 | } 66 | 67 | task "copyLoaderFiles"() { 68 | outputs.upToDateWhen { false } 69 | dependsOn("copyDebugSo") 70 | dependsOn("copyDebugDex") 71 | } 72 | 73 | dependencies { 74 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 75 | implementation 'com.jakewharton.android.repackaged:dalvik-dx:9.0.0_r3' 76 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:3.0' 77 | } -------------------------------------------------------------------------------- /loader/libs/annotation.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/loader/libs/annotation.jar -------------------------------------------------------------------------------- /loader/libs/hooklib-4.0.0.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/loader/libs/hooklib-4.0.0.aar -------------------------------------------------------------------------------- /loader/libs/xposedcompat.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/loader/libs/xposedcompat.aar -------------------------------------------------------------------------------- /loader/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | #-keep class com.wind.xposed.entry.XposedModuleEntry { 2 | # public (); 3 | # public void init(); 4 | #} 5 | #-keep class de.robv.android.xposed.**{*;} 6 | #-keep class com.swift.sandhook.**{*;} 7 | #-keep class com.swift.sandhook.xposedcompat.**{*;} 8 | # 9 | #-dontwarn de.robv.android.xposed.XposedHelper 10 | -keep class com.wind.xposed.entry.XposedModuleEntry { 11 | public (); 12 | public void init(); 13 | } 14 | -keep class de.robv.android.xposed.**{*;} 15 | 16 | -dontwarn de.robv.android.xposed.XposedHelper -------------------------------------------------------------------------------- /loader/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /loader/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | import android.content.res.CompatibilityInfo; 5 | 6 | public final class ActivityThread { 7 | public static ActivityThread currentActivityThread() { 8 | throw new UnsupportedOperationException("STUB"); 9 | } 10 | 11 | public static Application currentApplication() { 12 | throw new UnsupportedOperationException("STUB"); 13 | } 14 | 15 | public static String currentPackageName() { 16 | throw new UnsupportedOperationException("STUB"); 17 | } 18 | 19 | public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { 20 | throw new UnsupportedOperationException("STUB"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /loader/src/main/java/android/app/AndroidAppHelper.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.content.SharedPreferences; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.res.CompatibilityInfo; 6 | import android.content.res.Configuration; 7 | import android.content.res.Resources; 8 | import android.os.Build; 9 | import android.os.IBinder; 10 | import android.view.Display; 11 | 12 | import java.lang.ref.WeakReference; 13 | import java.util.Map; 14 | 15 | import de.robv.android.xposed.XSharedPreferences; 16 | import de.robv.android.xposed.XposedBridge; 17 | 18 | import static de.robv.android.xposed.XposedHelpers.findClass; 19 | import static de.robv.android.xposed.XposedHelpers.findFieldIfExists; 20 | import static de.robv.android.xposed.XposedHelpers.findMethodExactIfExists; 21 | import static de.robv.android.xposed.XposedHelpers.getObjectField; 22 | import static de.robv.android.xposed.XposedHelpers.newInstance; 23 | import static de.robv.android.xposed.XposedHelpers.setFloatField; 24 | 25 | /** 26 | * Contains various methods for information about the current app. 27 | * 28 | *

For historical reasons, this class is in the {@code android.app} package. It can't be moved 29 | * without breaking compatibility with existing modules. 30 | */ 31 | public final class AndroidAppHelper { 32 | private AndroidAppHelper() {} 33 | 34 | private static final Class CLASS_RESOURCES_KEY; 35 | private static final boolean HAS_IS_THEMEABLE; 36 | private static final boolean HAS_THEME_CONFIG_PARAMETER; 37 | 38 | static { 39 | CLASS_RESOURCES_KEY = (Build.VERSION.SDK_INT < 19) ? 40 | findClass("android.app.ActivityThread$ResourcesKey", null) 41 | : findClass("android.content.res.ResourcesKey", null); 42 | 43 | HAS_IS_THEMEABLE = findFieldIfExists(CLASS_RESOURCES_KEY, "mIsThemeable") != null; 44 | HAS_THEME_CONFIG_PARAMETER = HAS_IS_THEMEABLE && Build.VERSION.SDK_INT >= 21 45 | && findMethodExactIfExists("android.app.ResourcesManager", null, "getThemeConfig") != null; 46 | } 47 | 48 | @SuppressWarnings({ "unchecked", "rawtypes" }) 49 | private static Map getResourcesMap(ActivityThread activityThread) { 50 | if (Build.VERSION.SDK_INT >= 24) { 51 | Object resourcesManager = getObjectField(activityThread, "mResourcesManager"); 52 | return (Map) getObjectField(resourcesManager, "mResourceImpls"); 53 | } else if (Build.VERSION.SDK_INT >= 19) { 54 | Object resourcesManager = getObjectField(activityThread, "mResourcesManager"); 55 | return (Map) getObjectField(resourcesManager, "mActiveResources"); 56 | } else { 57 | return (Map) getObjectField(activityThread, "mActiveResources"); 58 | } 59 | } 60 | 61 | /* For SDK 15 & 16 */ 62 | private static Object createResourcesKey(String resDir, float scale) { 63 | try { 64 | if (HAS_IS_THEMEABLE) 65 | return newInstance(CLASS_RESOURCES_KEY, resDir, scale, false); 66 | else 67 | return newInstance(CLASS_RESOURCES_KEY, resDir, scale); 68 | } catch (Throwable t) { 69 | XposedBridge.log(t); 70 | return null; 71 | } 72 | } 73 | 74 | /* For SDK 17 & 18 & 23 */ 75 | private static Object createResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, float scale) { 76 | try { 77 | if (HAS_THEME_CONFIG_PARAMETER) 78 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, null); 79 | else if (HAS_IS_THEMEABLE) 80 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false); 81 | else 82 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale); 83 | } catch (Throwable t) { 84 | XposedBridge.log(t); 85 | return null; 86 | } 87 | } 88 | 89 | /* For SDK 19 - 22 */ 90 | private static Object createResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, float scale, IBinder token) { 91 | try { 92 | if (HAS_THEME_CONFIG_PARAMETER) 93 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, null, token); 94 | else if (HAS_IS_THEMEABLE) 95 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, token); 96 | else 97 | return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, token); 98 | } catch (Throwable t) { 99 | XposedBridge.log(t); 100 | return null; 101 | } 102 | } 103 | 104 | /* For SDK 24+ */ 105 | private static Object createResourcesKey(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { 106 | try { 107 | return newInstance(CLASS_RESOURCES_KEY, resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, compatInfo); 108 | } catch (Throwable t) { 109 | XposedBridge.log(t); 110 | return null; 111 | } 112 | } 113 | 114 | /** @hide */ 115 | public static void addActiveResource(String resDir, float scale, boolean isThemeable, Resources resources) { 116 | addActiveResource(resDir, resources); 117 | } 118 | 119 | /** @hide */ 120 | public static void addActiveResource(String resDir, Resources resources) { 121 | ActivityThread thread = ActivityThread.currentActivityThread(); 122 | if (thread == null) { 123 | return; 124 | } 125 | 126 | Object resourcesKey; 127 | if (Build.VERSION.SDK_INT >= 24) { 128 | CompatibilityInfo compatInfo = (CompatibilityInfo) newInstance(CompatibilityInfo.class); 129 | setFloatField(compatInfo, "applicationScale", resources.hashCode()); 130 | resourcesKey = createResourcesKey(resDir, null, null, null, Display.DEFAULT_DISPLAY, null, compatInfo); 131 | } else if (Build.VERSION.SDK_INT == 23) { 132 | resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode()); 133 | } else if (Build.VERSION.SDK_INT >= 19) { 134 | resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode(), null); 135 | } else if (Build.VERSION.SDK_INT >= 17) { 136 | resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode()); 137 | } else { 138 | resourcesKey = createResourcesKey(resDir, resources.hashCode()); 139 | } 140 | 141 | if (resourcesKey != null) { 142 | if (Build.VERSION.SDK_INT >= 24) { 143 | Object resImpl = getObjectField(resources, "mResourcesImpl"); 144 | getResourcesMap(thread).put(resourcesKey, new WeakReference<>(resImpl)); 145 | } else { 146 | getResourcesMap(thread).put(resourcesKey, new WeakReference<>(resources)); 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * Returns the name of the current process. It's usually the same as the main package name. 153 | */ 154 | public static String currentProcessName() { 155 | String processName = ActivityThread.currentPackageName(); 156 | if (processName == null) 157 | return "android"; 158 | return processName; 159 | } 160 | 161 | /** 162 | * Returns information about the main application in the current process. 163 | * 164 | *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the 165 | * Keyguard which both have {@code android:process="com.android.systemui"} set in their 166 | * manifest. In those cases, the first application that was initialized will be returned. 167 | */ 168 | public static ApplicationInfo currentApplicationInfo() { 169 | ActivityThread am = ActivityThread.currentActivityThread(); 170 | if (am == null) 171 | return null; 172 | 173 | Object boundApplication = getObjectField(am, "mBoundApplication"); 174 | if (boundApplication == null) 175 | return null; 176 | 177 | return (ApplicationInfo) getObjectField(boundApplication, "appInfo"); 178 | } 179 | 180 | /** 181 | * Returns the Android package name of the main application in the current process. 182 | * 183 | *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the 184 | * Keyguard which both have {@code android:process="com.android.systemui"} set in their 185 | * manifest. In those cases, the first application that was initialized will be returned. 186 | */ 187 | public static String currentPackageName() { 188 | ApplicationInfo ai = currentApplicationInfo(); 189 | return (ai != null) ? ai.packageName : "android"; 190 | } 191 | 192 | /** 193 | * Returns the main {@link Application} object in the current process. 194 | * 195 | *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the 196 | * Keyguard which both have {@code android:process="com.android.systemui"} set in their 197 | * manifest. In those cases, the first application that was initialized will be returned. 198 | */ 199 | public static Application currentApplication() { 200 | return ActivityThread.currentApplication(); 201 | } 202 | 203 | /** @deprecated Use {@link XSharedPreferences} instead. */ 204 | @SuppressWarnings("UnusedParameters") 205 | @Deprecated 206 | public static SharedPreferences getSharedPreferencesForPackage(String packageName, String prefFileName, int mode) { 207 | return new XSharedPreferences(packageName, prefFileName); 208 | } 209 | 210 | /** @deprecated Use {@link XSharedPreferences} instead. */ 211 | @Deprecated 212 | public static SharedPreferences getDefaultSharedPreferencesForPackage(String packageName) { 213 | return new XSharedPreferences(packageName); 214 | } 215 | 216 | /** @deprecated Use {@link XSharedPreferences#reload} instead. */ 217 | @Deprecated 218 | public static void reloadSharedPreferencesIfNeeded(SharedPreferences pref) { 219 | if (pref instanceof XSharedPreferences) { 220 | ((XSharedPreferences) pref).reload(); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /loader/src/main/java/android/app/LoadedApk.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | 5 | public final class LoadedApk { 6 | public ApplicationInfo getApplicationInfo() { 7 | throw new UnsupportedOperationException("STUB"); 8 | } 9 | 10 | public ClassLoader getClassLoader() { 11 | throw new UnsupportedOperationException("STUB"); 12 | } 13 | 14 | public String getPackageName() { 15 | throw new UnsupportedOperationException("STUB"); 16 | } 17 | 18 | public String getResDir() { 19 | throw new UnsupportedOperationException("STUB"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /loader/src/main/java/android/app/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains {@link android.app.AndroidAppHelper} with various methods for information about the current app. 3 | */ 4 | package android.app; 5 | -------------------------------------------------------------------------------- /loader/src/main/java/android/content/res/CompatibilityInfo.java: -------------------------------------------------------------------------------- 1 | package android.content.res; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class CompatibilityInfo implements Parcelable { 7 | @Override 8 | public int describeContents() { 9 | throw new UnsupportedOperationException("STUB"); 10 | } 11 | 12 | @Override 13 | public void writeToParcel(Parcel dest, int flags) { 14 | throw new UnsupportedOperationException("STUB"); 15 | } 16 | 17 | public static final Parcelable.Creator CREATOR = null; 18 | } -------------------------------------------------------------------------------- /loader/src/main/java/android/content/res/XResForwarder.java: -------------------------------------------------------------------------------- 1 | package android.content.res; 2 | 3 | /** 4 | * Instances of this class can be used for {@link XResources#setReplacement(String, String, String, Object)} 5 | * and its variants. They forward the resource request to a different {@link Resources} 6 | * instance with a possibly different ID. 7 | * 8 | *

Usually, instances aren't created directly but via {@link XModuleResources#fwd}. 9 | */ 10 | public class XResForwarder { 11 | private final Resources res; 12 | private final int id; 13 | 14 | /** 15 | * Creates a new instance. 16 | * 17 | * @param res The target {@link Resources} instance to forward requests to. 18 | * @param id The target resource ID. 19 | */ 20 | public XResForwarder(Resources res, int id) { 21 | this.res = res; 22 | this.id = id; 23 | } 24 | 25 | /** Returns the target {@link Resources} instance. */ 26 | public Resources getResources() { 27 | return res; 28 | } 29 | 30 | /** Returns the target resource ID. */ 31 | public int getId() { 32 | return id; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /loader/src/main/java/android/content/res/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains classes that are required for replacing resources. 3 | */ 4 | package android.content.res; 5 | -------------------------------------------------------------------------------- /loader/src/main/java/com/storm/wind/xposed/MainTestActivity.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xposed; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | //import android.support.v4.app.ActivityCompat; 9 | import android.view.View; 10 | 11 | public class MainTestActivity extends Activity { 12 | 13 | //读写权限 14 | private static String[] PERMISSIONS_STORAGE = { 15 | Manifest.permission.READ_EXTERNAL_STORAGE, 16 | Manifest.permission.WRITE_EXTERNAL_STORAGE}; 17 | private static final int REQUEST_PERMISSION_CODE = 1; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | 24 | // 需要文件读写权限才能读取sd卡里,用于管理xposed module的文件:xposed_config/modules.list 25 | // if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { 26 | // if (ActivityCompat.checkSelfPermission(this, 27 | // Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 28 | // ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE); 29 | // } 30 | // } 31 | } 32 | 33 | public void onClick(View view) { 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /loader/src/main/java/com/storm/wind/xposed/XposedTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xposed; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.graphics.Rect; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | 9 | import com.wind.xposed.entry.XposedModuleEntry; 10 | 11 | import java.lang.reflect.Method; 12 | 13 | //import de.robv.android.xposed.XC_MethodHook; 14 | //import de.robv.android.xposed.XposedBridge; 15 | //import de.robv.android.xposed.XposedHelper; 16 | //import de.robv.android.xposed.XposedHelpers; 17 | 18 | public class XposedTestApplication extends Application { 19 | 20 | static { 21 | // 加载系统中所有已安装的Xposed Modules 22 | XposedModuleEntry.init(); 23 | } 24 | 25 | @Override 26 | public void onCreate() { 27 | super.onCreate(); 28 | test(); 29 | // hookOnCreate(); 30 | } 31 | 32 | // private void hookOnCreate() { 33 | // XposedHelpers.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodHook() { 34 | // @Override 35 | // protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 36 | // super.beforeHookedMethod(param); 37 | // Log.e("xiawanli", " beforeHookedMethod onCreate"); 38 | // } 39 | // 40 | // @Override 41 | // protected void afterHookedMethod(MethodHookParam param) throws Throwable { 42 | // super.afterHookedMethod(param); 43 | // Log.e("xiawanli", " beforeHookedMethod onCreate"); 44 | // } 45 | // }); 46 | // XposedHelpers.findAndHookMethod(Test.class, "add", new XC_MethodHook() { 47 | // @Override 48 | // protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 49 | // super.beforeHookedMethod(param); 50 | // Log.e("xiawanli", " beforeHookedMethod Test add"); 51 | // } 52 | // 53 | // @Override 54 | // protected void afterHookedMethod(MethodHookParam param) throws Throwable { 55 | // super.afterHookedMethod(param); 56 | // Log.e("xiawanli", " afterHookedMethod Test add"); 57 | // } 58 | // }); 59 | // } 60 | 61 | private void test() { 62 | try { 63 | Class activityThreadClass = Class.forName("android.app.ActivityThread"); 64 | Log.e("XposedApplication", " activityThreadClass --> " + activityThreadClass); 65 | Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 66 | currentActivityThreadMethod.setAccessible(true); 67 | 68 | Log.e("XposedApplication", " currentActivityThreadMethod --> " + currentActivityThreadMethod); 69 | 70 | Object activityThreadObj = currentActivityThreadMethod.invoke(null); 71 | } catch (Exception e) { 72 | e.printStackTrace(); 73 | Log.e("XposedApplication", " exception --> ", e); 74 | } 75 | 76 | try { 77 | Class activityThreadClass = Class.forName("android.view.ScrollCaptureTargetResolver"); 78 | Log.e("XposedApplication", " ScrollCaptureTargetResolver --> " + activityThreadClass); 79 | Method nullOrEmpty = activityThreadClass.getDeclaredMethod("nullOrEmpty", Rect.class); 80 | nullOrEmpty.setAccessible(true); 81 | 82 | Object nullOrEmpty_result = nullOrEmpty.invoke(null, new Rect()); 83 | Log.e("XposedApplication", " nullOrEmpty --> " + nullOrEmpty + " nullOrEmpty_result = " + nullOrEmpty_result); 84 | 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | Log.e("XposedApplication", " exception --> ", e); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xpatch/proxy/XpatchProxyApplication.java: -------------------------------------------------------------------------------- 1 | package com.wind.xpatch.proxy; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.Log; 7 | 8 | import com.wind.xposed.entry.SandHookInitialization; 9 | import com.wind.xposed.entry.util.FileUtils; 10 | import com.wind.xposed.entry.util.VMRuntime; 11 | import com.wind.xposed.entry.util.XpatchUtils; 12 | 13 | import java.util.ArrayList; 14 | 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import de.robv.android.xposed.XposedBridge; 17 | import de.robv.android.xposed.XposedHelpers; 18 | 19 | import static com.wind.xposed.entry.XposedModuleEntry.init; 20 | 21 | import org.lsposed.hiddenapibypass.HiddenApiBypass; 22 | 23 | /** 24 | * Created by Windysha 25 | */ 26 | public class XpatchProxyApplication extends Application { 27 | private static String original_application_name = null; 28 | 29 | private static Application sOriginalApplication = null; 30 | 31 | private static ClassLoader appClassLoader; 32 | 33 | private static Object activityThread; 34 | 35 | private static final String ORIGINAL_APPLICATION_NAME_ASSET_PATH = "xpatch_asset/original_application_name.ini"; 36 | 37 | private static final String TAG = "XpatchProxyApplication"; 38 | 39 | static { 40 | 41 | // VMRuntime.setHiddenApiExemptions(new String[]{"L"}); 42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 43 | HiddenApiBypass.addHiddenApiExemptions(""); 44 | } 45 | 46 | Context context = XpatchUtils.createAppContext(); 47 | 48 | SandHookInitialization.init(context); 49 | 50 | original_application_name = FileUtils.readTextFromAssets(context, ORIGINAL_APPLICATION_NAME_ASSET_PATH); 51 | Log.d(TAG, " original_application_name = " + original_application_name); 52 | 53 | if (isApplicationProxied()) { 54 | doHook(); 55 | } 56 | 57 | init(context); 58 | } 59 | 60 | public XpatchProxyApplication() { 61 | super(); 62 | 63 | if (isApplicationProxied()) { 64 | createOriginalApplication(); 65 | } 66 | } 67 | 68 | private static boolean isApplicationProxied() { 69 | if (original_application_name != null && !original_application_name.isEmpty() 70 | && !("android.app.Application").equals(original_application_name)) { 71 | return true; 72 | } else { 73 | return false; 74 | } 75 | } 76 | 77 | @Override 78 | protected void attachBaseContext(Context base) { 79 | 80 | if (isApplicationProxied()) { 81 | // 将applicationInfo中保存的applcation class name还原为真实的application class name 82 | modifyApplicationInfo_className(); 83 | } 84 | 85 | super.attachBaseContext(base); 86 | 87 | if (isApplicationProxied()) { 88 | attachOrignalBaseContext(base); 89 | setLoadedApkField(base); 90 | } 91 | 92 | // setApplicationLoadedApk(base); 93 | } 94 | 95 | private void attachOrignalBaseContext(Context base) { 96 | try { 97 | XposedHelpers.callMethod(sOriginalApplication, "attachBaseContext", base); 98 | } catch (Exception e) { 99 | e.printStackTrace(); 100 | } 101 | } 102 | 103 | private void setLoadedApkField(Context base) { 104 | // mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; 105 | try { 106 | Class contextImplClass = Class.forName("android.app.ContextImpl"); 107 | Object contextImpl = XposedHelpers.callStaticMethod(contextImplClass, "getImpl", base); 108 | Object loadedApk = XposedHelpers.getObjectField(contextImpl, "mPackageInfo"); 109 | XposedHelpers.setObjectField(sOriginalApplication, "mLoadedApk", loadedApk); 110 | } catch (Exception e) { 111 | e.printStackTrace(); 112 | } 113 | } 114 | 115 | @Override 116 | public void onCreate() { 117 | // setLoadedApkField(sOriginalApplication); 118 | // XposedHelpers.setObjectField(sOriginalApplication, "mLoadedApk", XposedHelpers.getObjectField(this, "mLoadedApk")); 119 | super.onCreate(); 120 | 121 | if (isApplicationProxied()) { 122 | // replaceApplication(); 123 | replaceLoadedApk_Application(); 124 | replaceActivityThread_Applicatio(); 125 | 126 | sOriginalApplication.onCreate(); 127 | } 128 | } 129 | 130 | private void replaceLoadedApk_Application() { 131 | try { 132 | // replace LoadedApk.java makeApplication() mActivityThread.mAllApplications.add(app); 133 | ArrayList list = (ArrayList) XposedHelpers.getObjectField(getActivityThread(), "mAllApplications"); 134 | list.add(sOriginalApplication); 135 | 136 | Object mBoundApplication = XposedHelpers.getObjectField(getActivityThread(), "mBoundApplication"); // AppBindData 137 | Object loadedApkObj = XposedHelpers.getObjectField(mBoundApplication, "info"); // info 138 | 139 | // replace LoadedApk.java makeApplication() mApplication = app; 140 | XposedHelpers.setObjectField(loadedApkObj, "mApplication", sOriginalApplication); 141 | 142 | } catch (Exception e) { 143 | e.printStackTrace(); 144 | } 145 | } 146 | 147 | private void replaceActivityThread_Applicatio() { 148 | try { 149 | XposedHelpers.setObjectField(getActivityThread(), "mInitialApplication", sOriginalApplication); 150 | } catch (Exception e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | 155 | private Application createOriginalApplication() { 156 | if (sOriginalApplication == null) { 157 | try { 158 | sOriginalApplication = (Application) getAppClassLoader().loadClass(original_application_name).newInstance(); 159 | } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) { 160 | e.printStackTrace(); 161 | } 162 | } 163 | return sOriginalApplication; 164 | } 165 | 166 | private static ClassLoader getAppClassLoader() { 167 | if (appClassLoader != null) { 168 | return appClassLoader; 169 | } 170 | try { 171 | Object mBoundApplication = XposedHelpers.getObjectField(getActivityThread(), "mBoundApplication"); 172 | Object loadedApkObj = XposedHelpers.getObjectField(mBoundApplication, "info"); 173 | appClassLoader = (ClassLoader) XposedHelpers.callMethod(loadedApkObj, "getClassLoader"); 174 | } catch (Exception e) { 175 | e.printStackTrace(); 176 | } 177 | return appClassLoader; 178 | } 179 | 180 | private static void doHook() { 181 | hookContextImpl_setOuterContext(); 182 | 183 | hook_installContentProviders(); 184 | 185 | hook_Activity_attach(); 186 | hook_Service_attach(); 187 | } 188 | 189 | private static void hookContextImpl_setOuterContext() { 190 | XposedHelpers.findAndHookMethod("android.app.ContextImpl", getAppClassLoader(), "setOuterContext", Context 191 | .class, 192 | new XC_MethodHook() { 193 | @Override 194 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 195 | // if (param.args[0] == FakeApplication.this) { 196 | // param.args[0] = sOriginalApplication; 197 | // } 198 | replaceApplicationParam(param.args); 199 | 200 | // XposedHelpers.setObjectField(param.thisObject, "mOuterContext", sOriginalApplication); 201 | } 202 | }); 203 | } 204 | 205 | private static void hook_installContentProviders() { 206 | // ActivityThread.java handleBindAplication() 207 | // if (!data.restrictedBackupMode) { 208 | // if (!ArrayUtils.isEmpty(data.providers)) { 209 | // installContentProviders(app, data.providers); 210 | // ... 211 | // } 212 | // } 213 | XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.ActivityThread", getAppClassLoader()), 214 | "installContentProviders", 215 | new XC_MethodHook() { 216 | @Override 217 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 218 | replaceApplicationParam(param.args); 219 | } 220 | }); 221 | } 222 | 223 | private static void hook_Activity_attach() { 224 | XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.Activity", getAppClassLoader()), 225 | "attach", 226 | new XC_MethodHook() { 227 | @Override 228 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 229 | replaceApplicationParam(param.args); 230 | } 231 | }); 232 | } 233 | 234 | private static void hook_Service_attach() { 235 | XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.Service", getAppClassLoader()), 236 | "attach", 237 | new XC_MethodHook() { 238 | @Override 239 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 240 | replaceApplicationParam(param.args); 241 | } 242 | }); 243 | } 244 | 245 | private static void replaceApplicationParam(Object[] args) { 246 | if (args == null || args.length == 0) { 247 | return; 248 | } 249 | for (int i = 0; i < args.length; i++) { 250 | if (args[i] instanceof XpatchProxyApplication) { 251 | args[i] = sOriginalApplication; 252 | } 253 | } 254 | } 255 | 256 | private void modifyApplicationInfo_className() { 257 | try { 258 | Object mBoundApplication = XposedHelpers.getObjectField(getActivityThread(), "mBoundApplication"); // AppBindData 259 | Object applicationInfoObj = XposedHelpers.getObjectField(mBoundApplication, "appInfo"); // info 260 | 261 | XposedHelpers.setObjectField(applicationInfoObj, "className", original_application_name); 262 | } catch (Exception e) { 263 | e.printStackTrace(); 264 | } 265 | } 266 | 267 | private static Object getActivityThread() { 268 | if (activityThread == null) { 269 | try { 270 | Class activityThreadClass = Class.forName("android.app.ActivityThread"); 271 | activityThread = XposedHelpers.callStaticMethod(activityThreadClass, "currentActivityThread"); 272 | } catch (Exception e) { 273 | e.printStackTrace(); 274 | } 275 | } 276 | return activityThread; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/SandHookInitialization.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.wind.xposed.entry.util.XpatchUtils; 7 | 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | 12 | /** 13 | * @author Windysha 14 | */ 15 | public class SandHookInitialization { 16 | 17 | public static void init(Context context) { 18 | Log.d("SandHookInitialization", "start init"); 19 | if (context == null) { 20 | Log.e("SandHookInitialization", "try to init SandHook, but app context is null !!!!"); 21 | return; 22 | } 23 | 24 | sandHookCompat(context); 25 | 26 | // SandHookConfig.DEBUG = XpatchUtils.isApkDebugable(context); 27 | // XposedCompat.cacheDir = context.getCacheDir(); 28 | // XposedCompat.context = context; 29 | // XposedCompat.classLoader = context.getClassLoader(); 30 | // XposedCompat.isFirstApplication = true; 31 | 32 | String SandHookConfigClassName = "com.swift.sandhook.SandHookConfig"; 33 | boolean isDebug = XpatchUtils.isApkDebugable(context); 34 | 35 | try { 36 | Class SandHookConfigClaszz = Class.forName(SandHookConfigClassName); 37 | Field DEBUG_field = SandHookConfigClaszz.getDeclaredField("DEBUG"); 38 | DEBUG_field.setAccessible(true); 39 | DEBUG_field.set(null, isDebug); 40 | } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { 41 | e.printStackTrace(); 42 | } 43 | 44 | String XposedCompatClassName = "com.swift.sandhook.xposedcompat.XposedCompat"; 45 | 46 | try { 47 | Class XposedCompatgClaszz = Class.forName(XposedCompatClassName); 48 | 49 | Field cacheDir_field = XposedCompatgClaszz.getDeclaredField("cacheDir"); 50 | cacheDir_field.setAccessible(true); 51 | cacheDir_field.set(null, context.getCacheDir()); 52 | 53 | Field context_field = XposedCompatgClaszz.getDeclaredField("context"); 54 | context_field.setAccessible(true); 55 | context_field.set(null, context); 56 | 57 | Field classLoader_field = XposedCompatgClaszz.getDeclaredField("classLoader"); 58 | classLoader_field.setAccessible(true); 59 | classLoader_field.set(null, context.getClassLoader()); 60 | 61 | Field isFirstApplication_field = XposedCompatgClaszz.getDeclaredField("isFirstApplication"); 62 | isFirstApplication_field.setAccessible(true); 63 | isFirstApplication_field.set(null, true); 64 | } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | private static void sandHookCompat(Context context) { 70 | // SandHook.disableVMInline(); 71 | // SandHook.tryDisableProfile(context.getPackageName()); 72 | // SandHook.disableDex2oatInline(false); 73 | 74 | String className = "com.swift.sandhook.SandHook"; 75 | Class sandHook_Clazz = null; 76 | try { 77 | sandHook_Clazz = Class.forName(className); 78 | } catch (ClassNotFoundException e) { 79 | e.printStackTrace(); 80 | } 81 | if (sandHook_Clazz == null) return; 82 | 83 | try { 84 | Method disableVMInline_method = sandHook_Clazz.getDeclaredMethod("disableVMInline"); 85 | disableVMInline_method.setAccessible(true); 86 | disableVMInline_method.invoke(null); 87 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 88 | Log.e("SandHookInitialization", " exception: ", e); 89 | } 90 | 91 | try { 92 | Method tryDisableProfile_method = sandHook_Clazz.getDeclaredMethod("tryDisableProfile", String.class); 93 | tryDisableProfile_method.setAccessible(true); 94 | tryDisableProfile_method.invoke(null, context.getPackageName()); 95 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 96 | Log.e("SandHookInitialization", " exception: ", e); 97 | } 98 | 99 | try { 100 | Method disableDex2oatInline_method = sandHook_Clazz.getDeclaredMethod("disableDex2oatInline", boolean.class); 101 | disableDex2oatInline_method.setAccessible(true); 102 | disableDex2oatInline_method.invoke(null, false); 103 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 104 | Log.e("SandHookInitialization", " exception: ", e); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/XposedHookLoadPackageInner.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry; 2 | 3 | import com.wind.xposed.entry.hooker.PackageSignatureHooker; 4 | 5 | import de.robv.android.xposed.IXposedHookLoadPackage; 6 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 7 | 8 | /** 9 | * Created by Wind 10 | */ 11 | public class XposedHookLoadPackageInner implements IXposedHookLoadPackage { 12 | 13 | private static final String TAG = "XH_LoadPackageInner"; 14 | 15 | protected static XposedHookLoadPackageInner newIntance() { 16 | return new XposedHookLoadPackageInner(); 17 | } 18 | 19 | @Override 20 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { 21 | new PackageSignatureHooker().handleLoadPackage(lpparam); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/XposedModuleLoader.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.pm.ApplicationInfo; 5 | import android.util.Log; 6 | 7 | import com.wind.xposed.entry.util.XLog; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.lang.reflect.Method; 15 | 16 | import dalvik.system.DexClassLoader; 17 | import de.robv.android.xposed.IXposedHookInitPackageResources; 18 | import de.robv.android.xposed.IXposedHookLoadPackage; 19 | import de.robv.android.xposed.IXposedHookZygoteInit; 20 | import de.robv.android.xposed.XposedBridge; 21 | import de.robv.android.xposed.XposedHelper; 22 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 23 | 24 | public class XposedModuleLoader { 25 | 26 | private static final String TAG = "XposedModuleLoader"; 27 | 28 | public static boolean loadModule(final String moduleApkPath, String moduleOdexDir, String moduleLibPath, 29 | final ApplicationInfo currentApplicationInfo, ClassLoader appClassLoader) { 30 | 31 | XLog.i(TAG, "Loading modules from " + moduleApkPath); 32 | 33 | if (!new File(moduleApkPath).exists()) { 34 | Log.e(TAG, moduleApkPath + " does not exist"); 35 | return false; 36 | } 37 | 38 | ClassLoader mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, appClassLoader); 39 | InputStream is = mcl.getResourceAsStream("assets/xposed_init"); 40 | if (is == null) { 41 | Log.i(TAG, "assets/xposed_init not found in the APK"); 42 | return false; 43 | } 44 | 45 | BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is)); 46 | try { 47 | String moduleClassName; 48 | while ((moduleClassName = moduleClassesReader.readLine()) != null) { 49 | moduleClassName = moduleClassName.trim(); 50 | if (moduleClassName.isEmpty() || moduleClassName.startsWith("#")) 51 | continue; 52 | 53 | try { 54 | XLog.i(TAG, " Loading class " + moduleClassName); 55 | Class moduleClass = mcl.loadClass(moduleClassName); 56 | 57 | if (!XposedHelper.isIXposedMod(moduleClass)) { 58 | Log.i(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it"); 59 | continue; 60 | } else if (IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) { 61 | Log.i(TAG, " This class requires resource-related hooks (which are disabled), skipping it."); 62 | continue; 63 | } 64 | 65 | final Object moduleInstance = moduleClass.newInstance(); 66 | if (moduleInstance instanceof IXposedHookZygoteInit) { 67 | XposedHelper.callInitZygote(moduleApkPath, moduleInstance); 68 | } 69 | 70 | if (moduleInstance instanceof IXposedHookLoadPackage) { 71 | // hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance)); 72 | IXposedHookLoadPackage.Wrapper wrapper = new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance); 73 | XposedBridge.CopyOnWriteSortedSet xc_loadPackageCopyOnWriteSortedSet = new XposedBridge.CopyOnWriteSortedSet<>(); 74 | xc_loadPackageCopyOnWriteSortedSet.add(wrapper); 75 | XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(xc_loadPackageCopyOnWriteSortedSet); 76 | lpparam.packageName = currentApplicationInfo.packageName; 77 | lpparam.processName = getCurrentProcessName(currentApplicationInfo);; 78 | lpparam.classLoader = appClassLoader; 79 | lpparam.appInfo = currentApplicationInfo; 80 | lpparam.isFirstApplication = true; 81 | XC_LoadPackage.callAll(lpparam); 82 | } 83 | 84 | if (moduleInstance instanceof IXposedHookInitPackageResources) { 85 | // hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance)); 86 | // TODO: Support Resource hook 87 | } 88 | 89 | } catch (Throwable t) { 90 | Log.e(TAG, " error ", t); 91 | } 92 | } 93 | } catch (IOException e) { 94 | Log.e(TAG, " error ", e); 95 | } finally { 96 | try { 97 | is.close(); 98 | } catch (IOException ignored) { 99 | } 100 | } 101 | return true; 102 | } 103 | 104 | public static void startInnerHook(ApplicationInfo applicationInfo, ClassLoader originClassLoader) { 105 | IXposedHookLoadPackage.Wrapper wrapper = new IXposedHookLoadPackage.Wrapper(XposedHookLoadPackageInner.newIntance()); 106 | 107 | XposedBridge.CopyOnWriteSortedSet xc_loadPackageCopyOnWriteSortedSet = new XposedBridge.CopyOnWriteSortedSet<>(); 108 | xc_loadPackageCopyOnWriteSortedSet.add(wrapper); 109 | 110 | XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(xc_loadPackageCopyOnWriteSortedSet); 111 | 112 | lpparam.packageName = applicationInfo.packageName; 113 | lpparam.processName = getCurrentProcessName(applicationInfo); 114 | lpparam.classLoader = originClassLoader; 115 | lpparam.appInfo = applicationInfo; 116 | lpparam.isFirstApplication = true; 117 | 118 | XC_LoadPackage.callAll(lpparam); 119 | } 120 | 121 | private static String currentProcessName = null; 122 | 123 | @SuppressLint("DiscouragedPrivateApi") 124 | private static String getCurrentProcessName(ApplicationInfo applicationInfo) { 125 | if (currentProcessName != null) return currentProcessName; 126 | 127 | currentProcessName = applicationInfo.packageName; 128 | try { 129 | Class activityThread_clazz = Class.forName("android.app.ActivityThread"); 130 | Method method = activityThread_clazz.getDeclaredMethod("currentProcessName"); 131 | method.setAccessible(true); 132 | currentProcessName = (String) method.invoke(null); 133 | } catch (Exception e) { 134 | e.printStackTrace(); 135 | } 136 | return currentProcessName; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/hooker/PackageSignatureHooker.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.hooker; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.content.pm.Signature; 7 | import android.os.Build; 8 | import android.util.Log; 9 | 10 | // import com.lody.whale.WhaleRuntime; 11 | import com.wind.xposed.entry.XposedModuleEntry; 12 | import com.wind.xposed.entry.util.FileUtils; 13 | 14 | import java.lang.reflect.Field; 15 | import java.lang.reflect.InvocationHandler; 16 | import java.lang.reflect.Method; 17 | import java.lang.reflect.Proxy; 18 | 19 | import de.robv.android.xposed.IXposedHookLoadPackage; 20 | import de.robv.android.xposed.XC_MethodHook; 21 | import de.robv.android.xposed.XposedHelpers; 22 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 23 | 24 | public class PackageSignatureHooker implements IXposedHookLoadPackage { 25 | 26 | private final static String SIGNATURE_INFO_ASSET_PATH = "xpatch_asset/original_signature_info.ini"; 27 | 28 | @Override 29 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { 30 | 31 | Context context = XposedModuleEntry.getAppContext(); 32 | final String originalSignature = getOriginalSignatureFromAsset(context); 33 | android.util.Log.d("PackageSignatureHooker", "Get the original signature --> " + originalSignature); 34 | if (originalSignature == null || originalSignature.isEmpty()) { 35 | return; 36 | } 37 | 38 | // hookSignatureByXposed(lpparam, originalSignature); //不稳定 暂时不使用 39 | hookSignatureByProxy(lpparam, originalSignature, context); 40 | 41 | } 42 | 43 | private void hookSignatureByXposed(XC_LoadPackage.LoadPackageParam lpparam, final String originalSignature) { 44 | final String currentPackageName = lpparam.packageName; 45 | 46 | XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager", lpparam.classLoader, 47 | "getPackageInfo", String.class, int.class, 48 | new XC_MethodHook() { 49 | @Override 50 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 51 | try { 52 | int flag = (int) param.args[1]; 53 | PackageInfo packageInfo = (PackageInfo) param.getResult(); 54 | android.util.Log.d("PackageSignatureHooker", "Get flag " + flag + " packageInfo =" + 55 | packageInfo); 56 | 57 | if (PackageManager.GET_SIGNATURES == flag) { 58 | if (param.args[0] != null && param.args[0] instanceof String) { 59 | String packageName = (String) param.args[0]; 60 | if (!packageName.equals(currentPackageName)) { 61 | return; 62 | } 63 | } 64 | 65 | // 先获取这个方法返回的结果 66 | if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { 67 | android.util.Log.d("PackageSignatureHooker", "ackageInfo.signatures " + packageInfo.signatures 68 | + " packageInfo =" + 69 | packageInfo); 70 | // 替换结果里的签名信息 71 | packageInfo.signatures[0] = new Signature(originalSignature); 72 | } 73 | } else if (Build.VERSION.SDK_INT >= 28 && PackageManager.GET_SIGNING_CERTIFICATES == flag) { 74 | if (param.args[0] != null && param.args[0] instanceof String) { 75 | String packageName = (String) param.args[0]; 76 | if (!packageName.equals(currentPackageName)) { 77 | return; 78 | } 79 | } 80 | 81 | if (packageInfo.signingInfo != null) { 82 | Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); 83 | if (signaturesArray != null && signaturesArray.length > 0) { 84 | signaturesArray[0] = new Signature(originalSignature); 85 | } 86 | } 87 | } 88 | // 更改最终的返回结果 89 | param.setResult(packageInfo); 90 | } catch (Throwable e) { 91 | e.printStackTrace(); 92 | android.util.Log.e("PackageSignatureHooker", "Get the original signature " + 93 | " failed !!!!!!!! ", e); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | private void hookSignatureByProxy(XC_LoadPackage.LoadPackageParam lpparam, String originalSignature, Context context) { 100 | try { 101 | 102 | // Just make sure whale so is loaded, the the hidden policy is disabled. 103 | // WhaleRuntime.reserved2(); 104 | 105 | Class activityThreadClass = Class.forName("android.app.ActivityThread"); 106 | Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 107 | // 获取全局的ActivityThread对象的实现 108 | Object activityThreadObj = currentActivityThreadMethod.invoke(null); 109 | 110 | // 获取ActivityThread里的getPackageManager方法获取原始的sPackageManager对象 111 | Method getPackageManagerMethod = activityThreadClass.getDeclaredMethod("getPackageManager"); 112 | getPackageManagerMethod.setAccessible(true); 113 | Object packageManagerObj = getPackageManagerMethod.invoke(activityThreadObj); 114 | 115 | // 准备好代理对象, 用来替换原始的对象 116 | Class iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); 117 | Object proxy = Proxy.newProxyInstance( 118 | iPackageManagerInterface.getClassLoader(), 119 | new Class[]{iPackageManagerInterface}, 120 | new MyInvocationHandler(packageManagerObj, lpparam.packageName, originalSignature)); 121 | // 1. 替换掉ActivityThread里面的 sPackageManager 字段 122 | Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager"); 123 | sPackageManagerField.setAccessible(true); 124 | sPackageManagerField.set(activityThreadObj, proxy); 125 | // 2. 替换 ApplicationPackageManager里面的 mPM对象 126 | PackageManager pm = context.getPackageManager(); 127 | Field mPmField = pm.getClass().getDeclaredField("mPM"); 128 | mPmField.setAccessible(true); 129 | mPmField.set(pm, proxy); 130 | } catch (Exception e) { 131 | android.util.Log.e("PackageSignatureHooker", " hookSignatureByProxy failed !!", e); 132 | } 133 | } 134 | 135 | static class MyInvocationHandler implements InvocationHandler { 136 | 137 | private Object pmBase; 138 | private String currentPackageName; 139 | private String originalSignature; 140 | 141 | public MyInvocationHandler(Object base, String currentPackageName, String originalSignature) { 142 | pmBase = base; 143 | this.currentPackageName = currentPackageName; 144 | this.originalSignature = originalSignature; 145 | } 146 | 147 | @Override 148 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 149 | if ("getPackageInfo".equals(method.getName())) { 150 | try { 151 | if (args[0] != null && args[0] instanceof String) { 152 | String packageName = (String) args[0]; 153 | if (!packageName.equals(currentPackageName)) { 154 | return method.invoke(pmBase, args); 155 | } 156 | } 157 | 158 | int flag = 0; 159 | if (args[1] instanceof Long) { 160 | flag = ((Long) args[1]).intValue(); 161 | } else if (args[1] instanceof Integer) { 162 | flag = ((Integer) args[1]); 163 | } else { 164 | flag = (int) args[1]; 165 | } 166 | if (PackageManager.GET_SIGNATURES == flag) { 167 | PackageInfo packageInfo = (PackageInfo) method.invoke(pmBase, args); 168 | 169 | // 先获取这个方法返回的结果 170 | if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { 171 | // 替换结果里的签名信息 172 | packageInfo.signatures[0] = new Signature(originalSignature); 173 | } 174 | return packageInfo; 175 | } else if (Build.VERSION.SDK_INT >= 28 && PackageManager.GET_SIGNING_CERTIFICATES == flag) { 176 | PackageInfo packageInfo = (PackageInfo) method.invoke(pmBase, args); 177 | if (packageInfo.signingInfo != null) { 178 | Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); 179 | if (signaturesArray != null && signaturesArray.length > 0) { 180 | signaturesArray[0] = new Signature(originalSignature); 181 | } 182 | } 183 | return packageInfo; 184 | } 185 | } catch (Exception e) { 186 | android.util.Log.e("PackageSignatureHooker", " invoke PackageManager getPackageInfo failed !!", e); 187 | e.printStackTrace(); 188 | } 189 | } 190 | return method.invoke(pmBase, args); 191 | } 192 | } 193 | 194 | private String getOriginalSignatureFromAsset(Context context) { 195 | return FileUtils.readTextFromAssets(context, SIGNATURE_INFO_ASSET_PATH); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.os.Process; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.Closeable; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | 13 | public class FileUtils { 14 | 15 | //读写权限 16 | private static String[] PERMISSIONS_STORAGE = { 17 | Manifest.permission.READ_EXTERNAL_STORAGE, 18 | Manifest.permission.WRITE_EXTERNAL_STORAGE}; 19 | 20 | public static boolean isFilePermissionGranted(Context context) { 21 | int pid = android.os.Process.myPid(); 22 | int uid = Process.myUid(); 23 | return context.checkPermission(PERMISSIONS_STORAGE[0], pid, uid) == PackageManager.PERMISSION_GRANTED && 24 | context.checkPermission(PERMISSIONS_STORAGE[1], pid, uid) == PackageManager.PERMISSION_GRANTED; 25 | } 26 | 27 | public static String readTextFromAssets(Context context, String assetsFileName) { 28 | if (context == null) { 29 | return null; 30 | } 31 | try { 32 | InputStream is = context.getAssets().open(assetsFileName); 33 | return readTextFromInputStream(is); 34 | } catch (Exception e) { 35 | e.printStackTrace(); 36 | } 37 | return null; 38 | } 39 | 40 | public static String readTextFromInputStream(InputStream is) { 41 | InputStreamReader reader = null; 42 | BufferedReader bufferedReader = null; 43 | try { 44 | reader = new InputStreamReader(is, "UTF-8"); 45 | bufferedReader = new BufferedReader(reader); 46 | StringBuilder builder = new StringBuilder(); 47 | String str; 48 | while ((str = bufferedReader.readLine()) != null) { 49 | builder.append(str); 50 | } 51 | return builder.toString(); 52 | } catch (Exception e) { 53 | e.printStackTrace(); 54 | } finally { 55 | closeSafely(reader); 56 | closeSafely(bufferedReader); 57 | } 58 | return null; 59 | } 60 | 61 | private static void closeSafely(Closeable closeable) { 62 | try { 63 | if (closeable != null) { 64 | closeable.close(); 65 | } 66 | } catch (Exception e) { 67 | e.printStackTrace(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/NativeLibraryHelperCompat.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.os.Process; 6 | import android.util.Log; 7 | 8 | import java.io.File; 9 | import java.util.Collections; 10 | import java.util.Enumeration; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.zip.ZipEntry; 14 | import java.util.zip.ZipFile; 15 | 16 | public class NativeLibraryHelperCompat { 17 | private static final String TAG = "NativeLibraryHelper"; 18 | 19 | public static int copyNativeBinaries(File apkFile, File sharedLibraryDir) { 20 | Log.i(TAG, " copyNativeBinaries !!! apkFile = " + apkFile.getAbsolutePath() + " sharedLibraryDir = " + sharedLibraryDir.getAbsolutePath()); 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 22 | return copyNativeBinariesAfterL(apkFile, sharedLibraryDir); 23 | } else { 24 | return copyNativeBinariesBeforeL(apkFile, sharedLibraryDir); 25 | } 26 | } 27 | 28 | private static int copyNativeBinariesBeforeL(File apkFile, File sharedLibraryDir) { 29 | try { 30 | String className = "com.android.internal.content.NativeLibraryHelper"; 31 | Object result = ReflectUtils.callMethod(className, 32 | null, "copyNativeBinariesIfNeededLI", 33 | apkFile, sharedLibraryDir); 34 | if (result != null) { 35 | return (int) result; 36 | } 37 | } catch (Throwable e) { 38 | e.printStackTrace(); 39 | } 40 | return -1; 41 | } 42 | 43 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 44 | private static int copyNativeBinariesAfterL(File apkFile, File sharedLibraryDir) { 45 | try { 46 | String handleClassName = "com.android.internal.content.NativeLibraryHelper$Handle"; 47 | Object handle = ReflectUtils.callMethod(handleClassName, 48 | null, "create", apkFile); 49 | if (handle == null) { 50 | return -1; 51 | } 52 | 53 | String abi = null; 54 | Set abiSet = getSupportAbiList(apkFile.getAbsolutePath()); 55 | if (abiSet.isEmpty()) { 56 | return 0; 57 | } 58 | boolean is64Bit = is64bit(); 59 | String className = "com.android.internal.content.NativeLibraryHelper"; 60 | if (is64Bit && contain64bitAbi(abiSet)) { 61 | if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { 62 | int abiIndex = (int) ReflectUtils.callMethod(className, 63 | null, 64 | "findSupportedAbi", 65 | handle, Build.SUPPORTED_64_BIT_ABIS); 66 | if (abiIndex >= 0) { 67 | abi = Build.SUPPORTED_64_BIT_ABIS[abiIndex]; 68 | } 69 | } 70 | } else { 71 | if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { 72 | int abiIndex = (int) ReflectUtils.callMethod(className, 73 | null, 74 | "findSupportedAbi", 75 | handle, Build.SUPPORTED_32_BIT_ABIS); 76 | if (abiIndex >= 0) { 77 | abi = Build.SUPPORTED_32_BIT_ABIS[abiIndex]; 78 | } 79 | } 80 | } 81 | Log.i(TAG, " is64Bit=" + is64Bit + " abi = " + abi + " abiSet = " + abiSet + " sharedLibraryDir =" + sharedLibraryDir); 82 | if (abi == null) { 83 | Log.e(TAG, "Not match any abi." + apkFile.getAbsolutePath()); 84 | return -1; 85 | } 86 | int result = (int) ReflectUtils.callMethod(className, 87 | null, 88 | "copyNativeBinaries", 89 | handle, sharedLibraryDir, abi); 90 | Log.i(TAG, "copyNativeBinaries result = " + result + " apkFile path = " + apkFile.getAbsolutePath()); 91 | return result; 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | } 95 | return -1; 96 | } 97 | 98 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 99 | public static boolean is64bitAbi(String abi) { 100 | return "arm64-v8a".equals(abi) 101 | || "x86_64".equals(abi) 102 | || "mips64".equals(abi); 103 | } 104 | 105 | public static boolean is32bitAbi(String abi) { 106 | return "armeabi".equals(abi) 107 | || "armeabi-v7a".equals(abi) 108 | || "mips".equals(abi) 109 | || "x86".equals(abi); 110 | } 111 | 112 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 113 | public static boolean contain64bitAbi(Set supportedABIs) { 114 | for (String supportedAbi : supportedABIs) { 115 | if (is64bitAbi(supportedAbi)) { 116 | return true; 117 | } 118 | } 119 | return false; 120 | } 121 | 122 | public static Set getSupportAbiList(String apk) { 123 | try { 124 | ZipFile apkFile = new ZipFile(apk); 125 | Enumeration entries = apkFile.entries(); 126 | Set supportedABIs = new HashSet(); 127 | while (entries.hasMoreElements()) { 128 | ZipEntry entry = entries.nextElement(); 129 | String name = entry.getName(); 130 | if (name.contains("../")) { 131 | continue; 132 | } 133 | if (name.startsWith("lib/") && !entry.isDirectory() && name.endsWith(".so")) { 134 | String supportedAbi = name.substring(name.indexOf("/") + 1, name.lastIndexOf("/")); 135 | supportedABIs.add(supportedAbi); 136 | } 137 | } 138 | return supportedABIs; 139 | } catch (Exception e) { 140 | e.printStackTrace(); 141 | } 142 | return Collections.emptySet(); 143 | } 144 | 145 | public static boolean is64bit() { 146 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 147 | return false; 148 | } 149 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 150 | return Process.is64Bit(); 151 | } 152 | Object runtime = ReflectUtils.callMethod("dalvik.system.VMRuntime", null, "getRuntime"); 153 | Object is64Bit = ReflectUtils.callMethod("dalvik.system.VMRuntime", runtime, "is64Bit"); 154 | if (is64Bit == null) return true; 155 | return (boolean) is64Bit; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/PackageNameCache.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by Wind 12 | */ 13 | public class PackageNameCache { 14 | 15 | private static final String TAG = PackageNameCache.class.getSimpleName(); 16 | 17 | private Context mContext; 18 | private Map mPackageNameMap = new HashMap<>(); 19 | 20 | private static PackageNameCache instance; 21 | 22 | private PackageNameCache(Context context) { 23 | this.mContext = context; 24 | } 25 | 26 | public static PackageNameCache getInstance(Context context) { 27 | if (instance == null) { 28 | synchronized (PackageNameCache.class) { 29 | if (instance == null) { 30 | instance = new PackageNameCache(context); 31 | } 32 | } 33 | } 34 | return instance; 35 | } 36 | 37 | public String getPackageNameByPath(String apkPath) { 38 | if (apkPath == null || apkPath.length() == 0) { 39 | return ""; 40 | } 41 | String packageName = mPackageNameMap.get(apkPath); 42 | if (packageName != null && packageName.length() > 0) { 43 | return packageName; 44 | } 45 | packageName = ""; 46 | PackageManager pm = mContext.getPackageManager(); 47 | long startTime = System.currentTimeMillis(); 48 | PackageInfo info = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES); 49 | XLog.d(TAG, "Get package name time -> " + (System.currentTimeMillis() - startTime) 50 | + " apkPath -> " + apkPath); 51 | if (info != null) { 52 | packageName = info.packageName; 53 | mPackageNameMap.put(apkPath, packageName); 54 | } 55 | return packageName; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/PluginNativeLibExtractor.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import static android.content.Context.MODE_PRIVATE; 4 | 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import android.util.Log; 8 | 9 | import java.io.File; 10 | import java.util.Set; 11 | 12 | public class PluginNativeLibExtractor { 13 | 14 | private static final String TAG = "NativeLibExtractor"; 15 | private static final String SHARE_PREF_FILE_NAME = "xpatch_module_native_lib_config"; 16 | 17 | private static SharedPreferences sharedPreferences; 18 | 19 | public static void copySoFileIfNeeded(Context context, String libPath, String pluginApkPath) { 20 | boolean isMainProcess = XpatchUtils.isMainProcess(context); 21 | if (!isMainProcess) { 22 | return; 23 | } 24 | 25 | Set abiSet = NativeLibraryHelperCompat.getSupportAbiList(pluginApkPath); 26 | if (abiSet.isEmpty()) { 27 | Log.i(TAG, " plugin: " + pluginApkPath + " do not contains any so files."); 28 | return; 29 | } 30 | 31 | XpatchUtils.ensurePathExist(libPath); 32 | 33 | Log.i(TAG, " copySoFileIfNeeded procecess = " + XpatchUtils.getCurProcessName(context) + " isMainProcess = " + XpatchUtils.isMainProcess(context)); 34 | Log.i(TAG, " copyPluginSoFile libPath = " + libPath + " pluginApkPath = " + pluginApkPath); 35 | if (sharedPreferences == null) { 36 | sharedPreferences = context.getSharedPreferences(SHARE_PREF_FILE_NAME, MODE_PRIVATE); 37 | } 38 | String savedMd5 = getSavedApkFileMd5(sharedPreferences, pluginApkPath); 39 | String curMd5 = XpatchUtils.getFileMD5(new File(pluginApkPath)); 40 | Log.i(TAG, " copyPluginSoFile savedMd5 = " + savedMd5 + " curMd5 = " + curMd5); 41 | if (savedMd5 == null || savedMd5.isEmpty() || !savedMd5.equals(curMd5)) { 42 | NativeLibraryHelperCompat.copyNativeBinaries(new File(pluginApkPath), new File(libPath)); 43 | saveApkFileMd5(sharedPreferences, pluginApkPath, curMd5); 44 | } else { 45 | Log.d(TAG, "plugin is not changed, no need to copy so file again!"); 46 | } 47 | } 48 | 49 | private static String getSavedApkFileMd5(SharedPreferences sp, String key) { 50 | return sp.getString(key, ""); 51 | } 52 | 53 | private static void saveApkFileMd5(SharedPreferences sp, String key, String md5) { 54 | sp.edit().putString(key, md5).apply(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/ReflectUtils.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | 7 | public class ReflectUtils { 8 | 9 | //获取类的实例的变量的值 10 | public static Object getField(Object receiver, String fieldName) { 11 | return getField(null, receiver, fieldName); 12 | } 13 | 14 | //获取类的静态变量的值 15 | public static Object getField(String className, String fieldName) { 16 | return getField(className,null, fieldName); 17 | } 18 | 19 | public static Object getField(Class clazz, String className, String fieldName, Object receiver) { 20 | try { 21 | if (clazz == null) clazz = Class.forName(className); 22 | Field field = clazz.getDeclaredField(fieldName); 23 | if (field == null) return null; 24 | field.setAccessible(true); 25 | return field.get(receiver); 26 | } catch (Throwable e) { 27 | e.printStackTrace(); 28 | } 29 | return null; 30 | } 31 | 32 | private static Object getField(String className, Object receiver, String fieldName) { 33 | Class clazz = null; 34 | Field field; 35 | if (!isEmpty(className)) { 36 | try { 37 | clazz = Class.forName(className); 38 | } catch (ClassNotFoundException e) { 39 | e.printStackTrace(); 40 | } 41 | } else { 42 | if (receiver != null) { 43 | clazz = receiver.getClass(); 44 | } 45 | } 46 | if (clazz == null) return null; 47 | 48 | try { 49 | field = findField(clazz, fieldName); 50 | if (field == null) return null; 51 | field.setAccessible(true); 52 | return field.get(receiver); 53 | } catch (IllegalAccessException e) { 54 | e.printStackTrace(); 55 | } catch (IllegalArgumentException e) { 56 | e.printStackTrace(); 57 | } catch (NullPointerException e) { 58 | e.printStackTrace(); 59 | } 60 | return null; 61 | } 62 | 63 | public static Object setField(Object receiver, String fieldName, Object value) { 64 | try { 65 | Field field; 66 | field = findField(receiver.getClass(), fieldName); 67 | if (field == null) { 68 | return null; 69 | } 70 | field.setAccessible(true); 71 | Object old = field.get(receiver); 72 | field.set(receiver, value); 73 | return old; 74 | } catch (IllegalAccessException e) { 75 | e.printStackTrace(); 76 | } catch (IllegalArgumentException e) { 77 | e.printStackTrace(); 78 | } 79 | return null; 80 | } 81 | 82 | public static Object setField(Class clazz, Object receiver, String fieldName, Object value) { 83 | try { 84 | Field field; 85 | field = findField(clazz, fieldName); 86 | if (field == null) { 87 | return null; 88 | } 89 | field.setAccessible(true); 90 | Object old = field.get(receiver); 91 | field.set(receiver, value); 92 | return old; 93 | } catch (IllegalAccessException e) { 94 | e.printStackTrace(); 95 | } catch (IllegalArgumentException e) { 96 | e.printStackTrace(); 97 | } 98 | return null; 99 | } 100 | 101 | public static Object setField(String clazzName, Object receiver, String fieldName, Object value){ 102 | try { 103 | Class clazz = Class.forName(clazzName); 104 | Field field; 105 | field = findField(clazz, fieldName); 106 | if (field == null) { 107 | return null; 108 | } 109 | field.setAccessible(true); 110 | Object old = field.get(receiver); 111 | field.set(receiver, value); 112 | return old; 113 | } catch (IllegalAccessException e) { 114 | e.printStackTrace(); 115 | } catch (IllegalArgumentException e) { 116 | e.printStackTrace(); 117 | } catch (ClassNotFoundException e) { 118 | e.printStackTrace(); 119 | } 120 | return null; 121 | } 122 | 123 | public static Object callMethod(String className, Object receiver, String methodName, Object... params) { 124 | Class clazz = null; 125 | if (!isEmpty(className)) { 126 | try { 127 | clazz = Class.forName(className); 128 | } catch (ClassNotFoundException e) { 129 | e.printStackTrace(); 130 | } 131 | } else { 132 | if (receiver != null) { 133 | clazz = receiver.getClass(); 134 | } 135 | } 136 | if (clazz == null) return null; 137 | try { 138 | Method method = findMethod(clazz, methodName, params); 139 | if (method == null) { 140 | return null; 141 | } 142 | method.setAccessible(true); 143 | return method.invoke(receiver, params); 144 | } catch (IllegalArgumentException e) { 145 | e.printStackTrace(); 146 | } catch (IllegalAccessException e) { 147 | e.printStackTrace(); 148 | } catch (InvocationTargetException e) { 149 | e.printStackTrace(); 150 | } 151 | return null; 152 | } 153 | 154 | private static Method findMethod(Class clazz, String name, Object... arg) { 155 | Method[] methods = clazz.getMethods(); 156 | Method method = null; 157 | for (Method m : methods) { 158 | if (methodFitParam(m, name, arg)) { 159 | method = m; 160 | break; 161 | } 162 | } 163 | 164 | if (method == null) { 165 | method = findDeclaredMethod(clazz, name, arg); 166 | } 167 | return method; 168 | } 169 | 170 | private static Method findDeclaredMethod(Class clazz, String name, Object... arg) { 171 | Method[] methods = clazz.getDeclaredMethods(); 172 | Method method = null; 173 | for (Method m : methods) { 174 | if (methodFitParam(m, name, arg)) { 175 | method = m; 176 | break; 177 | } 178 | } 179 | 180 | if (method == null) { 181 | if (clazz.equals(Object.class)) { 182 | return null; 183 | } 184 | return findDeclaredMethod(clazz.getSuperclass(), name, arg); 185 | } 186 | return method; 187 | } 188 | 189 | private static boolean methodFitParam(Method method, String methodName, Object... arg) { 190 | if (!methodName.equals(method.getName())) { 191 | return false; 192 | } 193 | 194 | Class[] paramTypes = method.getParameterTypes(); 195 | if (arg == null || arg.length == 0) { 196 | if (paramTypes == null || paramTypes.length == 0) { 197 | return true; 198 | } else { 199 | return false; 200 | } 201 | } 202 | if (paramTypes.length != arg.length) { 203 | return false; 204 | } 205 | 206 | for (int i = 0; i < arg.length; ++i) { 207 | Object ar = arg[i]; 208 | Class paramT = paramTypes[i]; 209 | if (ar == null) continue; 210 | 211 | //TODO for primitive type 212 | if (paramT.isPrimitive()) continue; 213 | 214 | if (!paramT.isInstance(ar)) { 215 | return false; 216 | } 217 | } 218 | return true; 219 | } 220 | 221 | private static Field findField(Class clazz, String name) { 222 | try { 223 | return clazz.getDeclaredField(name); 224 | } catch (NoSuchFieldException e) { 225 | if (clazz.equals(Object.class)) { 226 | e.printStackTrace(); 227 | return null; 228 | } 229 | Class base = clazz.getSuperclass(); 230 | return findField(base, name); 231 | } 232 | } 233 | 234 | private static boolean isEmpty(String str) { 235 | return str == null || str.length() == 0; 236 | } 237 | } -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/SharedPrefUtils.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | public class SharedPrefUtils { 7 | 8 | private static Context appContext; 9 | 10 | private static final String SHARED_PREFERENE_FILE_PATH = "xpatch_wl_shared_pref"; 11 | 12 | public static void init(Context context) { 13 | appContext = context; 14 | } 15 | 16 | public static long getLong() { 17 | if (appContext == null) { 18 | return 0L; 19 | } 20 | SharedPreferences sharedPreferences = appContext.getSharedPreferences(SHARED_PREFERENE_FILE_PATH, Context.MODE_PRIVATE); 21 | long result = sharedPreferences.getLong("time", 0L); 22 | return result; 23 | } 24 | 25 | public static void putLong(long data) { 26 | if (appContext == null) { 27 | return; 28 | } 29 | SharedPreferences sharedPreferences = appContext.getSharedPreferences(SHARED_PREFERENE_FILE_PATH, Context.MODE_PRIVATE); 30 | SharedPreferences.Editor editor = sharedPreferences.edit(); 31 | editor.putLong("time", data); 32 | editor.apply(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/VMRuntime.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | @SuppressLint("DiscouragedPrivateApi") 9 | public class VMRuntime { 10 | public static void setHiddenApiExemptions(final String[] signaturePrefixes) { 11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 12 | try { 13 | final Method method_getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", 14 | String.class, Class[].class); 15 | final Method method_forName = Class.class.getDeclaredMethod("forName", String.class); 16 | final Class class_VMRuntime = (Class) method_forName.invoke(null, "dalvik.system.VMRuntime"); 17 | final Method method_getRuntime = (Method) method_getDeclaredMethod.invoke(class_VMRuntime, 18 | "getRuntime", null); 19 | final Object object_VMRuntime = method_getRuntime.invoke(null); 20 | 21 | Method setHiddenApiExemptions = null; 22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 23 | final Class class_Unsafe = Class.forName("sun.misc.Unsafe"); 24 | final Method method_getUnsafe = class_Unsafe.getDeclaredMethod("getUnsafe"); 25 | final Object object_Unsafe = method_getUnsafe.invoke(null); 26 | 27 | final Method method_getLong = class_Unsafe.getDeclaredMethod("getLong", long.class); 28 | final Method method_putLong = class_Unsafe.getDeclaredMethod("putLong", long.class, long.class); 29 | 30 | final Method method_getInt = class_Unsafe.getDeclaredMethod("getInt", long.class); 31 | // final Method method_putInt = class_Unsafe.getDeclaredMethod("putInt", long.class, int.class); 32 | 33 | final Method method_addressOf = class_VMRuntime.getDeclaredMethod("addressOf", Object.class); 34 | final Method method_newNonMovableArray = class_VMRuntime.getDeclaredMethod("newNonMovableArray", Class.class, int.class); 35 | 36 | final Method[] declaredMethods = class_VMRuntime.getDeclaredMethods(); 37 | final int length = declaredMethods.length; 38 | final Method[] array = (Method[]) method_newNonMovableArray.invoke(object_VMRuntime, 39 | Method.class, length); 40 | System.arraycopy(declaredMethods, 0, array, 0, length); 41 | 42 | // http://aosp.opersys.com/xref/android-11.0.0_r3/xref/art/runtime/mirror/executable.h 43 | // uint64_t Executable::art_method_ 44 | final int offset_art_method_ = 24; 45 | 46 | final long address = (long) method_addressOf.invoke(object_VMRuntime, (Object) array); 47 | long min = Long.MAX_VALUE, min_second = Long.MAX_VALUE, max = Long.MIN_VALUE; 48 | for (int k = 0; k < length; ++k) { 49 | final long address_Method = (int) method_getInt.invoke(object_Unsafe, address + k * Integer.BYTES); 50 | final long address_art_method = (long) method_getLong.invoke(object_Unsafe, 51 | address_Method + offset_art_method_); 52 | if (min >= address_art_method) { 53 | min = address_art_method; 54 | } else if (min_second >= address_art_method) { 55 | min_second = address_art_method; 56 | } 57 | if (max <= address_art_method) { 58 | max = address_art_method; 59 | } 60 | } 61 | 62 | final long size_art_method = min_second - min; 63 | if (size_art_method > 0 && size_art_method < 100) { 64 | for (min += size_art_method; min < max; min += size_art_method) { 65 | final long address_Method = (int) method_getInt.invoke(object_Unsafe, address); 66 | method_putLong.invoke(object_Unsafe, 67 | address_Method + offset_art_method_, min); 68 | final String name = array[0].getName(); 69 | if ("setHiddenApiExemptions".equals(name)) { 70 | setHiddenApiExemptions = array[0]; 71 | break; 72 | } 73 | } 74 | } 75 | } else { 76 | setHiddenApiExemptions = (Method) method_getDeclaredMethod.invoke(class_VMRuntime, 77 | "setHiddenApiExemptions", new Class[]{String[].class}); 78 | } 79 | 80 | if (setHiddenApiExemptions != null) { 81 | setHiddenApiExemptions.invoke(object_VMRuntime, (Object) signaturePrefixes); 82 | } 83 | } catch (final Exception e) { 84 | e.printStackTrace(); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/XLog.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | 4 | import com.storm.wind.xposed.BuildConfig; 5 | 6 | public class XLog { 7 | 8 | private static boolean enableLog = BuildConfig.DEBUG; 9 | 10 | public static void d(String tag, String msg) { 11 | if (enableLog) { 12 | android.util.Log.d(tag, msg); 13 | } 14 | } 15 | 16 | public static void v(String tag, String msg) { 17 | if (enableLog) { 18 | android.util.Log.v(tag, msg); 19 | } 20 | } 21 | 22 | public static void w(String tag, String msg) { 23 | if (enableLog) { 24 | android.util.Log.w(tag, msg); 25 | } 26 | } 27 | 28 | public static void i(String tag, String msg) { 29 | if (enableLog) { 30 | android.util.Log.i(tag, msg); 31 | } 32 | } 33 | 34 | public static void e(String tag, String msg) { 35 | if (enableLog) { 36 | android.util.Log.e(tag, msg); 37 | } 38 | } 39 | 40 | public static void e(String tag, String msg, Throwable tr) { 41 | if (enableLog) { 42 | android.util.Log.e(tag, msg, tr); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /loader/src/main/java/com/wind/xposed/entry/util/XpatchUtils.java: -------------------------------------------------------------------------------- 1 | package com.wind.xposed.entry.util; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.content.pm.ApplicationInfo; 6 | import android.util.Log; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.lang.reflect.Field; 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.lang.reflect.Method; 17 | import java.math.BigInteger; 18 | import java.security.MessageDigest; 19 | import java.security.NoSuchAlgorithmException; 20 | 21 | public class XpatchUtils { 22 | 23 | private static String sCurProcessName = null; 24 | 25 | public static Context createAppContext() { 26 | 27 | // LoadedApk.makeApplication() 28 | // ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); 29 | 30 | try { 31 | Class activityThreadClass = Class.forName("android.app.ActivityThread"); 32 | Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 33 | currentActivityThreadMethod.setAccessible(true); 34 | 35 | Object activityThreadObj = currentActivityThreadMethod.invoke(null); 36 | 37 | Field boundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); 38 | boundApplicationField.setAccessible(true); 39 | Object mBoundApplication = boundApplicationField.get(activityThreadObj); // AppBindData 40 | 41 | Field infoField = mBoundApplication.getClass().getDeclaredField("info"); // info 42 | infoField.setAccessible(true); 43 | Object loadedApkObj = infoField.get(mBoundApplication); // LoadedApk 44 | 45 | Class contextImplClass = Class.forName("android.app.ContextImpl"); 46 | Method createAppContextMethod = contextImplClass.getDeclaredMethod("createAppContext", activityThreadClass, loadedApkObj.getClass()); 47 | createAppContextMethod.setAccessible(true); 48 | 49 | Object context = createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj); 50 | 51 | if (context instanceof Context) { 52 | return (Context) context; 53 | } 54 | 55 | } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { 56 | e.printStackTrace(); 57 | } 58 | return null; 59 | } 60 | 61 | public static boolean isApkDebugable(Context context) { 62 | try { 63 | ApplicationInfo info = context.getApplicationInfo(); 64 | return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; 65 | } catch (Exception e) { 66 | } 67 | return false; 68 | } 69 | 70 | public static String getCurProcessName(Context context) { 71 | String procName = sCurProcessName; 72 | if (procName != null && !procName.isEmpty()) { 73 | return procName; 74 | } 75 | try { 76 | int pid = android.os.Process.myPid(); 77 | ActivityManager mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 78 | for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager.getRunningAppProcesses()) { 79 | if (appProcess.pid == pid) { 80 | Log.d("Process", "processName = " + appProcess.processName); 81 | sCurProcessName = appProcess.processName; 82 | return sCurProcessName; 83 | } 84 | } 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } 88 | sCurProcessName = getCurProcessNameFromProc(); 89 | return sCurProcessName; 90 | } 91 | 92 | private static String getCurProcessNameFromProc() { 93 | BufferedReader cmdlineReader = null; 94 | try { 95 | cmdlineReader = new BufferedReader(new InputStreamReader( 96 | new FileInputStream( 97 | "/proc/" + android.os.Process.myPid() + "/cmdline"), 98 | "iso-8859-1")); 99 | int c; 100 | StringBuilder processName = new StringBuilder(); 101 | while ((c = cmdlineReader.read()) > 0) { 102 | processName.append((char) c); 103 | } 104 | Log.d("Process", "get processName = " + processName.toString()); 105 | return processName.toString(); 106 | } catch (Throwable e) { 107 | // ignore 108 | } finally { 109 | if (cmdlineReader != null) { 110 | try { 111 | cmdlineReader.close(); 112 | } catch (Exception e) { 113 | // ignore 114 | } 115 | } 116 | } 117 | return null; 118 | } 119 | 120 | public static boolean isMainProcess(Context context) { 121 | String processName = getCurProcessName(context); 122 | if (processName != null && processName.contains(":")) { 123 | return false; 124 | } 125 | return (processName != null && processName.equals(context.getPackageName())); 126 | } 127 | 128 | public static String getFileMD5(File file) { 129 | if (!file.isFile()) { 130 | return ""; 131 | } 132 | MessageDigest digest = null; 133 | BufferedInputStream in = null; 134 | byte buffer[] = new byte[1024]; 135 | int len; 136 | try { 137 | digest = MessageDigest.getInstance("MD5"); 138 | in = new BufferedInputStream(new FileInputStream(file)); 139 | while ((len = in.read(buffer, 0, buffer.length)) != -1) { 140 | digest.update(buffer, 0, len); 141 | } 142 | String md5Result = byteArrayToHex(digest.digest()); 143 | return md5Result; 144 | } catch (Exception e) { 145 | return ""; 146 | } finally { 147 | if (in != null) { 148 | try { 149 | in.close(); 150 | } catch (IOException ignored) { 151 | } 152 | } 153 | } 154 | } 155 | 156 | public static String byteArrayToHex(byte[] byteArray) { 157 | if (byteArray == null || byteArray.length <= 0) { 158 | return ""; 159 | } 160 | // 首先初始化一个字符数组,用来存放每个16进制字符 161 | char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 162 | // new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)) 163 | char[] resultCharArray = new char[byteArray.length * 2]; 164 | // 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去 165 | int index = 0; 166 | for (byte b : byteArray) { 167 | resultCharArray[index++] = hexDigits[b >>> 4 & 0xf]; 168 | resultCharArray[index++] = hexDigits[b & 0xf]; 169 | } 170 | // 字符数组组合成字符串返回 171 | return new String(resultCharArray); 172 | } 173 | 174 | public static String strMd5(String input) { 175 | if (input == null || input.length() == 0) { 176 | return null; 177 | } 178 | try { 179 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 180 | md5.update(input.getBytes()); 181 | byte[] byteArray = md5.digest(); 182 | 183 | BigInteger bigInt = new BigInteger(1, byteArray); 184 | // 参数16表示16进制 185 | String result = bigInt.toString(16); 186 | // 不足32位高位补零 187 | while (result.length() < 32) { 188 | result = "0" + result; 189 | } 190 | return result; 191 | } catch (NoSuchAlgorithmException e) { 192 | e.printStackTrace(); 193 | } 194 | return null; 195 | } 196 | 197 | public static final void ensurePathExist(String path) { 198 | File file = new File(path); 199 | if (!file.exists()) { 200 | file.mkdirs(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /loader/src/main/java/de/robv/android/xposed/XposedHelper.java: -------------------------------------------------------------------------------- 1 | package de.robv.android.xposed; 2 | 3 | import android.util.Log; 4 | 5 | import java.lang.reflect.Member; 6 | 7 | public class XposedHelper { 8 | 9 | private static final String TAG = "XposedHelper"; 10 | 11 | public static void initSeLinux(String processName) { 12 | // SELinuxHelper.initOnce(); 13 | // SELinuxHelper.initForProcess(processName); 14 | } 15 | 16 | public static boolean isIXposedMod(Class moduleClass) { 17 | // Log.d(TAG, "module's classLoader : " + moduleClass.getClassLoader() + ", super: " + moduleClass.getSuperclass()); 18 | // Log.d(TAG, "IXposedMod's classLoader : " + IXposedMod.class.getClassLoader()); 19 | return IXposedMod.class.isAssignableFrom(moduleClass); 20 | } 21 | 22 | 23 | public static XC_MethodHook.Unhook newUnHook(XC_MethodHook methodHook, Member member) { 24 | return methodHook.new Unhook(member); 25 | } 26 | 27 | public static void callInitZygote(String modulePath, Object moduleInstance) throws Throwable { 28 | IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam(); 29 | param.modulePath = modulePath; 30 | param.startsSystemServer = false; 31 | ((IXposedHookZygoteInit) moduleInstance).initZygote(param); 32 | } 33 | 34 | public static void beforeHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable{ 35 | methodHook.beforeHookedMethod(param); 36 | } 37 | 38 | public static void afterHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable{ 39 | methodHook.afterHookedMethod(param); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /loader/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /loader/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/loader/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /loader/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/loader/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /loader/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Xposed Module Loader 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Xpatch' 2 | dependencyResolutionManagement { 3 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 4 | repositories { 5 | google() 6 | mavenCentral() 7 | jcenter() // Warning: this repository is going to shut down soon 8 | } 9 | } 10 | include ':xpatch' 11 | include ':loader' 12 | -------------------------------------------------------------------------------- /xpatch/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /xpatch/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | compileJava.options.encoding = "UTF-8" 6 | 7 | dependencies { 8 | implementation fileTree(dir: 'libs', include: ['*.jar']) 9 | } 10 | 11 | jar { 12 | baseName = 'xpatch' 13 | version = '6.0' 14 | manifest { 15 | attributes 'Main-Class': 'com.storm.wind.xpatch.MainCommand' 16 | } 17 | destinationDirectory = new File("$rootProject.projectDir/out") 18 | //添加将引用的jar的源码打入最终的jar 19 | dependsOn configurations.runtimeClasspath 20 | from { 21 | configurations.runtimeClasspath.collect { 22 | it.isDirectory() ? it : zipTree(it) 23 | } 24 | } 25 | 26 | from fileTree(dir: 'src/main', includes: ['assets/**']) 27 | 28 | //排除引用的jar中的签名信息 29 | exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.MF', 'META-INF/*.txt', "META-INF/versions/**" 30 | 31 | manifest { 32 | attributes("Implementation-Title": baseName, 33 | "Implementation-Version": version, 34 | "Build-Time": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), 35 | "Build-Number": System.env.BUILD_NUMBER ? System.env.BUILD_NUMBER : "-1", 36 | ) 37 | } 38 | from(project.parent.projectDir) { 39 | include 'NOTICE.txt' 40 | include 'LICENSE.txt' 41 | into('META-INF') 42 | } 43 | } 44 | 45 | //添加源码中引入的非代码文件,例如资源等 46 | sourceSets.main.resources { 47 | srcDirs = [ 48 | "src/main/java", 49 | ]; 50 | include "**/*.*" 51 | } 52 | 53 | assemble.dependsOn(':loader:copyLoaderFiles') 54 | build.dependsOn(':loader:copyLoaderFiles') -------------------------------------------------------------------------------- /xpatch/libs/ManifestEditor-aff5123.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/libs/ManifestEditor-aff5123.jar -------------------------------------------------------------------------------- /xpatch/libs/apksigner.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/libs/apksigner.jar -------------------------------------------------------------------------------- /xpatch/libs/dex-tools-2.1-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/libs/dex-tools-2.1-SNAPSHOT.jar -------------------------------------------------------------------------------- /xpatch/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /xpatch/src/main/assets/android.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/android.keystore -------------------------------------------------------------------------------- /xpatch/src/main/assets/dex/sandhook/classes.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/dex/sandhook/classes.dex -------------------------------------------------------------------------------- /xpatch/src/main/assets/dex/whale/classes-1.0.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/dex/whale/classes-1.0.dex -------------------------------------------------------------------------------- /xpatch/src/main/assets/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/keystore -------------------------------------------------------------------------------- /xpatch/src/main/assets/lib/arm64-v8a/libsandhook: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/lib/arm64-v8a/libsandhook -------------------------------------------------------------------------------- /xpatch/src/main/assets/lib/arm64-v8a/libwhale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/lib/arm64-v8a/libwhale -------------------------------------------------------------------------------- /xpatch/src/main/assets/lib/armeabi-v7a/libsandhook: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/lib/armeabi-v7a/libsandhook -------------------------------------------------------------------------------- /xpatch/src/main/assets/lib/armeabi-v7a/libwhale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/lib/armeabi-v7a/libwhale -------------------------------------------------------------------------------- /xpatch/src/main/assets/win/zipalign.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/win/zipalign.exe -------------------------------------------------------------------------------- /xpatch/src/main/assets/xposedmodule/hook_apk_path_module.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/xposedmodule/hook_apk_path_module.apk -------------------------------------------------------------------------------- /xpatch/src/main/assets/zipalign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindySha/Xpatch/83a92d77ecc3767506397c28e54d39152ab171de/xpatch/src/main/assets/zipalign -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/ApkModifyTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.googlecode.dex2jar.tools.Dex2jarCmd; 4 | import com.googlecode.dex2jar.tools.Jar2Dex; 5 | 6 | import java.io.File; 7 | import java.util.ArrayList; 8 | 9 | /** 10 | * Created by Wind 11 | */ 12 | public class ApkModifyTask implements Runnable { 13 | 14 | private static final String JAR_FILE_NAME = "output-jar.jar"; 15 | 16 | private String unzipApkFilePath; 17 | private boolean keepJarFile; 18 | private boolean showAllLogs; 19 | private String applicationName; 20 | 21 | private int dexFileCount; 22 | 23 | public ApkModifyTask(boolean showAllLogs, boolean keepJarFile, String unzipApkFilePath, String applicationName, int 24 | dexFileCount) { 25 | this.showAllLogs = showAllLogs; 26 | this.unzipApkFilePath = unzipApkFilePath; 27 | this.keepJarFile = keepJarFile; 28 | this.applicationName = applicationName; 29 | this.dexFileCount = dexFileCount; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | 35 | File unzipApkFile = new File(unzipApkFilePath); 36 | 37 | String jarOutputPath = unzipApkFile.getParent() + File.separator + JAR_FILE_NAME; 38 | 39 | // classes.dex 40 | String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName); 41 | 42 | if (showAllLogs) { 43 | System.out.println(" the application class is in this dex file = " + targetDexFileName); 44 | } 45 | 46 | String dexOutputPath = unzipApkFilePath + targetDexFileName; 47 | File dexFile = new File(dexOutputPath); 48 | if (dexFile.exists()) { 49 | dexFile.delete(); 50 | } 51 | // 将jar转换为dex文件 52 | jar2DexCmd(jarOutputPath, dexOutputPath); 53 | 54 | // 删除掉jar文件 55 | File jarFile = new File(jarOutputPath); 56 | if (!keepJarFile && jarFile.exists()) { 57 | jarFile.delete(); 58 | } 59 | 60 | } 61 | 62 | private String dumpJarFile(int dexFileCount, String dexFilePath, String jarOutputPath, String applicationName) { 63 | ArrayList dexFileList = createClassesDotDexFileList(dexFileCount); 64 | // String jarOutputPath = dexFilePath + JAR_FILE_NAME; 65 | for (String dexFileName : dexFileList) { 66 | String filePath = dexFilePath + dexFileName; 67 | // 执行dex2jar命令,修改源代码 68 | boolean isApplicationClassFound = dex2JarCmd(filePath, jarOutputPath, applicationName); 69 | // 找到了目标应用主application的包名,说明代码注入成功,则返回当前dex文件 70 | if (isApplicationClassFound) { 71 | return dexFileName; 72 | } 73 | } 74 | return ""; 75 | } 76 | 77 | private boolean dex2JarCmd(String dexPath, String jarOutputPath, String applicationName) { 78 | Dex2jarCmd cmd = new Dex2jarCmd(); 79 | String[] args = new String[]{ 80 | dexPath, 81 | "-o", 82 | jarOutputPath, 83 | "-app", 84 | applicationName, 85 | "--force" 86 | }; 87 | cmd.doMain(args); 88 | 89 | boolean isApplicationClassFounded = cmd.isApplicationClassFounded(); 90 | if (showAllLogs) { 91 | System.out.println("isApplicationClassFounded -> " + isApplicationClassFounded + "the dexPath is " + 92 | dexPath); 93 | } 94 | return isApplicationClassFounded; 95 | } 96 | 97 | private void jar2DexCmd(String jarFilePath, String dexOutPath) { 98 | Jar2Dex cmd = new Jar2Dex(); 99 | String[] args = new String[]{ 100 | jarFilePath, 101 | "-o", 102 | dexOutPath 103 | }; 104 | cmd.doMain(args); 105 | } 106 | 107 | // 列出目录下所有dex文件,classes.dex,classes2.dex,classes3.dex ..... 108 | private ArrayList createClassesDotDexFileList(int dexFileCount) { 109 | ArrayList list = new ArrayList<>(); 110 | for (int i = 0; i < dexFileCount; i++) { 111 | if (i == 0) { 112 | list.add("classes.dex"); 113 | } else { 114 | list.add("classes" + (i + 1) + ".dex"); 115 | } 116 | } 117 | return list; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/BuildAndSignApkTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.android.apksigner.ApkSignerTool; 4 | import com.storm.wind.xpatch.util.FileUtils; 5 | import com.storm.wind.xpatch.util.ShellCmdUtil; 6 | import java.io.File; 7 | import java.util.ArrayList; 8 | 9 | /** 10 | * Created by Wind 11 | */ 12 | public class BuildAndSignApkTask implements Runnable { 13 | 14 | private boolean keepUnsignedApkFile; 15 | 16 | private String signedApkPath; 17 | 18 | private String unzipApkFilePath; 19 | 20 | private String originalApkFilePath; 21 | 22 | public BuildAndSignApkTask(boolean keepUnsignedApkFile, String unzipApkFilePath, String signedApkPath, String originalApkFilePath) { 23 | this.keepUnsignedApkFile = keepUnsignedApkFile; 24 | this.unzipApkFilePath = unzipApkFilePath; 25 | this.signedApkPath = signedApkPath; 26 | this.originalApkFilePath = originalApkFilePath; 27 | } 28 | 29 | @Override 30 | public void run() { 31 | 32 | File unzipApkFile = new File(unzipApkFilePath); 33 | 34 | // 将文件压缩到当前apk文件的上一级目录上 35 | String unsignedApkPath = unzipApkFile.getParent() + File.separator + "unsigned.apk"; 36 | FileUtils.compressToZip(unzipApkFilePath, unsignedApkPath); 37 | 38 | // 将签名文件复制从assets目录下复制出来 39 | String keyStoreFilePath = unzipApkFile.getParent() + File.separator + "keystore"; 40 | 41 | File keyStoreFile = new File(keyStoreFilePath); 42 | // assets/keystore分隔符不能使用File.separator,否则在windows上抛出IOException !!! 43 | String keyStoreAssetPath; 44 | if (ShellCmdUtil.isAndroid()) { 45 | // BKS-V1 类型 46 | keyStoreAssetPath = "assets/android.keystore"; 47 | } else { 48 | // BKS 类型 49 | keyStoreAssetPath = "assets/keystore"; 50 | } 51 | 52 | FileUtils.copyFileFromJar(keyStoreAssetPath, keyStoreFilePath); 53 | 54 | String unsignedZipalignedApkPath = unzipApkFile.getParent() + File.separator + "unsigned_zipaligned.apk"; 55 | try { 56 | zipalignApk(unsignedApkPath, unsignedZipalignedApkPath); 57 | } catch (Exception e) { 58 | e.printStackTrace(); 59 | } 60 | 61 | String apkPath = unsignedZipalignedApkPath; 62 | if (!(new File(apkPath).exists())) { 63 | apkPath = unsignedApkPath; 64 | System.out.println(" zipalign apk failed, just sign not zipaligned apk !!!"); 65 | } 66 | 67 | boolean signResult = signApk(apkPath, keyStoreFilePath, signedApkPath); 68 | 69 | File unsignedApkFile = new File(unsignedApkPath); 70 | File signedApkFile = new File(signedApkPath); 71 | // delete unsigned apk file 72 | if (!keepUnsignedApkFile && unsignedApkFile.exists() && signedApkFile.exists() && signResult) { 73 | unsignedApkFile.delete(); 74 | } 75 | 76 | File unsign_zipaligned_file = new File(unsignedZipalignedApkPath); 77 | if (!keepUnsignedApkFile && unsign_zipaligned_file.exists() && signedApkFile.exists() && signResult) { 78 | unsign_zipaligned_file.delete(); 79 | } 80 | 81 | File idsigFile = new File(signedApkPath + ".idsig"); 82 | if (idsigFile.exists()) { 83 | idsigFile.delete(); 84 | } 85 | 86 | // delete the keystore file 87 | if (keyStoreFile.exists()) { 88 | keyStoreFile.delete(); 89 | } 90 | System.out.println(" out put apk :" + signedApkPath); 91 | } 92 | 93 | private boolean signApk(String apkPath, String keyStorePath, String signedApkPath) { 94 | String apkParentPath = (new File(apkPath)).getParent(); 95 | 96 | ShellCmdUtil.chmodNoException(apkParentPath, ShellCmdUtil.FileMode.MODE_755); 97 | if (signApkUsingAndroidApksigner(apkPath, keyStorePath, signedApkPath, "123456")) { 98 | return true; 99 | } 100 | if (ShellCmdUtil.isAndroid()) { 101 | System.out.println(" Sign apk failed, please sign it yourself."); 102 | return false; 103 | } 104 | try { 105 | long time = System.currentTimeMillis(); 106 | File keystoreFile = new File(keyStorePath); 107 | if (keystoreFile.exists()) { 108 | StringBuilder signCmd; 109 | signCmd = new StringBuilder("jarsigner "); 110 | signCmd.append(" -keystore ") 111 | .append(keyStorePath) 112 | .append(" -storepass ") 113 | .append("123456") 114 | .append(" -signedjar ") 115 | .append(" " + signedApkPath + " ") 116 | .append(" " + apkPath + " ") 117 | .append(" -digestalg SHA1 -sigalg SHA1withRSA ") 118 | .append(" key0 "); 119 | // System.out.println("\n" + signCmd + "\n"); 120 | String result = ShellCmdUtil.execCmd(signCmd.toString(), null); 121 | System.out.println(" sign apk time is :" + ((System.currentTimeMillis() - time) / 1000) + 122 | "s\n\n" + " result=" + result); 123 | return true; 124 | } 125 | System.out.println(" keystore not exist :" + keystoreFile.getAbsolutePath() + 126 | " please sign the apk by hand. \n"); 127 | return false; 128 | } catch (Throwable e) { 129 | System.out.println("use default jarsigner to sign apk failed, fail msg is :" + 130 | e.toString()); 131 | return false; 132 | } 133 | } 134 | 135 | // 使用Android build-tools里自带的apksigner工具进行签名 136 | private boolean signApkUsingAndroidApksigner(String apkPath, String keyStorePath, String signedApkPath, String keyStorePassword) { 137 | ArrayList commandList = new ArrayList<>(); 138 | 139 | commandList.add("sign"); 140 | commandList.add("--ks"); 141 | commandList.add(keyStorePath); 142 | commandList.add("--ks-key-alias"); 143 | commandList.add("key0"); 144 | commandList.add("--ks-pass"); 145 | commandList.add("pass:" + keyStorePassword); 146 | commandList.add("--key-pass"); 147 | commandList.add("pass:" + keyStorePassword); 148 | commandList.add("--out"); 149 | commandList.add(signedApkPath); 150 | commandList.add("--v1-signing-enabled"); 151 | commandList.add("true"); 152 | commandList.add("--v2-signing-enabled"); // v2签名不兼容android 6 153 | commandList.add("true"); 154 | commandList.add("--v3-signing-enabled"); // v3签名不兼容android 6 155 | commandList.add("true"); 156 | commandList.add(apkPath); 157 | 158 | int size = commandList.size(); 159 | String[] commandArray = new String[size]; 160 | commandArray = commandList.toArray(commandArray); 161 | 162 | try { 163 | ApkSignerTool.main(commandArray); 164 | } catch (Exception e) { 165 | e.printStackTrace(); 166 | return false; 167 | } 168 | return true; 169 | } 170 | 171 | private void zipalignApk(String inputApkPath, String outputApkPath) { 172 | long time = System.currentTimeMillis(); 173 | 174 | String os = System.getProperty("os.name"); 175 | String zipalignAssetPath = "assets/zipalign"; 176 | if (os.toLowerCase().startsWith("win")) { 177 | System.out.println(" The running os is " + os); 178 | zipalignAssetPath = "assets/win/zipalign.exe"; 179 | } 180 | 181 | String zipalignPath = (new File(inputApkPath)).getParent() + File.separator + "zipalign"; 182 | FileUtils.copyFileFromJar(zipalignAssetPath, zipalignPath); 183 | ShellCmdUtil.chmodNoException(zipalignPath, ShellCmdUtil.FileMode.MODE_755); 184 | StringBuilder signCmd = new StringBuilder(zipalignPath + " "); 185 | 186 | signCmd.append(" -f ") 187 | .append(" -p ") 188 | .append(" 4 ") 189 | .append(" " + inputApkPath + " ") 190 | .append(" " + outputApkPath + " "); 191 | System.out.println("\n" + signCmd + "\n"); 192 | String result = null; 193 | try { 194 | result = ShellCmdUtil.execCmd(signCmd.toString(), null); 195 | } catch (Exception e) { 196 | e.printStackTrace(); 197 | } 198 | File zipalignFile = new File(zipalignPath); 199 | if (zipalignFile.exists()) { 200 | zipalignFile.delete(); 201 | } 202 | System.out.println(" zipalign apk time is :" + ((System.currentTimeMillis() - time)) + 203 | "s\n\n" + " result=" + result); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/SaveApkSignatureTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.storm.wind.xpatch.util.ApkSignatureHelper; 4 | import com.storm.wind.xpatch.util.FileUtils; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * Created by Wind 10 | */ 11 | public class SaveApkSignatureTask implements Runnable { 12 | 13 | private String apkPath; 14 | private String dstFilePath; 15 | 16 | private final static String SIGNATURE_INFO_ASSET_PATH = "assets/xpatch_asset/original_signature_info.ini"; 17 | 18 | public SaveApkSignatureTask(String apkPath, String unzipApkFilePath) { 19 | this.apkPath = apkPath; 20 | this.dstFilePath = (unzipApkFilePath + SIGNATURE_INFO_ASSET_PATH).replace("/", File.separator); 21 | } 22 | 23 | @Override 24 | public void run() { 25 | // First, get the original signature 26 | String originalSignature = ApkSignatureHelper.getApkSignInfo(apkPath); 27 | if (originalSignature == null || originalSignature.isEmpty()) { 28 | System.out.println(" Get original signature failed !!!!"); 29 | return; 30 | } 31 | 32 | // Then, save the signature chars to the asset file 33 | File file = new File(dstFilePath); 34 | File fileParent = file.getParentFile(); 35 | if (!fileParent.exists()) { 36 | fileParent.mkdirs(); 37 | } 38 | 39 | FileUtils.writeFile(dstFilePath, originalSignature); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/SaveOriginalApkTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.storm.wind.xpatch.util.FileUtils; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Created by Windysha 9 | * This is used to copy original apk into the apk asset directory, so another xposed module can use it to by pass apk check. 10 | */ 11 | public class SaveOriginalApkTask implements Runnable { 12 | 13 | private final String apkPath; 14 | private String dstApkFilePath; 15 | private String dstXposedModulePath; 16 | 17 | private static final String ORIGINAL_APK_ASSET_PATH = "assets/xpatch_asset/original_apk/base.apk"; 18 | public static final String XPOSED_MODULE_ASSET_PATH = "assets/xpatch_asset/original_apk/xposedmodule.apk"; 19 | 20 | public SaveOriginalApkTask(String apkPath, String unzipApkFilePath) { 21 | this.apkPath = apkPath; 22 | this.dstApkFilePath = (unzipApkFilePath + ORIGINAL_APK_ASSET_PATH).replace("/", File.separator); 23 | this.dstXposedModulePath = (unzipApkFilePath + XPOSED_MODULE_ASSET_PATH).replace("/", File.separator); 24 | } 25 | 26 | @Override 27 | public void run() { 28 | ensureDstFileCreated(); 29 | FileUtils.copyFile(apkPath, dstApkFilePath); 30 | 31 | String moduleAssetPath = "assets/xposedmodule/hook_apk_path_module.apk"; 32 | FileUtils.copyFileFromJar(moduleAssetPath, dstXposedModulePath); 33 | } 34 | 35 | private void ensureDstFileCreated() { 36 | File dstParentFile = new File(dstApkFilePath); 37 | if (!dstParentFile.getParentFile().exists()) { 38 | dstParentFile.getParentFile().mkdirs(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/SaveOriginalApplicationNameTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.storm.wind.xpatch.util.FileUtils; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Created by xiawanli on 2019/4/6 9 | */ 10 | public class SaveOriginalApplicationNameTask implements Runnable { 11 | 12 | private final String applcationName; 13 | private final String unzipApkFilePath; 14 | private String dstFilePath; 15 | 16 | private final String APPLICATION_NAME_ASSET_PATH = "assets/xpatch_asset/original_application_name.ini"; 17 | 18 | public SaveOriginalApplicationNameTask(String applicationName, String unzipApkFilePath) { 19 | this.applcationName = applicationName; 20 | this.unzipApkFilePath = unzipApkFilePath; 21 | 22 | this.dstFilePath = (unzipApkFilePath + APPLICATION_NAME_ASSET_PATH).replace("/", File.separator); 23 | } 24 | 25 | @Override 26 | public void run() { 27 | ensureDstFileCreated(); 28 | FileUtils.writeFile(dstFilePath, applcationName); 29 | } 30 | 31 | private void ensureDstFileCreated() { 32 | File dstParentFile = new File(dstFilePath); 33 | if (!dstParentFile.getParentFile().getParentFile().exists()) { 34 | dstParentFile.getParentFile().getParentFile().mkdirs(); 35 | } 36 | if (!dstParentFile.getParentFile().exists()) { 37 | dstParentFile.getParentFile().mkdirs(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/task/SoAndDexCopyTask.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.task; 2 | 3 | import com.storm.wind.xpatch.util.FileUtils; 4 | 5 | import java.io.File; 6 | import java.util.HashMap; 7 | 8 | /** 9 | * Created by Wind 10 | */ 11 | public class SoAndDexCopyTask implements Runnable { 12 | 13 | private static final String SANDHOOK_SO_FILE_NAME = "libsandhook"; 14 | private static final String WHALE_SO_FILE_NAME = "libwhale"; 15 | 16 | private static final String SANDHOOK_SO_FILE_NAME_WITH_SUFFIX = "libsandhook.so"; 17 | private static final String WHALE_SO_FILE_NAME_WITH_SUFFIX = "libwhale.so"; 18 | 19 | private static final String XPOSED_MODULE_FILE_NAME_PREFIX = "libxpatch_xp_module_"; 20 | private static final String SO_FILE_SUFFIX = ".so"; 21 | 22 | private final String[] APK_LIB_PATH_ARRAY = { 23 | "lib/armeabi-v7a/", 24 | "lib/armeabi/", 25 | "lib/arm64-v8a/" 26 | }; 27 | 28 | private final HashMap mSoFilePathMap = new HashMap<>(); 29 | private int dexFileCount; 30 | private String unzipApkFilePath; 31 | private String[] xposedModuleArray; 32 | 33 | private boolean useWhaleHookFramework; 34 | 35 | public SoAndDexCopyTask(int dexFileCount, String unzipApkFilePath, 36 | String[] xposedModuleArray, boolean useWhaleHookFramework) { 37 | this.dexFileCount = dexFileCount; 38 | this.unzipApkFilePath = unzipApkFilePath; 39 | this.xposedModuleArray = xposedModuleArray; 40 | this.useWhaleHookFramework = useWhaleHookFramework; 41 | 42 | String soFileName; 43 | if (useWhaleHookFramework) { 44 | soFileName = WHALE_SO_FILE_NAME; 45 | } else { 46 | soFileName = SANDHOOK_SO_FILE_NAME; 47 | } 48 | 49 | mSoFilePathMap.put(APK_LIB_PATH_ARRAY[0], "assets/lib/armeabi-v7a/" + soFileName); 50 | mSoFilePathMap.put(APK_LIB_PATH_ARRAY[1], "assets/lib/armeabi-v7a/" + soFileName); 51 | mSoFilePathMap.put(APK_LIB_PATH_ARRAY[2], "assets/lib/arm64-v8a/" + soFileName); 52 | } 53 | 54 | @Override 55 | public void run() { 56 | // 复制xposed兼容层的dex文件以及so文件到当前目录下 57 | copySoFile(); 58 | copyDexFile(dexFileCount); 59 | 60 | // 删除签名信息 61 | deleteMetaInfo(); 62 | } 63 | 64 | private void copySoFile() { 65 | String[] existLibPathArray = new String[3]; 66 | int arrayIndex = 0; 67 | for (String libPath : APK_LIB_PATH_ARRAY) { 68 | String apkSoFullPath = fullLibPath(libPath); 69 | File apkSoFullPathFile = new File(apkSoFullPath); 70 | if (apkSoFullPathFile.exists()) { 71 | existLibPathArray[arrayIndex] = libPath; 72 | arrayIndex++; 73 | } 74 | } 75 | 76 | // 不存在lib目录,则创建lib/armeabi-v7 文件夹 77 | if (arrayIndex == 0) { 78 | String libPath = APK_LIB_PATH_ARRAY[0]; 79 | String apkSoFullPath = fullLibPath(libPath); 80 | File apkSoFullPathFile = new File(apkSoFullPath); 81 | apkSoFullPathFile.mkdirs(); 82 | existLibPathArray[arrayIndex] = libPath; 83 | } 84 | 85 | for (String libPath : existLibPathArray) { 86 | if (libPath != null && !libPath.isEmpty()) { 87 | String apkSoFullPath = fullLibPath(libPath); 88 | copyLibFile(apkSoFullPath, mSoFilePathMap.get(libPath)); 89 | } 90 | } 91 | 92 | // copy xposed modules into the lib path 93 | if (xposedModuleArray != null && xposedModuleArray.length > 0) { 94 | int index = 0; 95 | for (String modulePath : xposedModuleArray) { 96 | modulePath = modulePath.trim(); 97 | if (modulePath == null || modulePath.length() == 0) { 98 | continue; 99 | } 100 | File moduleFile = new File(modulePath); 101 | if (!moduleFile.exists()) { 102 | continue; 103 | } 104 | for (String libPath : existLibPathArray) { 105 | if (libPath != null && !libPath.isEmpty()) { 106 | String apkSoFullPath = fullLibPath(libPath); 107 | String outputModuleName = XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX; 108 | File outputModuleSoFile = new File(apkSoFullPath, outputModuleName); 109 | FileUtils.copyFile(moduleFile, outputModuleSoFile); 110 | } 111 | } 112 | index++; 113 | } 114 | } 115 | } 116 | 117 | private void copyDexFile(int dexFileCount) { 118 | // copy dex file to root dir, rename it first 119 | String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex"; 120 | // assets/classes.dex分隔符不能使用File.seperater,否则在windows上无法读取到文件,IOException 121 | String dexAssetPath; 122 | if (useWhaleHookFramework) { 123 | dexAssetPath = "assets/dex/whale/classes-1.0.dex"; 124 | } else { 125 | dexAssetPath = "assets/dex/sandhook/classes.dex"; 126 | } 127 | FileUtils.copyFileFromJar(dexAssetPath, unzipApkFilePath + copiedDexFileName); 128 | } 129 | 130 | private String fullLibPath(String libPath) { 131 | return unzipApkFilePath + libPath.replace("/", File.separator); 132 | } 133 | 134 | private void copyLibFile(String libFilePath, String srcSoPath) { 135 | File apkSoParentFile = new File(libFilePath); 136 | if (!apkSoParentFile.exists()) { 137 | apkSoParentFile.mkdirs(); 138 | } 139 | 140 | // get the file name first 141 | // int lastIndex = srcSoPath.lastIndexOf('/'); 142 | // int length = srcSoPath.length(); 143 | String soFileName; 144 | if (useWhaleHookFramework) { 145 | soFileName = WHALE_SO_FILE_NAME_WITH_SUFFIX; 146 | } else { 147 | soFileName = SANDHOOK_SO_FILE_NAME_WITH_SUFFIX; 148 | } 149 | 150 | // do copy 151 | FileUtils.copyFileFromJar(srcSoPath, new File(apkSoParentFile, soFileName).getAbsolutePath()); 152 | } 153 | 154 | 155 | private void deleteMetaInfo() { 156 | String metaInfoFilePath = "META-INF"; 157 | File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath); 158 | if (!metaInfoFileRoot.exists()) { 159 | return; 160 | } 161 | File[] childFileList = metaInfoFileRoot.listFiles(); 162 | if (childFileList == null || childFileList.length == 0) { 163 | return; 164 | } 165 | for (File file : childFileList) { 166 | String fileName = file.getName().toUpperCase(); 167 | if (fileName.endsWith(".MF") || fileName.endsWith(".RAS") || fileName.endsWith(".SF")) { 168 | file.delete(); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/util/ApkSignatureHelper.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.util; 2 | 3 | import java.io.InputStream; 4 | import java.security.cert.Certificate; 5 | import java.util.Enumeration; 6 | import java.util.jar.JarEntry; 7 | import java.util.jar.JarFile; 8 | 9 | /** 10 | * Created by Wind 11 | */ 12 | public class ApkSignatureHelper { 13 | 14 | private static char[] toChars(byte[] mSignature) { 15 | byte[] sig = mSignature; 16 | final int N = sig.length; 17 | final int N2 = N * 2; 18 | char[] text = new char[N2]; 19 | for (int j = 0; j < N; j++) { 20 | byte v = sig[j]; 21 | int d = (v >> 4) & 0xf; 22 | text[j * 2] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d)); 23 | d = v & 0xf; 24 | text[j * 2 + 1] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d)); 25 | } 26 | return text; 27 | } 28 | 29 | private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) { 30 | try { 31 | InputStream is = jarFile.getInputStream(je); 32 | while (is.read(readBuffer, 0, readBuffer.length) != -1) { 33 | } 34 | is.close(); 35 | return (Certificate[]) (je != null ? je.getCertificates() : null); 36 | } catch (Exception e) { 37 | } 38 | return null; 39 | } 40 | 41 | public static String getApkSignInfo(String apkFilePath) { 42 | byte[] readBuffer = new byte[8192]; 43 | Certificate[] certs = null; 44 | try { 45 | JarFile jarFile = new JarFile(apkFilePath); 46 | Enumeration entries = jarFile.entries(); 47 | while (entries.hasMoreElements()) { 48 | JarEntry je = (JarEntry) entries.nextElement(); 49 | if (je.isDirectory()) { 50 | continue; 51 | } 52 | if (je.getName().startsWith("META-INF/")) { 53 | continue; 54 | } 55 | Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer); 56 | if (certs == null) { 57 | certs = localCerts; 58 | } else { 59 | for (int i = 0; i < certs.length; i++) { 60 | boolean found = false; 61 | for (int j = 0; j < localCerts.length; j++) { 62 | if (certs[i] != null && certs[i].equals(localCerts[j])) { 63 | found = true; 64 | break; 65 | } 66 | } 67 | if (!found || certs.length != localCerts.length) { 68 | jarFile.close(); 69 | return null; 70 | } 71 | } 72 | } 73 | } 74 | jarFile.close(); 75 | System.out.println(" getApkSignInfo result --> " + certs[0]); 76 | return new String(toChars(certs[0].getEncoded())); 77 | } catch (Exception e) { 78 | e.printStackTrace(); 79 | } 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/util/ManifestParser.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | 7 | import wind.android.content.res.AXmlResourceParser; 8 | import wind.v1.XmlPullParser; 9 | import wind.v1.XmlPullParserException; 10 | 11 | /** 12 | * Created by Wind 13 | */ 14 | public class ManifestParser { 15 | 16 | /** 17 | * Get the package name and the main application name from the manifest file 18 | * */ 19 | public static Pair parseManifestFile(String filePath) { 20 | AXmlResourceParser parser = new AXmlResourceParser(); 21 | File file = new File(filePath); 22 | String packageName = null; 23 | String applicationName = null; 24 | if (!file.exists()) { 25 | System.out.println(" manifest file not exist!!! filePath -> " + filePath); 26 | return null; 27 | } 28 | FileInputStream inputStream = null; 29 | try { 30 | inputStream = new FileInputStream(file); 31 | 32 | parser.open(inputStream); 33 | 34 | while (true) { 35 | int type = parser.next(); 36 | if (type == XmlPullParser.END_DOCUMENT) { 37 | break; 38 | } 39 | if (type == XmlPullParser.START_TAG) { 40 | int attrCount = parser.getAttributeCount(); 41 | for (int i = 0; i < attrCount; i++) { 42 | String attrName = parser.getAttributeName(i); 43 | int attrNameRes = parser.getAttributeNameResource(i); 44 | 45 | String name = parser.getName(); 46 | 47 | if ("manifest".equals(name)) { 48 | if ("package".equals(attrName)) { 49 | packageName = parser.getAttributeValue(i); 50 | } 51 | } 52 | 53 | if ("application".equals(name)) { 54 | if ("name".equals(attrName) || attrNameRes == 0x01010003) { 55 | applicationName = parser.getAttributeValue(i); 56 | } 57 | } 58 | 59 | if (packageName != null && packageName.length() > 0 && applicationName != null && applicationName.length() > 0) { 60 | return new Pair(packageName, applicationName); 61 | } 62 | } 63 | } else if (type == XmlPullParser.END_TAG) { 64 | // ignored 65 | } 66 | } 67 | } catch (XmlPullParserException | IOException e) { 68 | e.printStackTrace(); 69 | System.out.println("parseManifestFile failed, reason --> " + e.getMessage()); 70 | } finally { 71 | if (inputStream != null) { 72 | try { 73 | inputStream.close(); 74 | } catch (IOException e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | } 79 | return new Pair(packageName, applicationName); 80 | } 81 | 82 | public static class Pair { 83 | public String packageName; 84 | public String applicationName; 85 | 86 | public Pair(String packageName, String applicationName) { 87 | this.packageName = packageName; 88 | this.applicationName = applicationName; 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/util/ReflectUtils.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.util; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | 7 | public class ReflectUtils { 8 | 9 | //获取类的实例的变量的值 10 | public static Object getField(Object receiver, String fieldName) { 11 | return getField(null, receiver, fieldName); 12 | } 13 | 14 | //获取类的静态变量的值 15 | public static Object getField(String className, String fieldName) { 16 | return getField(className, null, fieldName); 17 | } 18 | 19 | public static Object getField(Class clazz, String className, String fieldName, Object receiver) { 20 | try { 21 | if (clazz == null) { 22 | clazz = Class.forName(className); 23 | } 24 | Field field = clazz.getDeclaredField(fieldName); 25 | if (field == null) { 26 | return null; 27 | } 28 | field.setAccessible(true); 29 | return field.get(receiver); 30 | } catch (Throwable e) { 31 | e.printStackTrace(); 32 | } 33 | return null; 34 | } 35 | 36 | private static Object getField(String className, Object receiver, String fieldName) { 37 | Class clazz = null; 38 | Field field; 39 | if (className != null && className.length() > 0) { 40 | try { 41 | clazz = Class.forName(className); 42 | } catch (ClassNotFoundException e) { 43 | e.printStackTrace(); 44 | } 45 | } else { 46 | if (receiver != null) { 47 | clazz = receiver.getClass(); 48 | } 49 | } 50 | if (clazz == null) { 51 | return null; 52 | } 53 | 54 | try { 55 | field = findField(clazz, fieldName); 56 | if (field == null) { 57 | return null; 58 | } 59 | field.setAccessible(true); 60 | return field.get(receiver); 61 | } catch (IllegalAccessException e) { 62 | e.printStackTrace(); 63 | } catch (IllegalArgumentException e) { 64 | e.printStackTrace(); 65 | } catch (NullPointerException e) { 66 | e.printStackTrace(); 67 | } 68 | return null; 69 | } 70 | 71 | public static Object setField(Object receiver, String fieldName, Object value) { 72 | try { 73 | Field field; 74 | field = findField(receiver.getClass(), fieldName); 75 | if (field == null) { 76 | return null; 77 | } 78 | field.setAccessible(true); 79 | Object old = field.get(receiver); 80 | field.set(receiver, value); 81 | return old; 82 | } catch (IllegalAccessException e) { 83 | e.printStackTrace(); 84 | } catch (IllegalArgumentException e) { 85 | e.printStackTrace(); 86 | } 87 | return null; 88 | } 89 | 90 | public static Object setField(Class clazz, Object receiver, String fieldName, Object value) { 91 | try { 92 | Field field; 93 | field = findField(clazz, fieldName); 94 | if (field == null) { 95 | return null; 96 | } 97 | field.setAccessible(true); 98 | Object old = field.get(receiver); 99 | field.set(receiver, value); 100 | return old; 101 | } catch (IllegalAccessException e) { 102 | e.printStackTrace(); 103 | } catch (IllegalArgumentException e) { 104 | e.printStackTrace(); 105 | } 106 | return null; 107 | } 108 | 109 | public static Object callMethod(Object receiver, String methodName, Object... params) { 110 | return callMethod(null, receiver, methodName, params); 111 | } 112 | 113 | public static Object setField(String clazzName, Object receiver, String fieldName, Object value) { 114 | try { 115 | Class clazz = Class.forName(clazzName); 116 | Field field; 117 | field = findField(clazz, fieldName); 118 | if (field == null) { 119 | return null; 120 | } 121 | field.setAccessible(true); 122 | Object old = field.get(receiver); 123 | field.set(receiver, value); 124 | return old; 125 | } catch (IllegalAccessException e) { 126 | e.printStackTrace(); 127 | } catch (IllegalArgumentException e) { 128 | e.printStackTrace(); 129 | } catch (ClassNotFoundException e) { 130 | e.printStackTrace(); 131 | } 132 | return null; 133 | } 134 | 135 | 136 | public static Object callMethod(String className, String methodName, Object... params) { 137 | return callMethod(className, null, methodName, params); 138 | } 139 | 140 | public static Object callMethod(Class clazz, String className, String methodName, Object receiver, 141 | Class[] types, Object... params) { 142 | try { 143 | if (clazz == null) { 144 | clazz = Class.forName(className); 145 | } 146 | Method method = clazz.getDeclaredMethod(methodName, types); 147 | method.setAccessible(true); 148 | return method.invoke(receiver, params); 149 | } catch (Throwable throwable) { 150 | throwable.printStackTrace(); 151 | } 152 | return null; 153 | } 154 | 155 | private static Object callMethod(String className, Object receiver, String methodName, Object... params) { 156 | Class clazz = null; 157 | if (className != null && className.length() > 0) { 158 | try { 159 | clazz = Class.forName(className); 160 | } catch (ClassNotFoundException e) { 161 | e.printStackTrace(); 162 | } 163 | } else { 164 | if (receiver != null) { 165 | clazz = receiver.getClass(); 166 | } 167 | } 168 | if (clazz == null) { 169 | return null; 170 | } 171 | try { 172 | Method method = findMethod(clazz, methodName, params); 173 | if (method == null) { 174 | return null; 175 | } 176 | method.setAccessible(true); 177 | return method.invoke(receiver, params); 178 | } catch (IllegalArgumentException e) { 179 | e.printStackTrace(); 180 | } catch (IllegalAccessException e) { 181 | e.printStackTrace(); 182 | } catch (InvocationTargetException e) { 183 | e.printStackTrace(); 184 | } 185 | return null; 186 | } 187 | 188 | private static Method findMethod(Class clazz, String name, Object... arg) { 189 | Method[] methods = clazz.getMethods(); 190 | Method method = null; 191 | for (Method m : methods) { 192 | if (methodFitParam(m, name, arg)) { 193 | method = m; 194 | break; 195 | } 196 | } 197 | 198 | if (method == null) { 199 | method = findDeclaredMethod(clazz, name, arg); 200 | } 201 | return method; 202 | } 203 | 204 | private static Method findDeclaredMethod(Class clazz, String name, Object... arg) { 205 | Method[] methods = clazz.getDeclaredMethods(); 206 | Method method = null; 207 | for (Method m : methods) { 208 | if (methodFitParam(m, name, arg)) { 209 | method = m; 210 | break; 211 | } 212 | } 213 | 214 | if (method == null) { 215 | if (clazz.equals(Object.class)) { 216 | return null; 217 | } 218 | return findDeclaredMethod(clazz.getSuperclass(), name, arg); 219 | } 220 | return method; 221 | } 222 | 223 | private static boolean methodFitParam(Method method, String methodName, Object... arg) { 224 | if (!methodName.equals(method.getName())) { 225 | return false; 226 | } 227 | 228 | Class[] paramTypes = method.getParameterTypes(); 229 | if (arg == null || arg.length == 0) { 230 | return paramTypes == null || paramTypes.length == 0; 231 | } 232 | if (paramTypes.length != arg.length) { 233 | return false; 234 | } 235 | 236 | for (int i = 0; i < arg.length; ++i) { 237 | Object ar = arg[i]; 238 | Class paramT = paramTypes[i]; 239 | if (ar == null) { 240 | continue; 241 | } 242 | 243 | //TODO for primitive type 244 | if (paramT.isPrimitive()) { 245 | continue; 246 | } 247 | 248 | if (!paramT.isInstance(ar)) { 249 | return false; 250 | } 251 | } 252 | return true; 253 | } 254 | 255 | private static Field findField(Class clazz, String name) { 256 | try { 257 | return clazz.getDeclaredField(name); 258 | } catch (NoSuchFieldException e) { 259 | if (clazz.equals(Object.class)) { 260 | e.printStackTrace(); 261 | return null; 262 | } 263 | Class base = clazz.getSuperclass(); 264 | return findField(base, name); 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /xpatch/src/main/java/com/storm/wind/xpatch/util/ShellCmdUtil.java: -------------------------------------------------------------------------------- 1 | package com.storm.wind.xpatch.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.Closeable; 5 | import java.io.File; 6 | import java.io.InputStreamReader; 7 | 8 | /** 9 | * Created by Wind 10 | */ 11 | public class ShellCmdUtil { 12 | 13 | /** 14 | * 执行系统命令, 返回执行结果 15 | * 16 | * @param cmd 需要执行的命令 17 | * @param dir 执行命令的子进程的工作目录, null 表示和当前主进程工作目录相同 18 | */ 19 | public static String execCmd(String cmd, File dir) throws Exception { 20 | StringBuilder result = new StringBuilder(); 21 | 22 | Process process = null; 23 | BufferedReader bufrIn = null; 24 | BufferedReader bufrError = null; 25 | 26 | try { 27 | // 执行命令, 返回一个子进程对象(命令在子进程中执行) 28 | process = Runtime.getRuntime().exec(cmd, null, dir); 29 | 30 | // 方法阻塞, 等待命令执行完成(成功会返回0) 31 | process.waitFor(); 32 | 33 | // 获取命令执行结果, 有两个结果: 正常的输出 和 错误的输出(PS: 子进程的输出就是主进程的输入) 34 | bufrIn = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); 35 | bufrError = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); 36 | 37 | // 读取输出 38 | String line = null; 39 | while ((line = bufrIn.readLine()) != null) { 40 | result.append(line).append('\n'); 41 | } 42 | while ((line = bufrError.readLine()) != null) { 43 | result.append(line).append('\n'); 44 | } 45 | 46 | } finally { 47 | close(bufrIn); 48 | close(bufrError); 49 | 50 | // 销毁子进程 51 | if (process != null) { 52 | process.destroy(); 53 | } 54 | } 55 | 56 | // 返回执行结果 57 | return result.toString(); 58 | } 59 | 60 | public static void chmodNoException(String path, int mode) { 61 | try { 62 | chmod(path, mode); 63 | } catch (Exception e) { 64 | e.printStackTrace(); 65 | System.err.println("chmod exception path --> " + path + " exception -->" + e.getMessage()); 66 | } 67 | } 68 | 69 | public static void chmod(String path, int mode) throws Exception { 70 | if (isAndroid()) { 71 | chmodOnAndroid(path, mode); 72 | } 73 | 74 | File file = new File(path); 75 | String cmd = "chmod "; 76 | if (file.isDirectory()) { 77 | cmd += " -R "; 78 | } 79 | String cmode = String.format("%o", mode); 80 | Runtime.getRuntime().exec(cmd + cmode + " " + path).waitFor(); 81 | } 82 | 83 | private static void chmodOnAndroid(String path, int mode) { 84 | Object sdk_int = ReflectUtils.getField("android.os.Build$VERSION", "SDK_INT"); 85 | if (!(sdk_int instanceof Integer)) { 86 | return; 87 | } 88 | if ((int)sdk_int >= 21) { 89 | System.out.println("chmod on android is called, path = " + path); 90 | ReflectUtils.callMethod("android.system.Os", "chmod", path, mode); 91 | } 92 | } 93 | 94 | private static void close(Closeable stream) { 95 | if (stream != null) { 96 | try { 97 | stream.close(); 98 | } catch (Exception e) { 99 | // nothing 100 | } 101 | } 102 | } 103 | 104 | public static boolean isAndroid() { 105 | boolean isAndroid = true; 106 | try { 107 | Class.forName("android.content.Context"); 108 | } catch (ClassNotFoundException e) { 109 | isAndroid = false; 110 | } 111 | return isAndroid; 112 | } 113 | 114 | public interface FileMode { 115 | int MODE_ISUID = 04000; 116 | int MODE_ISGID = 02000; 117 | int MODE_ISVTX = 01000; 118 | int MODE_IRUSR = 00400; 119 | int MODE_IWUSR = 00200; 120 | int MODE_IXUSR = 00100; 121 | int MODE_IRGRP = 00040; 122 | int MODE_IWGRP = 00020; 123 | int MODE_IXGRP = 00010; 124 | int MODE_IROTH = 00004; 125 | int MODE_IWOTH = 00002; 126 | int MODE_IXOTH = 00001; 127 | 128 | int MODE_755 = MODE_IRUSR | MODE_IWUSR | MODE_IXUSR 129 | | MODE_IRGRP | MODE_IXGRP 130 | | MODE_IROTH | MODE_IXOTH; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/content/res/ChunkUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.content.res; 17 | 18 | import java.io.IOException; 19 | 20 | /** 21 | * @author Dmitry Skiba 22 | * 23 | */ 24 | class ChunkUtil { 25 | 26 | public static final void readCheckType(IntReader reader, int expectedType) throws IOException { 27 | int type=reader.readInt(); 28 | if (type!=expectedType) { 29 | throw new IOException( 30 | "Expected chunk of type 0x"+Integer.toHexString(expectedType)+ 31 | ", read 0x"+Integer.toHexString(type)+"."); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/content/res/IntReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.content.res; 17 | 18 | import java.io.DataInputStream; 19 | import java.io.EOFException; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | 23 | /** 24 | * @author Dmitry Skiba 25 | * 26 | * Simple helper class that allows reading of integers. 27 | * 28 | * TODO: 29 | * * implement buffering 30 | * 31 | */ 32 | public final class IntReader { 33 | 34 | public IntReader() { 35 | } 36 | public IntReader(InputStream stream,boolean bigEndian) { 37 | reset(stream,bigEndian); 38 | } 39 | 40 | public final void reset(InputStream stream,boolean bigEndian) { 41 | m_stream=stream; 42 | m_bigEndian=bigEndian; 43 | m_position=0; 44 | } 45 | 46 | public final void close() { 47 | if (m_stream==null) { 48 | return; 49 | } 50 | try { 51 | m_stream.close(); 52 | } 53 | catch (IOException e) { 54 | } 55 | reset(null,false); 56 | } 57 | 58 | public final InputStream getStream() { 59 | return m_stream; 60 | } 61 | 62 | public final boolean isBigEndian() { 63 | return m_bigEndian; 64 | } 65 | public final void setBigEndian(boolean bigEndian) { 66 | m_bigEndian=bigEndian; 67 | } 68 | 69 | public final int readByte() throws IOException { 70 | return readInt(1); 71 | } 72 | public final int readShort() throws IOException { 73 | return readInt(2); 74 | } 75 | public final int readInt() throws IOException { 76 | return readInt(4); 77 | } 78 | public final void readFully(byte[] b) throws IOException { 79 | new DataInputStream(m_stream).readFully(b); 80 | } 81 | public final int readInt(int length) throws IOException { 82 | if (length<0 || length>4) { 83 | throw new IllegalArgumentException(); 84 | } 85 | int result=0; 86 | if (m_bigEndian) { 87 | for (int i=(length-1)*8;i>=0;i-=8) { 88 | int b=m_stream.read(); 89 | if (b==-1) { 90 | throw new EOFException(); 91 | } 92 | m_position+=1; 93 | result|=(b<0;length-=1) { 117 | array[offset++]=readInt(); 118 | } 119 | } 120 | 121 | public final byte[] readByteArray(int length) throws IOException { 122 | byte[] array=new byte[length]; 123 | int read=m_stream.read(array); 124 | m_position+=read; 125 | if (read!=length) { 126 | throw new EOFException(); 127 | } 128 | return array; 129 | } 130 | 131 | public final void skip(int bytes) throws IOException { 132 | if (bytes<=0) { 133 | return; 134 | } 135 | long skipped=m_stream.skip(bytes); 136 | m_position+=skipped; 137 | if (skipped!=bytes) { 138 | throw new EOFException(); 139 | } 140 | } 141 | 142 | public final void skipInt() throws IOException { 143 | skip(4); 144 | } 145 | 146 | public final int available() throws IOException { 147 | return m_stream.available(); 148 | } 149 | 150 | public final int getPosition() { 151 | return m_position; 152 | } 153 | 154 | /////////////////////////////////// data 155 | 156 | private InputStream m_stream; 157 | private boolean m_bigEndian; 158 | private int m_position; 159 | } 160 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/content/res/StringBlock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.content.res; 17 | 18 | import java.io.IOException; 19 | import java.nio.ByteBuffer; 20 | import java.nio.charset.CharacterCodingException; 21 | import java.nio.charset.Charset; 22 | import java.nio.charset.CharsetDecoder; 23 | 24 | /** 25 | * @author Dmitry Skiba 26 | * 27 | * Block of strings, used in binary xml and arsc. 28 | * 29 | * TODO: 30 | * - implement get() 31 | * 32 | */ 33 | public class StringBlock { 34 | 35 | private int[] m_stringOffsets; 36 | private byte[] m_strings; 37 | private int[] m_styleOffsets; 38 | private int[] m_styles; 39 | private boolean m_isUTF8; 40 | private static final int CHUNK_TYPE = 0x001C0001; 41 | private static final int UTF8_FLAG = 0x00000100; 42 | 43 | private final CharsetDecoder UTF8_DECODER = Charset.forName("UTF-8").newDecoder(); 44 | private final CharsetDecoder UTF16LE_DECODER = Charset.forName("UTF-16LE").newDecoder(); 45 | 46 | /** 47 | * Reads whole (including chunk type) string block from stream. 48 | * Stream must be at the chunk type. 49 | */ 50 | public static StringBlock read(IntReader reader) throws IOException { 51 | ChunkUtil.readCheckType(reader, CHUNK_TYPE); 52 | int chunkSize = reader.readInt(); 53 | int stringCount = reader.readInt(); 54 | int styleOffsetCount = reader.readInt(); 55 | int flags = reader.readInt(); 56 | int stringsOffset = reader.readInt(); 57 | int stylesOffset = reader.readInt(); 58 | 59 | StringBlock block = new StringBlock(); 60 | block.m_isUTF8 = (flags & UTF8_FLAG) != 0; 61 | block.m_stringOffsets = reader.readIntArray(stringCount); 62 | if (styleOffsetCount != 0) { 63 | block.m_styleOffsets = reader.readIntArray(styleOffsetCount); 64 | } 65 | { 66 | int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset; 67 | block.m_strings = new byte[size]; 68 | reader.readFully(block.m_strings); 69 | } 70 | if (stylesOffset != 0) { 71 | int size = (chunkSize - stylesOffset); 72 | if ((size % 4) != 0) { 73 | throw new IOException("Style data size is not multiple of 4 (" + size + ")."); 74 | } 75 | block.m_styles = reader.readIntArray(size / 4); 76 | } 77 | 78 | return block; 79 | } 80 | 81 | /** 82 | * Returns number of strings in block. 83 | */ 84 | public int getCount() { 85 | return m_stringOffsets != null ? 86 | m_stringOffsets.length : 87 | 0; 88 | } 89 | 90 | 91 | public String getString(int index) { 92 | if (index < 0 || m_stringOffsets == null || index >= m_stringOffsets.length) { 93 | return null; 94 | } 95 | int offset = m_stringOffsets[index]; 96 | int length; 97 | if (m_isUTF8) { 98 | int[] val = getUtf8(m_strings, offset); 99 | offset = val[0]; 100 | length = val[1]; 101 | } else { 102 | int[] val = getUtf16(m_strings, offset); 103 | offset += val[0]; 104 | length = val[1]; 105 | } 106 | return decodeString(offset, length); 107 | } 108 | 109 | private String decodeString(int offset, int length) { 110 | try { 111 | return (m_isUTF8 ? UTF8_DECODER : UTF16LE_DECODER).decode( 112 | ByteBuffer.wrap(m_strings, offset, length)).toString(); 113 | } catch (CharacterCodingException e) { 114 | return null; 115 | } 116 | } 117 | 118 | private static final int getShort(byte[] array, int offset) { 119 | return (array[offset + 1] & 0xff) << 8 | array[offset] & 0xff; 120 | } 121 | 122 | private static final int[] getUtf8(byte[] array, int offset) { 123 | int val = array[offset]; 124 | int length; 125 | if ((val & 0x80) != 0) { 126 | offset += 2; 127 | } else { 128 | offset += 1; 129 | } 130 | val = array[offset]; 131 | if ((val & 0x80) != 0) { 132 | offset += 2; 133 | } else { 134 | offset += 1; 135 | } 136 | length = 0; 137 | while (array[offset + length] != 0) { 138 | length++; 139 | } 140 | return new int[]{offset, length}; 141 | } 142 | 143 | private static final int[] getUtf16(byte[] array, int offset) { 144 | int val = (array[offset + 1] & 0xff) << 8 | array[offset] & 0xff; 145 | if (val == 0x8000) { 146 | int heigh = (array[offset + 3] & 0xFF) << 8; 147 | int low = (array[offset + 2] & 0xFF); 148 | return new int[]{4, (heigh + low) * 2}; 149 | } 150 | return new int[]{2, val * 2}; 151 | } 152 | 153 | 154 | 155 | 156 | /** 157 | * Not yet implemented. 158 | *

159 | * Returns string with style information (if any). 160 | */ 161 | public CharSequence get(int index) { 162 | return getString(index); 163 | } 164 | 165 | /** 166 | * Returns string with style tags (html-like). 167 | */ 168 | public String getHTML(int index) { 169 | String raw=getString(index); 170 | if (raw==null) { 171 | return raw; 172 | } 173 | int[] style=getStyle(index); 174 | if (style==null) { 175 | return raw; 176 | } 177 | StringBuilder html=new StringBuilder(raw.length()+32); 178 | int offset=0; 179 | while (true) { 180 | int i=-1; 181 | for (int j=0;j!=style.length;j+=3) { 182 | if (style[j+1]==-1) { 183 | continue; 184 | } 185 | if (i==-1 || style[i+1]>style[j+1]) { 186 | i=j; 187 | } 188 | } 189 | int start=((i!=-1)?style[i+1]:raw.length()); 190 | for (int j=0;j!=style.length;j+=3) { 191 | int end=style[j+2]; 192 | if (end==-1 || end>=start) { 193 | continue; 194 | } 195 | if (offset<=end) { 196 | html.append(raw,offset,end+1); 197 | offset=end+1; 198 | } 199 | style[j+2]=-1; 200 | html.append('<'); 201 | html.append('/'); 202 | html.append(getString(style[j])); 203 | html.append('>'); 204 | } 205 | if (offset'); 215 | style[i+1]=-1; 216 | } 217 | return html.toString(); 218 | } 219 | 220 | /** 221 | * Finds index of the string. 222 | * Returns -1 if the string was not found. 223 | */ 224 | public int find(String string) { 225 | if (string==null) { 226 | return -1; 227 | } 228 | for (int i=0;i!=m_stringOffsets.length;++i) { 229 | int offset=m_stringOffsets[i]; 230 | int length=getShort(m_strings,offset); 231 | if (length!=string.length()) { 232 | continue; 233 | } 234 | int j=0; 235 | for (;j!=length;++j) { 236 | offset+=2; 237 | if (string.charAt(j)!=getShort(m_strings,offset)) { 238 | break; 239 | } 240 | } 241 | if (j==length) { 242 | return i; 243 | } 244 | } 245 | return -1; 246 | } 247 | 248 | ///////////////////////////////////////////// implementation 249 | 250 | private StringBlock() { 251 | } 252 | 253 | /** 254 | * Returns style information - array of int triplets, 255 | * where in each triplet: 256 | * * first int is index of tag name ('b','i', etc.) 257 | * * second int is tag start index in string 258 | * * third int is tag end index in string 259 | */ 260 | private int[] getStyle(int index) { 261 | if (m_styleOffsets==null || m_styles==null || 262 | index>=m_styleOffsets.length) 263 | { 264 | return null; 265 | } 266 | int offset=m_styleOffsets[index]/4; 267 | int style[]; 268 | { 269 | int count=0; 270 | for (int i=offset;i>> 16); 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/content/res/XmlResourceParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.content.res; 17 | 18 | import wind.android.util.AttributeSet; 19 | import wind.v1.XmlPullParser; 20 | 21 | /** 22 | * @author Dmitry Skiba 23 | * 24 | */ 25 | public interface XmlResourceParser extends XmlPullParser, AttributeSet { 26 | void close(); 27 | } 28 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/util/AttributeSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.util; 17 | 18 | /** 19 | * @author Dmitry Skiba 20 | * 21 | */ 22 | public interface AttributeSet { 23 | int getAttributeCount(); 24 | String getAttributeName(int index); 25 | String getAttributeValue(int index); 26 | String getPositionDescription(); 27 | int getAttributeNameResource(int index); 28 | int getAttributeListValue(int index, String options[], int defaultValue); 29 | boolean getAttributeBooleanValue(int index, boolean defaultValue); 30 | int getAttributeResourceValue(int index, int defaultValue); 31 | int getAttributeIntValue(int index, int defaultValue); 32 | int getAttributeUnsignedIntValue(int index, int defaultValue); 33 | float getAttributeFloatValue(int index, float defaultValue); 34 | String getIdAttribute(); 35 | String getClassAttribute(); 36 | int getIdAttributeResourceValue(int index); 37 | int getStyleAttribute(); 38 | String getAttributeValue(String namespace, String attribute); 39 | int getAttributeListValue(String namespace, String attribute, String options[], int defaultValue); 40 | boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue); 41 | int getAttributeResourceValue(String namespace, String attribute, int defaultValue); 42 | int getAttributeIntValue(String namespace, String attribute, int defaultValue); 43 | int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue); 44 | float getAttributeFloatValue(String namespace, String attribute, float defaultValue); 45 | 46 | //TODO: remove 47 | int getAttributeValueType(int index); 48 | int getAttributeValueData(int index); 49 | } 50 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/android/util/TypedValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.android.util; 17 | 18 | /** 19 | * @author Dmitry Skiba 20 | * 21 | */ 22 | public class TypedValue { 23 | 24 | public int type; 25 | public CharSequence string; 26 | public int data; 27 | public int assetCookie; 28 | public int resourceId; 29 | public int changingConfigurations; 30 | 31 | public static final int 32 | TYPE_NULL =0, 33 | TYPE_REFERENCE =1, 34 | TYPE_ATTRIBUTE =2, 35 | TYPE_STRING =3, 36 | TYPE_FLOAT =4, 37 | TYPE_DIMENSION =5, 38 | TYPE_FRACTION =6, 39 | TYPE_FIRST_INT =16, 40 | TYPE_INT_DEC =16, 41 | TYPE_INT_HEX =17, 42 | TYPE_INT_BOOLEAN =18, 43 | TYPE_FIRST_COLOR_INT =28, 44 | TYPE_INT_COLOR_ARGB8 =28, 45 | TYPE_INT_COLOR_RGB8 =29, 46 | TYPE_INT_COLOR_ARGB4 =30, 47 | TYPE_INT_COLOR_RGB4 =31, 48 | TYPE_LAST_COLOR_INT =31, 49 | TYPE_LAST_INT =31; 50 | 51 | public static final int 52 | COMPLEX_UNIT_PX =0, 53 | COMPLEX_UNIT_DIP =1, 54 | COMPLEX_UNIT_SP =2, 55 | COMPLEX_UNIT_PT =3, 56 | COMPLEX_UNIT_IN =4, 57 | COMPLEX_UNIT_MM =5, 58 | COMPLEX_UNIT_SHIFT =0, 59 | COMPLEX_UNIT_MASK =15, 60 | COMPLEX_UNIT_FRACTION =0, 61 | COMPLEX_UNIT_FRACTION_PARENT=1, 62 | COMPLEX_RADIX_23p0 =0, 63 | COMPLEX_RADIX_16p7 =1, 64 | COMPLEX_RADIX_8p15 =2, 65 | COMPLEX_RADIX_0p23 =3, 66 | COMPLEX_RADIX_SHIFT =4, 67 | COMPLEX_RADIX_MASK =3, 68 | COMPLEX_MANTISSA_SHIFT =8, 69 | COMPLEX_MANTISSA_MASK =0xFFFFFF; 70 | 71 | } 72 | -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/test/AXMLPrinter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Android4ME 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package wind.test; 17 | 18 | import java.io.FileInputStream; 19 | 20 | import wind.android.content.res.AXmlResourceParser; 21 | import wind.v1.XmlPullParser; 22 | //import android.util.TypedValue; 23 | 24 | /** 25 | * @author Dmitry Skiba 26 | * 27 | * This is example usage of AXMLParser class. 28 | * 29 | * Prints xml document from Android's binary xml file. 30 | */ 31 | public class AXMLPrinter { 32 | 33 | public static void main(String[] arguments) { 34 | if (arguments.length<1) { 35 | log("Usage: AXMLPrinter "); 36 | return; 37 | } 38 | try { 39 | AXmlResourceParser parser=new AXmlResourceParser(); 40 | parser.open(new FileInputStream(arguments[0])); 41 | StringBuilder indent=new StringBuilder(10); 42 | final String indentStep=" "; 43 | while (true) { 44 | int type=parser.next(); 45 | if (type==XmlPullParser.END_DOCUMENT) { 46 | break; 47 | } 48 | switch (type) { 49 | case XmlPullParser.START_DOCUMENT: 50 | { 51 | log(""); 52 | break; 53 | } 54 | case XmlPullParser.START_TAG: 55 | { 56 | log("%s<%s%s",indent, 57 | getNamespacePrefix(parser.getPrefix()),parser.getName()); 58 | indent.append(indentStep); 59 | 60 | int namespaceCountBefore=parser.getNamespaceCount(parser.getDepth()-1); 61 | int namespaceCount=parser.getNamespaceCount(parser.getDepth()); 62 | for (int i=namespaceCountBefore;i!=namespaceCount;++i) { 63 | log("%sxmlns:%s=\"%s\"", 64 | indent, 65 | parser.getNamespacePrefix(i), 66 | parser.getNamespaceUri(i)); 67 | } 68 | 69 | for (int i=0;i!=parser.getAttributeCount();++i) { 70 | log("%s%s%s=\"%s\"",indent, 71 | getNamespacePrefix(parser.getAttributePrefix(i)), 72 | parser.getAttributeName(i), 73 | getAttributeValue(parser,i)); 74 | } 75 | log("%s>",indent); 76 | break; 77 | } 78 | case XmlPullParser.END_TAG: 79 | { 80 | indent.setLength(indent.length()-indentStep.length()); 81 | log("%s",indent, 82 | getNamespacePrefix(parser.getPrefix()), 83 | parser.getName()); 84 | break; 85 | } 86 | case XmlPullParser.TEXT: 87 | { 88 | log("%s%s",indent,parser.getText()); 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | catch (Exception e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | 99 | private static String getNamespacePrefix(String prefix) { 100 | if (prefix==null || prefix.length()==0) { 101 | return ""; 102 | } 103 | return prefix+":"; 104 | } 105 | 106 | private static String getAttributeValue(AXmlResourceParser parser,int index) { 107 | int type=parser.getAttributeValueType(index); 108 | int data=parser.getAttributeValueData(index); 109 | // if (type==TypedValue.TYPE_STRING) { 110 | // return parser.getAttributeValue(index); 111 | // } 112 | // if (type==TypedValue.TYPE_ATTRIBUTE) { 113 | // return String.format("?%s%08X",getPackage(data),data); 114 | // } 115 | // if (type==TypedValue.TYPE_REFERENCE) { 116 | // return String.format("@%s%08X",getPackage(data),data); 117 | // } 118 | // if (type==TypedValue.TYPE_FLOAT) { 119 | // return String.valueOf(Float.intBitsToFloat(data)); 120 | // } 121 | // if (type==TypedValue.TYPE_INT_HEX) { 122 | // return String.format("0x%08X",data); 123 | // } 124 | // if (type==TypedValue.TYPE_INT_BOOLEAN) { 125 | // return data!=0?"true":"false"; 126 | // } 127 | // if (type==TypedValue.TYPE_DIMENSION) { 128 | // return Float.toString(complexToFloat(data))+ 129 | // DIMENSION_UNITS[data & TypedValue.COMPLEX_UNIT_MASK]; 130 | // } 131 | // if (type==TypedValue.TYPE_FRACTION) { 132 | // return Float.toString(complexToFloat(data))+ 133 | // FRACTION_UNITS[data & TypedValue.COMPLEX_UNIT_MASK]; 134 | // } 135 | // if (type>=TypedValue.TYPE_FIRST_COLOR_INT && type<=TypedValue.TYPE_LAST_COLOR_INT) { 136 | // return String.format("#%08X",data); 137 | // } 138 | // if (type>=TypedValue.TYPE_FIRST_INT && type<=TypedValue.TYPE_LAST_INT) { 139 | // return String.valueOf(data); 140 | // } 141 | return String.format("<0x%X, type 0x%02X>",data,type); 142 | } 143 | 144 | private static String getPackage(int id) { 145 | if (id>>>24==1) { 146 | return "android:"; 147 | } 148 | return ""; 149 | } 150 | 151 | private static void log(String format,Object...arguments) { 152 | System.out.printf(format,arguments); 153 | System.out.println(); 154 | } 155 | 156 | /////////////////////////////////// ILLEGAL STUFF, DONT LOOK :) 157 | 158 | public static float complexToFloat(int complex) { 159 | return (float)(complex & 0xFFFFFF00)*RADIX_MULTS[(complex>>4) & 3]; 160 | } 161 | 162 | private static final float RADIX_MULTS[]={ 163 | 0.00390625F,3.051758E-005F,1.192093E-007F,4.656613E-010F 164 | }; 165 | private static final String DIMENSION_UNITS[]={ 166 | "px","dip","sp","pt","in","mm","","" 167 | }; 168 | private static final String FRACTION_UNITS[]={ 169 | "%","%p","","","","","","" 170 | }; 171 | } -------------------------------------------------------------------------------- /xpatch/src/main/java/wind/v1/XmlPullParserException.java: -------------------------------------------------------------------------------- 1 | /* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ 2 | // for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) 3 | 4 | package wind.v1; 5 | 6 | /** 7 | * This exception is thrown to signal XML Pull Parser related faults. 8 | * 9 | * @author Aleksander Slominski 10 | */ 11 | public class XmlPullParserException extends Exception { 12 | protected Throwable detail; 13 | protected int row = -1; 14 | protected int column = -1; 15 | 16 | /* public XmlPullParserException() { 17 | }*/ 18 | 19 | public XmlPullParserException(String s) { 20 | super(s); 21 | } 22 | 23 | /* 24 | public XmlPullParserException(String s, Throwable thrwble) { 25 | super(s); 26 | this.detail = thrwble; 27 | } 28 | 29 | public XmlPullParserException(String s, int row, int column) { 30 | super(s); 31 | this.row = row; 32 | this.column = column; 33 | } 34 | */ 35 | 36 | public XmlPullParserException(String msg, XmlPullParser parser, Throwable chain) { 37 | super ((msg == null ? "" : msg+" ") 38 | + (parser == null ? "" : "(position:"+parser.getPositionDescription()+") ") 39 | + (chain == null ? "" : "caused by: "+chain)); 40 | 41 | if (parser != null) { 42 | this.row = parser.getLineNumber(); 43 | this.column = parser.getColumnNumber(); 44 | } 45 | this.detail = chain; 46 | } 47 | 48 | public Throwable getDetail() { return detail; } 49 | // public void setDetail(Throwable cause) { this.detail = cause; } 50 | public int getLineNumber() { return row; } 51 | public int getColumnNumber() { return column; } 52 | 53 | /* 54 | public String getMessage() { 55 | if(detail == null) 56 | return super.getMessage(); 57 | else 58 | return super.getMessage() + "; nested exception is: \n\t" 59 | + detail.getMessage(); 60 | } 61 | */ 62 | 63 | //NOTE: code that prints this and detail is difficult in J2ME 64 | public void printStackTrace() { 65 | if (detail == null) { 66 | super.printStackTrace(); 67 | } else { 68 | synchronized(System.err) { 69 | System.err.println(super.getMessage() + "; nested exception is:"); 70 | detail.printStackTrace(); 71 | } 72 | } 73 | } 74 | 75 | } 76 | 77 | --------------------------------------------------------------------------------