diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a1c2a23
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1d89a10
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 wangcong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 950338a..061e852 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,79 @@
-# wework
-【xposed wework wechat 企业微信 微信 逆向】自动抢回复 会话 自动通过 好友列表 群管理 机器人 SDK ,底层需要 Xposed 或 VirtualXposed 等Hooking框架的支持,如果你手机安装有xposed框架,那么可以下载源码直接运行
+
+一个使用Kotlin编写的半开源企业插件框架,底层需要 Xposed 或 VirtualXposed 等Hooking框架的支持,目前项目主要针对wework进行逆向学习。
+
+### 最重要事情
+
+**【免责声明】:**
+此系列文章主要关于xposed的相关学习,以下所提及到的所有方式皆为学习,如有他人使用本系列学习文章中所提及的知识点用于其他非法用途,本人不承担由此造成的任何后果!!
+
+
+### 全部功能文档列表,持续更新
+
+当前适配版本从2.8.12 ~ **3.0.36**最新版
+
+**主要功能列表**
+
+- 好友、联系人、用户相关
+
+> - 联系人列表监听:好友数量变化、联系人变化、同步状态变化等
+> - 修改联系人备注修改:备注名、企业名、电话、描述、备注的名片或图片
+> - 内部成员备注修改:备注名、描述
+> - 联系人操作:通过id获取、添加联系人、删除联系人、搜索联系人、通过好友申请、拒绝添加、删除申请、标记客户等等
+> - 用户信息:获取公司信息、获取二维码、获取不同风格的二维码、修改头像、修改职务等
+> - 部门联系人:获取所有部门信息、获取部门内部信息、获取组织架构、获取父级部门、获取子部门等
+
+- 会话相关
+
+> - 群列表监听:同步状态、添加到群内、退出群聊
+> - 群会话监听:添加成员、群主变化、收到消息、群名称变化、成员变化等等
+> - 会话操作:获取列表、退出群聊、创建群聊、解散群聊、修改群名称、修改群内昵称、邀请成员、移除成员、搜索会话及联系人、获取群二维码等等
+> - 群操作:设置群主、设置入群验证、设置禁言、置顶、保存到通讯录、设置免打扰等等
+> - 会话信息:最近消息、成员信息、会话名称、头像、判定是否包含某成员、会话扩展信息、判定是否为微信用户等等
+
+- 消息相关
+
+> - 发送消息:包括但不限于文字、图片、语音、视频、文件、小程序、链接、地理位置等
+> - 接收消息:包括但不限于文字、图片、语音、视频、文件、小程序、链接、地理位置等
+> - 文件消息自动下载:图片、语音、文件、视频
+
+- 企业微信与微信公用
+
+> - Activity
+> - 文件操作(写入、读取)
+> - 数据库操作(增删改查)
+
+- 基础核心功能
+
+> - APK自动解析
+> - 异步批处理
+> - 二级缓存
+> - 网络请求
+> - 重试策略
+> - 文件下载
+> - 自动缓存
+> - 反射查找
+> - silk音频编解码
+
+
+当然还有更多的功能不仅限于上述,更多可以查阅我针对企业微信的xposed学习的成果,这些成果的部分我将在后续通过讲解并上传
+
+为了保证执行的可靠稳定性,针对上述功能**在客户端**设计了关于指令的队列处理,解决了很多复杂场景下的问题
+
+如果需要查阅具体接口文档可以与我联系申请查看,我将毫无保留的开放设计理念和文档
+
+### SDK已经可以使用了
+以下是根据SDK开发出来的demo,欢迎交流
+
+
+
+
+### 注意:
+为了避免某些xxx风险,我只是持续做一些分享,但并不会将完整代码上传,我所上传的基础核心,基本上你都可以在我所写的文章及有Android基础之上一步一步的去实现
+
+### 联系我
+
+如果你在学习过程中遇到问题,你可以直接提交issue,或者直接联系我,请添加时备注:xposed、wework+姓
+
+
+
+
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/app.iml b/app/app.iml
new file mode 100644
index 0000000..9e1e1c1
--- /dev/null
+++ b/app/app.iml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..c648a75
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,86 @@
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+ defaultConfig {
+ applicationId "com.magic.xmagichooker"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ lintOptions {
+ checkReleaseBuilds false
+ abortOnError false
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ debuggable false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled false
+ debuggable true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.0.2'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation project(":kernel")
+ implementation project(":shared")
+ implementation project(":wework")
+ implementation 'com.google.code.gson:gson:2.8.6'
+ compileOnly 'de.robv.android.xposed:api:82'
+ compileOnly 'de.robv.android.xposed:api:82:sources'
+}
+
+// 每次修改运行后自动让 VXP 中的模块`即时生效` ,需要将 (Debug Configurations) - Before Launch - Gradle aware Make - 修改为 :app:installDebug
+afterEvaluate {
+ installDebug.doLast {
+ updateVirtualXposedAPP.execute()
+ rebootVirtualXposedAPP.execute()
+ launchVirtualXposedAPP.execute()
+ }
+}
+
+// 更新 VXP 中的 app
+task updateVirtualXposedAPP(type: Exec) {
+ def pkg = android.defaultConfig.applicationId
+ commandLine android.adbExecutable, 'shell', 'am', 'broadcast', '-a', 'io.va.exposed.CMD', '-e', 'cmd', 'update', '-e', 'pkg', pkg
+}
+
+// 重启 VXP
+task rebootVirtualXposedAPP(type: Exec) {
+ commandLine android.adbExecutable, 'shell', 'am', 'broadcast', '-a', 'io.va.exposed.CMD', '-e', 'cmd', 'reboot'
+}
+
+// 重启 VXP 企业微信
+task launchVirtualXposedAPP(type: Exec) {
+ def pkg = 'com.tencent.wework'
+ commandLine android.adbExecutable, 'shell', 'am', 'broadcast', '-a', 'io.va.exposed.CMD', '-e', 'cmd', 'launch', '-e', 'pkg', pkg
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/androidTest/java/com/magic/xmagichooker/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/magic/xmagichooker/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..7cef1fd
--- /dev/null
+++ b/app/src/androidTest/java/com/magic/xmagichooker/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.magic.xmagichooker
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.magic.xmagichooker", appContext.packageName)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b00cc4c
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init
new file mode 100644
index 0000000..5ee19a2
--- /dev/null
+++ b/app/src/main/assets/xposed_init
@@ -0,0 +1 @@
+com.magic.xmagichooker.Hooker
diff --git a/app/src/main/java/com/magic/xmagichooker/Hooker.kt b/app/src/main/java/com/magic/xmagichooker/Hooker.kt
new file mode 100644
index 0000000..83a6729
--- /dev/null
+++ b/app/src/main/java/com/magic/xmagichooker/Hooker.kt
@@ -0,0 +1,76 @@
+package com.magic.xmagichooker
+
+import android.app.Application
+import android.content.Context
+import android.util.Log
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.MagicHooker
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+import com.magic.kernel.helper.TryHelper.tryVerbosely
+import com.magic.shared.apis.SharedEngine
+import com.magic.wework.apis.WwEngine
+import dalvik.system.PathClassLoader
+import de.robv.android.xposed.*
+import java.io.File
+
+class Hooker : IXposedHookLoadPackage, IXposedHookZygoteInit {
+
+ private val TARGET_PACKAGE = "com.magic.xmagichooker"
+
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
+ tryVerbosely {
+ when (lpparam.packageName) {
+ TARGET_PACKAGE ->
+ hookAttachBaseContext(lpparam.classLoader) {
+ hookLoadHooker(lpparam.classLoader)
+ }
+ else -> if (MagicHooker.isImportantWechatProcess(lpparam)) {
+ hookAttachBaseContext(lpparam.classLoader) {
+ hookTencent(lpparam, it)
+ }
+ }
+ }
+ }
+ }
+
+ override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam?) {
+ Log.e(Hooker::class.java.name, "initZygote ${startupParam?.modulePath} ${startupParam?.startsSystemServer}")
+ }
+
+ private fun hookAttachBaseContext(classLoader: ClassLoader, callback: (Context) -> Unit) {
+ XposedHelpers.findAndHookMethod(
+ "android.content.ContextWrapper",
+ classLoader,
+ "attachBaseContext",
+ Context::class.java,
+ object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam?) {
+ callback(param?.thisObject as? Application ?: return)
+ }
+ })
+ }
+
+ private fun hookLoadHooker(classLoader: ClassLoader) {
+ XposedHelpers.findAndHookMethod(
+ "$TARGET_PACKAGE.MainActivity", classLoader,
+ "checkHook", object : XC_MethodReplacement() {
+ override fun replaceHookedMethod(param: MethodHookParam): Any = true
+ })
+ }
+
+ private fun hookTencent(lpparam: XC_LoadPackage.LoadPackageParam, context: Context) {
+ when (lpparam.packageName) {
+ "com.tencent.wework" -> {
+ MagicHooker.startup(
+ lpparam = lpparam,
+ plugins = listOf(Plugins),
+ centers = WwEngine.hookerCenters + SharedEngine.hookerCenters
+ )
+ }
+ "com.tencent.mm" -> {
+ Log.e(Hooker::class.java.name, "开始启动个人微信插件")
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/magic/xmagichooker/MainActivity.kt b/app/src/main/java/com/magic/xmagichooker/MainActivity.kt
new file mode 100644
index 0000000..157aca1
--- /dev/null
+++ b/app/src/main/java/com/magic/xmagichooker/MainActivity.kt
@@ -0,0 +1,40 @@
+package com.magic.xmagichooker
+
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import com.magic.kernel.MagicHooker
+import com.magic.kernel.media.audio.AudioHelper
+import com.magic.kernel.okhttp.HttpClients
+import com.magic.kernel.okhttp.IHttpConfigs
+import com.magic.kernel.utils.CmdUtil
+import kotlinx.android.synthetic.main.activity_main.*
+import java.io.File
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ CmdUtil.isRoot
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (checkHook()) {
+ val path = MagicHooker.getApplicationApkPath("com.magic.xmagichooker")
+ sample_text.text = "hooked = true \n \n $path"
+ }
+ }
+
+ fun checkHook(): Boolean {
+ return false
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+}
diff --git a/app/src/main/java/com/magic/xmagichooker/Plugins.kt b/app/src/main/java/com/magic/xmagichooker/Plugins.kt
new file mode 100644
index 0000000..82b3550
--- /dev/null
+++ b/app/src/main/java/com/magic/xmagichooker/Plugins.kt
@@ -0,0 +1,470 @@
+package com.magic.xmagichooker
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import com.magic.shared.hookers.interfaces.IActivityHooker
+import com.magic.wework.hookers.interfaces.IApplicationHooker
+import com.magic.wework.hookers.interfaces.IConversationHooker
+import com.magic.wework.apis.com.tencent.wework.foundation.model.Conversation
+import com.magic.wework.apis.com.tencent.wework.foundation.model.Message
+
+object Plugins: IActivityHooker, IApplicationHooker, IConversationHooker {
+
+ /* ------------------ IActivityHooker ----------------- */
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ Log.e(Plugins::class.java.name, "onActivityCreated class: ${activity.javaClass}")
+ }
+
+ /* ------------------ IConversationHooker ----------------- */
+ override fun onReconvergeConversation() {
+ Log.e(Plugins::class.java.name, "onReconvergeConversation")
+ }
+
+ override fun onReloadConvsProperty() {
+ Log.e(Plugins::class.java.name, "onReloadConvsProperty")
+ }
+
+ override fun onSyncStateChanged(i: Int, i2: Int) {
+ Log.e(Plugins::class.java.name, "onSyncStateChanged i: $i i2: $i2")
+ }
+
+ override fun onAddConversations(conversationArr: Array) {
+ for (conv in conversationArr) {
+ Log.e(Plugins.javaClass.name, "onAddConversations ${Conversation.getInfo(conv)}")
+ }
+ }
+
+ override fun onExitConversation(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onExitConversation ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onExitConversation remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onSetReadReceipt(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetReadReceipt ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetReadReceipt remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onAddMembers(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onAddMembers ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// for (member in conv.getMembers()) {
+// Log.e(Plugins::class.java.name, "onAddMembers remoteId: ${member.operatorRemoteId} name: ${member.name} nickname: ${member.nickName}")
+// }
+ }
+
+ override fun onAddMessages(conversation: Any, messageArr: Array, z: Boolean) {
+ for (msg in messageArr) {
+ Log.e(Plugins.javaClass.name, "onAddMessages ${Conversation.getInfo(conversation)} ${Message.getInfo(msg)}")
+ }
+// val conv = Conversation(conversation)
+// for (message0 in messageArr) {
+// Log.e(Plugins.javaClass.name, "emotion消息类型: $contentType 地址:$downloadInfo")
+// if (downloadInfo != null) {
+// FileDownloadApiImpl.newInstance().downloadFile(downloadInfo) { i, str ->
+// Log.e(Plugins.javaClass.name, "下载文件:${if (i == 0) "成功" else "失败"}")
+// }
+// }
+// }
+// Log.e(Plugins.javaClass.name, "下载文件:${if (i == 0) "成功" else "失败"}")
+// }
+// }
+// }
+// val timeInterval = System.currentTimeMillis() - (Message(message0).getInfo().sendTime.toLong() * 1000)
+// Log.e(Plugins::class.java.name, "onAddMessages 文本消息 ${textMessage.codeLanguage} 消息内容: ${String(textMessage.content)} 时间: $timeInterval")
+// val text = String(textMessage.content)
+// val textSplits = text.split(":")
+// if (timeInterval > 10000) return
+// if (text.startsWith("调试", true)) {
+// } else if (text.startsWith("发送文本消息")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// }
+// } else {
+// }
+// } else if (text.startsWith("发送图片消息") || text.startsWith("发送语音消息") || text.startsWith("发送视频消息") || text.startsWith("发送文件消息")) {
+// var type = IHttpConfigs.Type.DEFAULT
+// if (textSplits.first().equals("发送图片消息", true)) {
+// type = IHttpConfigs.Type.IMAGE
+// } else if (textSplits.first().equals("发送语音消息", true)) {
+// type = IHttpConfigs.Type.VOICE
+// } else if (textSplits.first().equals("发送视频消息", true)) {
+// type = IHttpConfigs.Type.VIDEO
+// }
+// if (textSplits.size > 1) {
+// val splits = text.removeRange(0, 7).split(",")
+// var id = conv.getLocalId()
+// var urlString = ""
+// if (splits.size > 1) {
+// id = splits.first().toLong()
+// urlString = if (splits.last().toString().startsWith("http")) splits.last().toString() else ""
+// } else {
+// urlString = splits.last().toString()
+// }
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("发送定位消息")) {
+// var id = conv.getLocalId()
+// if (textSplits.size > 1) {
+// id = textSplits.last().toString().trim().toLong()
+// }
+// } else if (text.startsWith("发送链接消息")) {
+// var id = conv.getLocalId()
+// if (textSplits.size > 1) {
+// id = textSplits.last().toString().trim().toLong()
+// } else if (text.startsWith("发送小程序消息")) {
+// var id = conv.getLocalId()
+// if (textSplits.size > 1) {
+// id = textSplits.last().toString().trim().toLong()
+// }
+// } else if (text.startsWith("获取所有群聊")) {
+// val conversationInfos = conversations.map {
+// val conv = Conversation(it)
+// val convName = if (conv.getInfo().name.isEmpty()) conv.getDefaultName(true) else conv.getInfo().name
+// return@map "群rId: ${conv.getRemoteId()} type: ${conv.getType()} 群名称: $convName \n"
+// }
+// } else if (text.startsWith("获取保存到通讯录的群聊")) {
+// val conversationInfos = conversations.map {
+// val conv = Conversation(it)
+// return@map "群rId: ${conv.getRemoteId()} 群名称: ${conv.getDefaultName(true)}"
+// }
+// } else if (text.startsWith("获取免打扰及置顶会话")) {
+// } else if (text.startsWith("获取群二维码")) {
+// } else if (text.startsWith("保存到通讯录")) {
+// var convId = conv.getLocalId()
+// if (textSplits.size > 1) {
+// convId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("创建新群聊")) {
+// if (textSplits.size > 1) {
+// val userIds = textSplits[1].split(",").map { it.trim().toLong() }.toLongArray()
+// if (userIds.size > 1) {
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("解散群聊")) {
+// var convId = conv.getLocalId()
+// if (textSplits.size > 1) {
+// convId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("更新会话信息")) {
+// var convId = conv.getLocalId()
+// if (textSplits.size > 1) {
+// convId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("邀请他人入群")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// val convId = splits.first().trim().toLong()
+// val userId = splits.last().trim().toLong()
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("撤回邀请")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// val convId = splits.first().trim().toLong()
+// val userId = splits.last().trim().toLong()
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("添加他人入群")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// val convId = splits.first().trim().toLong()
+// val userId = splits.last().trim().toLong()
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("移除群聊")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// val convId = splits.first().trim().toLong()
+// val userId = splits.last().trim().toLong()
+// } else {
+// }
+// } else {
+// }
+// } else if (text.startsWith("获取成员信息")) {
+// var convId = conv.getLocalId()
+// if (textSplits.size > 1) {
+// convId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("清除未读消息")) {
+// var convId = conv.getLocalId()
+// if (textSplits.size > 1) {
+// convId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("退出群聊")) {
+// val conversationId = if (textSplits.size > 1) textSplits[1].trim().toLong() else conv.getInfo().id
+// } else if (text.startsWith("修改群名称")) {
+// val name = if (textSplits.size > 1) textSplits[1].trim() else "测试修改群名称"
+// } else if (text.startsWith("修改群内昵称")) {
+// val nickname = if (textSplits.size > 1) textSplits[1].trim() else "测试修改群内昵称"
+// } else if (text.startsWith("撤回该条消息")) {
+// } else if (text.startsWith("设置群公告")) {
+// var convId = conv.getLocalId()
+// var notification = "测试群公告"
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// if (splits.size > 1) {
+// convId = splits.first().trim().toLong()
+// notification = splits.last().toString()
+// } else {
+// notification = splits.last().toString()
+// }
+// }
+// } else if (text.startsWith("置顶")) {
+// } else if (text.startsWith("取消置顶")) {
+// } else if (text.startsWith("免打扰")) {
+// } else if (text.startsWith("取消免打扰")) {
+// } else if (text.startsWith("获取缓存的联系人")) {
+// } else if (text.startsWith("获取我的二维码")) {
+// } else if (text.startsWith("获取二维码")) {
+// var type = ContactService.GETCONTACT_BY_QR_CODE
+// if (textSplits.size > 1) {
+// type = textSplits.last().trim().toInt()
+// }
+// } else if (text.startsWith("获取公司信息")) {
+// } else if (text.startsWith("修改客户备注")) {
+// if (textSplits.size > 1) {
+// val userId = textSplits.last().trim().toLong()
+// } else {
+// }
+// } else if (text.startsWith("修改同事备注")) {
+// if (textSplits.size > 1) {
+// val splits = textSplits[1].split(",")
+// var realRemark = "备注"
+// var remarks = "描述"
+// val userId = splits.first().trim().toLong()
+// if (splits.size > 2) {
+// realRemark = splits[1].toString()
+// remarks = splits.last().toString()
+// } else if (splits.size > 1) {
+// realRemark = splits.last().toString()
+// }
+// }
+// } else if (text.startsWith("搜索联系人")) {
+// if (textSplits.size > 1) {
+// val keyword = textSplits.last().trim()
+// }
+// } else if (text.startsWith("搜索本地联系人")) {
+// if (textSplits.size > 1) {
+// val keyword = textSplits.last().trim()
+// } else if (text.startsWith("标记联系人")) {
+// } else if (text.startsWith("获取被标记的联系人")) {
+// } else if (text.startsWith("获取一级部门信息")) {
+// } else if (text.startsWith("获取部门用户信息")) {
+// var departmentId = 1688852946270840
+// if (textSplits.size > 1) {
+// departmentId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("获取二级部门信息")) {
+// } else if (text.startsWith("获取指定部门")) {
+// } else if (text.startsWith("获取部门架构")) {
+// var departmentId = 1688852946270840
+// if (textSplits.size > 1) {
+// departmentId = textSplits.last().trim().toLong()
+// }
+// } else if (text.startsWith("修改职务")) {
+// var jobName = "测试"
+// if (textSplits.size > 1) {
+// jobName = textSplits.last().toString()
+// }
+// } else if (text.startsWith("修改对外职务")) {
+// var jobName = "测试"
+// if (textSplits.size > 1) {
+// jobName = textSplits.last().toString()
+// }
+// } else if (text.startsWith("修改头像")) {
+// var avatarUrl = "http://b.hiphotos.baidu.com/image/pic/item/0eb30f2442a7d9337119f7dba74bd11372f001e0.jpg"
+// if (textSplits.size > 1) {
+// avatarUrl = textSplits.last().toString().trim()
+// }
+// HttpClients.download(avatarUrl, IHttpConfigs.Type.IMAGE, iDownloadCallback = { localPath, _ ->
+// if (localPath != null) {
+// })
+// } else if (text.startsWith("获取绑定微信状态")) {
+// } else if (text.startsWith("删除联系人")) {
+// when (textSplits.size > 1) {
+// true -> {
+// val contactIds = textSplits[1].split(",").map { it.trim().toLong() }.toLongArray()
+// }
+// }
+// } else {
+// }
+// }
+// }
+// false -> {
+// }
+// }
+// } else if (text.startsWith("获取")) {
+// var contactType = 0
+// when (String(textMessage.content)) {
+// "获取我的微信联系人" -> contactType
+// "获取我的手机联系人" -> contactType
+// "获取推荐的好友" -> contactType
+// "获取我的同事" -> contactType
+// "获取我的客户" -> contactType
+// "获取待添加的客户" -> contactType
+// "获取内部联系客户" -> contactType =
+// "获取联系群组" -> contactType =
+// "获取历史好友" -> contactType =
+// "获取加星联系人" -> contactType =
+// "获取其他组织" -> contactType =
+// "获取保存的群组" -> contactType = .CONTACT_TYPE_GROUP_MEM
+// "获取我的好友" -> contactType = .CONTACT_TYPE_RCT_FRIEND
+// }
+// } else if (text.startsWith("查看指令集")) {
+// }
+// }
+// }
+// }
+// }
+
+ }
+
+ override fun onChangeOwner(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onChangeOwner ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onChangeOwner remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onDraftDidChange(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onDraftDidChange ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onDraftDidChange remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onMessageStateChange(conversation: Any, message: Any, i: Int) {
+ Log.e(Plugins.javaClass.name, "onMessageStateChange ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onMessageStateChange: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+// val msg = Message(message)
+// Log.e(Plugins::class.java.name, "onMessageStateChange: ${msg.getInfo().contentType} - ${String(msg.getInfo().content)} 状态: ${i}")
+ }
+
+ override fun onMessageUpdate(conversation: Any, message: Any) {
+ Log.e(Plugins.javaClass.name, "onMessageUpdate ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onMessageUpdate remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+// val msg = Message(message)
+// Log.e(Plugins::class.java.name, "onMessageUpdate 类型: ${msg.getInfo().contentType} - 内容: ${String(msg.getInfo().content)}")
+ }
+
+ override fun onModifyName(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onModifyName ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onMessageUpdate remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onPropertyChanged(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onPropertyChanged ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onPropertyChanged remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onRemoveAllMessages(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onRemoveAllMessages ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onRemoveAllMessages remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+ }
+
+ override fun onRemoveMembers(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onRemoveMembers ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onRemoveMembers remoteId: ${conv.getInfo().remoteId} - name: ${conv.getInfo().name} - type: ${conv.getInfo().type}")
+// for (member in conv.getMembers()) {
+// Log.e(Plugins::class.java.name, "onRemoveMembers remoteId: ${member.operatorRemoteId} name: ${member.name} nickname: ${member.nickName}")
+// }
+ }
+
+ override fun onRemoveMessages(conversation: Any, message: Any) {
+ Log.e(Plugins.javaClass.name, "onRemoveMessages ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onRemoveMessages: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+// val msg = Message(message)
+// Log.e(Plugins::class.java.name, "onRemoveMessages: ${msg.getInfo().contentType} - ${String(msg.getInfo().content)}")
+ }
+
+ override fun onSetAllBan(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetAllBan ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetAllBan: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetCollect(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetCollect ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetCollect: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetConfirmAddMember(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetConfirmAddMember ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetConfirmAddMember: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetMembersBan(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetMembersBan ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetMembersBan: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetOwnerManager(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetOwnerManager ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetOwnerManager: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetShield(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetShield ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetShield: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onSetTop(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onSetTop ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onSetTop: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onTypingStateUpdate(conversation: Any) {
+ Log.e(Plugins.javaClass.name, "onTypingStateUpdate ${Conversation.getInfo(conversation)}")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onTypingStateUpdate: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ override fun onUnReadCountChanged(conversation: Any, i: Int, i2: Int) {
+ Log.e(Plugins.javaClass.name, "onUnReadCountChanged ${Conversation.getInfo(conversation)} $i $i2")
+// val conv = Conversation(conversation)
+// Log.e(Plugins::class.java.name, "onUnReadCountChanged: ${conv.getInfo().remoteId} - ${conv.getInfo().name} - ${conv.getInfo().type}")
+ }
+
+ /* ------------------ IContactHooker ----------------- */
+//
+// override fun onApplyUnReadCountChanged(i: Int) {
+// Log.e(Plugins::class.java.name, "onApplyUnReadCountChanged: i: $i")
+// }
+//
+// override fun onContactListUnradCountChanged(i: Int, i2: Int, i3: Int) {
+// Log.e(Plugins::class.java.name, "onContactListUnradCountChanged: i: $i i2: $i2 i3: $i3")
+// }
+//
+// override fun onSyncContactFinish(i: Int, z: Boolean) {
+// Log.e(Plugins::class.java.name, "onSyncContactFinish: i: $i z: $z")
+// }
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..2408e30
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..96ee307
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..69b2233
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fd72fc9
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ XMagicHooker
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/test/java/com/magic/xmagichooker/ExampleUnitTest.kt b/app/src/test/java/com/magic/xmagichooker/ExampleUnitTest.kt
new file mode 100644
index 0000000..e2db6d4
--- /dev/null
+++ b/app/src/test/java/com/magic/xmagichooker/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.magic.xmagichooker
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..52c96ce
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,35 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ jcenter()
+ google()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+
+ mavenCentral()
+ maven {
+ url 'https://jitpack.io'
+ }
+ maven { url "https://dl.bintray.com/thelasterstar/maven/" }
+
+ jcenter()
+ google()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..97de4ff
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,28 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..0bf5490
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Feb 01 00:21:08 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kernel/.cxx/ndk_locator_record.json b/kernel/.cxx/ndk_locator_record.json
new file mode 100644
index 0000000..b91a5ce
--- /dev/null
+++ b/kernel/.cxx/ndk_locator_record.json
@@ -0,0 +1,41 @@
+{
+ "ndkFolder": "/Users/wangcong/Library/Android/sdk/ndk/21.0.6113669",
+ "messages": [
+ {
+ "level": "INFO",
+ "message": "android.ndkVersion from module build.gradle is not set"
+ },
+ {
+ "level": "INFO",
+ "message": "ndk.dir in local.properties is not set"
+ },
+ {
+ "level": "INFO",
+ "message": "ANDROID_NDK_HOME environment variable is not set"
+ },
+ {
+ "level": "INFO",
+ "message": "sdkFolder is /Users/wangcong/Library/Android/sdk"
+ },
+ {
+ "level": "INFO",
+ "message": "Considering /Users/wangcong/Library/Android/sdk/ndk-bundle in SDK ndk-bundle folder"
+ },
+ {
+ "level": "INFO",
+ "message": "Considering /Users/wangcong/Library/Android/sdk/ndk/16.1.4479499 in SDK ndk folder"
+ },
+ {
+ "level": "INFO",
+ "message": "Considering /Users/wangcong/Library/Android/sdk/ndk/21.0.6113669 in SDK ndk folder"
+ },
+ {
+ "level": "INFO",
+ "message": "Rejected /Users/wangcong/Library/Android/sdk/ndk-bundle in SDK ndk-bundle folder because that location has no source.properties"
+ },
+ {
+ "level": "INFO",
+ "message": "No user requested version, choosing /Users/wangcong/Library/Android/sdk/ndk/21.0.6113669 which is version 21.0.6113669"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/kernel/.gitignore b/kernel/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/kernel/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/kernel/build.gradle b/kernel/build.gradle
new file mode 100644
index 0000000..b8185a7
--- /dev/null
+++ b/kernel/build.gradle
@@ -0,0 +1,75 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+android {
+
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'consumer-rules.pro'
+
+ externalNativeBuild {
+ cmake {
+ version "3.10.2"
+ cppFlags "-frtti -fexceptions"
+ }
+ }
+ ndk {
+ moduleName "kernel"
+ abiFilters "armeabi-v7a", "x86"
+ cFlags "-DANDROID_NDK"
+ }
+
+ //Gradle 构建并打包某个特定abi体系架构下的.so库
+ sourceSets {
+ main() {
+ jniLibs.srcDirs=['src/main/jniLibs']
+ }
+ }
+ }
+
+ packagingOptions {
+ pickFirst 'lib/armeabi-v7a/libsilk.so'
+ pickFirst 'lib/arm64/libsilk.so'
+ pickFirst 'lib/arm64-v8a/libsilk.so'
+ pickFirst 'lib/x86/libsilk.so'
+ }
+
+ externalNativeBuild {
+ cmake {
+// path "src/main/cpp/CMakeLists.txt"
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation 'com.google.code.gson:gson:2.8.6'
+ implementation("com.squareup.okhttp3:okhttp:4.4.0")
+ compileOnly 'de.robv.android.xposed:api:82'
+ compileOnly 'de.robv.android.xposed:api:82:sources'
+}
+
+this.afterEvaluate {
+ this.copy {
+ from 'src/main/jniLibs/armeabi-v7a'
+ into 'src/main/jniLibs/arm64-v8a'
+ }
+}
+
diff --git a/kernel/consumer-rules.pro b/kernel/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/kernel/kernel.iml b/kernel/kernel.iml
new file mode 100644
index 0000000..fe71fd4
--- /dev/null
+++ b/kernel/kernel.iml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kernel/proguard-rules.pro b/kernel/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/kernel/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/kernel/src/androidTest/java/com/magic/kernel/ExampleInstrumentedTest.kt b/kernel/src/androidTest/java/com/magic/kernel/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..baec8f2
--- /dev/null
+++ b/kernel/src/androidTest/java/com/magic/kernel/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.magic.kernel
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.magic.kernel.test", appContext.packageName)
+ }
+}
diff --git a/kernel/src/main/AndroidManifest.xml b/kernel/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3ffd736
--- /dev/null
+++ b/kernel/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/kernel/src/main/java/com/magic/kernel/MagicGlobal.kt b/kernel/src/main/java/com/magic/kernel/MagicGlobal.kt
new file mode 100644
index 0000000..368b71b
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/MagicGlobal.kt
@@ -0,0 +1,122 @@
+package com.magic.kernel
+
+import android.util.Log
+import com.magic.kernel.core.Version
+import com.magic.kernel.core.WaitChannel
+import com.magic.kernel.helper.ParserHelper.ApkFile
+import com.magic.kernel.helper.ParserHelper.ClassTrie
+import com.magic.kernel.helper.TryHelper
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+
+object MagicGlobal {
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ const val INIT_TIMEOUT = 2000L // ms
+
+ @Volatile
+ var unitTestMode: Boolean = false
+
+ private val initChannel = WaitChannel()
+
+ @Volatile
+ var version: Version? = null
+ get() {
+ if (!unitTestMode) {
+ initChannel.wait(INIT_TIMEOUT)
+ initChannel.done()
+ }
+ return field
+ }
+
+ @Volatile
+ var packageName: String = ""
+ get() {
+ if (!unitTestMode) {
+ initChannel.wait(INIT_TIMEOUT)
+ initChannel.done()
+ }
+ return field
+ }
+
+ @Volatile
+ var classLoader: ClassLoader? = null
+ get() {
+ if (!unitTestMode) {
+ initChannel.wait(INIT_TIMEOUT)
+ initChannel.done()
+ }
+ return field
+ }
+
+ @Volatile
+ var classes: ClassTrie? = null
+ get() {
+ if (!unitTestMode) {
+ initChannel.wait(INIT_TIMEOUT)
+ initChannel.done()
+ }
+ return field
+ }
+
+ inline fun lazy(name: String, crossinline initializer: () -> T?): Lazy {
+ return if (unitTestMode) {
+ UnitTestLazyImpl {
+ initializer() ?: throw Error("Failed to evaluate $name")
+ }
+ } else {
+ lazy(LazyThreadSafetyMode.PUBLICATION) {
+ when (null) {
+ version -> throw Error("Invalid version")
+ packageName -> throw Error("Invalid packageName")
+ classLoader -> throw Error("Invalid classLoader")
+ classes -> throw Error("Invalid classes")
+ }
+ initializer() ?: throw Error("Failed to evaluate $name")
+ }
+ }
+ }
+
+ class UnitTestLazyImpl(private val initializer: () -> T) : Lazy,
+ java.io.Serializable {
+ @Volatile
+ private var lazyValue: Lazy = lazy(initializer)
+
+ fun refresh() {
+ lazyValue = lazy(initializer)
+ }
+
+ override val value: T
+ get() = lazyValue.value
+
+ override fun toString(): String = lazyValue.toString()
+
+ override fun isInitialized(): Boolean = lazyValue.isInitialized()
+ }
+
+ @JvmStatic
+ fun init(lpparam: XC_LoadPackage.LoadPackageParam, callback: (Boolean) -> Unit) {
+ TryHelper.tryAsynchronously {
+ if (initChannel.isDone()) {
+ return@tryAsynchronously
+ }
+
+ try {
+ version = MagicHooker.getApplicationVersion(lpparam.packageName)
+ packageName = lpparam.packageName
+ classLoader = lpparam.classLoader
+
+ Log.e(
+ MagicGlobal::class.java.name,
+ "init ${lpparam.appInfo.sourceDir} ${lpparam.appInfo.publicSourceDir} \n${version} ${packageName} ${classLoader}"
+ )
+ ApkFile(lpparam.appInfo.sourceDir).use {
+ classes = it.classTypes
+ callback(true)
+ }
+ } finally {
+ initChannel.done()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/MagicHooker.kt b/kernel/src/main/java/com/magic/kernel/MagicHooker.kt
new file mode 100644
index 0000000..62034d1
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/MagicHooker.kt
@@ -0,0 +1,109 @@
+package com.magic.kernel
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import com.magic.kernel.core.HookerCenter
+import com.magic.kernel.core.IHookerProvider
+import com.magic.kernel.core.Version
+import com.magic.kernel.utils.ParallelUtil.parallelForEach
+import com.magic.kernel.utils.XposedUtil
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.XposedBridge
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+import java.io.File
+
+object MagicHooker {
+
+ fun isImportantWechatProcess(lpparam: XC_LoadPackage.LoadPackageParam): Boolean {
+ val processName = lpparam.processName
+ when {
+ !processName.contains(':') -> {
+ }
+ processName.endsWith(":tools") -> {
+ }
+ else -> return false
+ }
+ // 检查微信依赖的JNI库是否存在, 以此判断当前应用是不是微信/企业微信
+ val features = listOf (
+ "libwechatcommon.so",
+ "libwechatmm.so",
+ "libwechatnetwork.so",
+ "libwechatsight.so",
+ "libwechatxlog.so"
+ )
+ return try {
+ val libraryDir = File(lpparam.appInfo.nativeLibraryDir)
+ features.filter { filename ->
+ File(libraryDir, filename).exists()
+ }.size >= 3
+ } catch (t: Throwable) { false }
+ }
+
+ fun getSystemContext(): Context {
+ val activityThreadClass = XposedHelpers.findClass("android.app.ActivityThread", null)
+ val activityThread =
+ XposedHelpers.callStaticMethod(activityThreadClass, "currentActivityThread")
+ val context = XposedHelpers.callMethod(activityThread, "getSystemContext") as Context?
+ return context ?: throw Error("Failed to get system context.")
+ }
+
+ fun getApplicationApkPath(packageName: String): String {
+ val pm = getSystemContext().packageManager
+ val apkPath = pm.getApplicationInfo(packageName, 0).publicSourceDir
+ return apkPath ?: throw Error("Failed to get the APK path of $packageName")
+ }
+
+ fun getApplicationLibsPath(packageName: String): String =
+ "${getApplicationApkPath(packageName).removeSuffix("base.apk")}lib/${Build.SUPPORTED_ABIS.first().split("-").first()}"
+
+ fun getApplicationVersion(packageName: String): Version {
+ val pm = getSystemContext().packageManager
+ val versionName = pm.getPackageInfo(packageName, 0).versionName
+ return Version(versionName
+ ?: throw Error("Failed to get the version of $packageName"))
+ }
+
+ fun startup(lpparam: XC_LoadPackage.LoadPackageParam, plugins: List?, centers: List) {
+ XposedBridge.log("Wechat XMagicHooker: ${plugins?.size ?: 0} plugins.")
+ MagicGlobal.init(lpparam) {
+ when (it) {
+ true -> {
+ registerPlugins(plugins, centers)
+ registerHookers(plugins)
+ }
+ else ->
+ Log.e(MagicHooker::class.java.name, "查找初始化企微失败")
+ }
+ }
+ }
+
+ private fun registerPlugins(plugins: List?, centers: List) {
+ val observers = plugins?.filter { it !is IHookerProvider } ?: listOf()
+ centers.parallelForEach { center ->
+ center.interfaces.forEach { `interface` ->
+ observers.forEach { plugin ->
+ val assignable = `interface`.isAssignableFrom(plugin::class.java)
+ if (assignable) {
+ center.register(`interface`, plugin)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 检查插件中是否存在自定义的事件, 将它们直接注册到 Xposed 框架上
+ */
+ private fun registerHookers(plugins: List?) {
+ val providers = plugins?.filter { it is IHookerProvider } ?: listOf()
+ providers.parallelForEach {
+ (it as IHookerProvider).provideStaticHookers()?.forEach { hooker ->
+ if (!hooker.hasHooked) {
+ XposedUtil.postHooker(hooker)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/async/AsyncHandler.kt b/kernel/src/main/java/com/magic/kernel/async/AsyncHandler.kt
new file mode 100644
index 0000000..e68ca33
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/async/AsyncHandler.kt
@@ -0,0 +1,122 @@
+package com.magic.kernel.async
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+
+/**
+ * 用于异步处理消息
+ */
+open class AsyncHandler : Handler() {
+ private val mWorkThreadHandler: Handler
+ private var mWorkLooper: Looper? = null
+
+ companion object {
+ const val TAG = "AsyncHandler"
+ }
+
+ /**
+ * 内部工作Handler,用于处理异步消息
+ */
+ private inner class WorkHandler(looper: Looper, private val replyHandler: Handler) :
+ Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ // 处理异步消息
+ onAsyncDeal(msg)
+ // 处理完成后重新将数据发送到之前的线程处理
+ val reply = Message.obtain()
+ reply.copyFrom(msg)
+ reply.target = replyHandler
+ replyHandler.post { onPostComplete(reply) }
+ }
+
+ }
+
+ private fun createHandler(looper: Looper): Handler {
+ return WorkHandler(looper, this)
+ }
+
+ /**
+ * 移除还未开始的操作
+ * @param what msg.what
+ */
+ fun cancelOperation(what: Int) {
+ mWorkThreadHandler.removeMessages(what)
+ }
+
+ /**
+ * 移除还未开始的操作
+ * @param what msg.what
+ * @param obj msg.obj
+ */
+ fun cancelOperation(what: Int, obj: Any?) {
+ mWorkThreadHandler.removeMessages(what, obj)
+ }
+
+ /**
+ * 发送消息
+ * @param msg
+ */
+ fun sendAsyncMessage(msg: Message) {
+ sendAsyncMessageDelay(msg, 0)
+ }
+
+ /**
+ * 延时发送异步消息
+ * @param msg 消息内容
+ * @param delayMillis 多少时长后发送
+ */
+ fun sendAsyncMessageDelay(msg: Message, delayMillis: Long) {
+ val workMsg = Message.obtain()
+ workMsg.copyFrom(msg)
+ mWorkThreadHandler.sendMessageDelayed(workMsg, delayMillis)
+ }
+
+ /**
+ * 延时循环发送异步消息
+ * @param asyncMSg 消息内容等
+ * @param delayMillis 多少时长后发送
+ * @param intervalMillis 间隔多少时长
+ */
+ fun sendScheduleAsyncMessage(msg: Message, delayMillis: Long, intervalMillis: Long) {
+ mWorkThreadHandler.postDelayed({
+ sendAsyncMessage(msg)
+ mWorkThreadHandler.post {
+ sendScheduleAsyncMessage(
+ msg,
+ delayMillis,
+ intervalMillis
+ )
+ }
+ }, delayMillis)
+ }
+
+ /**
+ * 异步处理消息,该消息中所有参数将原本返回至onPostComplete中,也可以在某些处理完成后重赋值该asyncMsg,
+ * 如果需要监听处理完成的方法,请重写onPostComplete
+ * @param msg
+ */
+ protected open fun onAsyncDeal(msg: Message) {}
+
+ /**
+ * 异步处理完成后执行方法
+ * @param msg
+ */
+ protected open fun onPostComplete(msg: Message) {}
+
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ // onAsyncComplete(msg);
+ }
+
+ init {
+ synchronized(AsyncHandler::class.java) {
+ val thread = HandlerThread(TAG)
+ thread.start()
+ mWorkLooper = thread.looper
+ }
+ mWorkThreadHandler = createHandler(mWorkLooper!!)
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/async/CrashHandler.kt b/kernel/src/main/java/com/magic/kernel/async/CrashHandler.kt
new file mode 100644
index 0000000..33f4558
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/async/CrashHandler.kt
@@ -0,0 +1,66 @@
+package com.magic.kernel.async
+
+import com.magic.kernel.cache.LRUCache
+import com.magic.kernel.helper.defaultFormat
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.Date
+import java.util.concurrent.Executors
+
+class CrashHandler: Thread.UncaughtExceptionHandler {
+
+ interface Callback {
+ fun handException(e: Throwable)
+ }
+
+ companion object {
+
+ private var instance: CrashHandler? = null
+
+ fun newInstance(): CrashHandler {
+ if (instance == null) {
+ instance = CrashHandler()
+ }
+ return instance!!
+ }
+ }
+
+ var callback: Callback? = null
+
+ init {
+ Thread.setDefaultUncaughtExceptionHandler(this)
+ }
+
+ override fun uncaughtException(t: Thread, e: Throwable) {
+ var tmpThrowable: Throwable? = null
+ if (e.message == null) {
+ val builder = StringBuilder(256)
+ builder.append("\n")
+ for (element in e.getStackTrace()) {
+ builder.append(element.toString()).append("\n")
+ }
+ tmpThrowable = Throwable(builder.toString(), e)
+ }
+ if (handleException(tmpThrowable ?: e)) callback?.handException(tmpThrowable ?: e)
+ }
+
+ private fun handleException(e: Throwable?): Boolean {
+ return if (e == null) false else try {
+ Executors.newSingleThreadExecutor().submit {
+ try {
+ val file = File(LRUCache.cachePath("crash", "crash_" + Date().defaultFormat() + ".log"))
+ file.createNewFile()
+ val fos = FileOutputStream(file)
+ fos.write(e.message?.toByteArray(Charsets.UTF_8) ?: byteArrayOf())
+ fos.close()
+ } catch (e: IOException) {
+ }
+ }
+ true
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/cache/ByteArrayEntry.kt b/kernel/src/main/java/com/magic/kernel/cache/ByteArrayEntry.kt
new file mode 100644
index 0000000..a885e92
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/cache/ByteArrayEntry.kt
@@ -0,0 +1,24 @@
+package com.magic.kernel.cache
+
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.SoftReference
+
+class ByteArrayEntry(
+ val key: String,
+ value: ByteArray,
+ queue: ReferenceQueue,
+ val timestamp: Long = System.currentTimeMillis()): SoftReference(value, queue) {
+ var size: Long = 0
+
+ init {
+ size = value.size.toLong()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return super.equals(other)
+ }
+
+ override fun hashCode(): Int {
+ return super.hashCode()
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/cache/LRUCache.kt b/kernel/src/main/java/com/magic/kernel/cache/LRUCache.kt
new file mode 100644
index 0000000..061c595
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/cache/LRUCache.kt
@@ -0,0 +1,73 @@
+package com.magic.kernel.cache
+
+import android.os.Environment
+import com.magic.kernel.helper.MD5
+import java.io.File
+
+object LRUCache {
+
+ private const val ROOT_CACHE_DIR = "magic"
+ private var memoryCache = LRUMemoryCache()
+ private var diskCache = LRUDiskCache(getCacheDir())
+
+ fun setup(memoryCacheSize: Long = 0, diskCacheSize: Long = 0) {
+ if (memoryCacheSize != 0L) {
+ memoryCache = LRUMemoryCache()
+ }
+ when (diskCacheSize == 0L) {
+ true -> diskCache = LRUDiskCache(getCacheDir(), diskCacheSize)
+ false -> diskCache = LRUDiskCache(getCacheDir())
+ }
+ }
+
+ fun clearCache(callback: (() -> Unit)? = null) {
+ memoryCache.clearCache()
+ diskCache.clearCache { callback?.invoke() }
+ }
+
+ /** ---- 磁盘缓存 ---- */
+ fun cacheInDisk(dir: String = "", key: String, params: Map? = null, content: ByteArray): String? =
+ diskCache.cache(cachePath(dir, key, params), content)
+
+ fun cacheInDisk(dir: String = "", key: String, params: Map? = null, content: ByteArray, callback: (String?) -> Unit) =
+ diskCache.cache(cachePath(dir, key, params), content, callback)
+
+ fun getFromDisk(dir: String = "", key: String, params: Map? = null): ByteArray? =
+ diskCache.get(cachePath(dir, key, params))
+
+ fun getFromDisk(dir: String = "", key: String, params: Map?, callback: (ByteArray?) -> Unit) =
+ diskCache.get(cachePath(dir, key, params), callback)
+
+ fun cacheDiskPath(dir: String = "", key: String, params: Map? = null): String? =
+ diskCache.exists(cachePath(dir, key, params))
+
+ /** ---- 磁盘缓存 ---- */
+ fun cacheInMemory(key: String, params: Map? = null, content: ByteArray) =
+ memoryCache.cache(realKey(key, params), content)
+
+ fun getEntryFromMemory(key: String, params: Map? = null): ByteArrayEntry? =
+ memoryCache.getEntry(realKey(key, params))
+
+ fun getByteArrayFromMemory(key: String, params: Map? = null): ByteArray? =
+ memoryCache.getByteArray(realKey(key, params))
+
+ fun cachePath(dir: String, key: String, params: Map? = null, md5: Boolean = false): String =
+ getCacheDir(dir) + File.separator + realKey(key, params, md5)
+
+ private fun realKey(key: String, params: Map? = null, md5: Boolean = true): String {
+ val indexOf = key.lastIndexOf(".")
+ val key = when (indexOf > 0) {
+ true -> key.substring(0, indexOf) + (if (params != null) params?.toString() else "") + key.substring(indexOf)
+ false -> key + (if (params != null) params?.toString() else "")
+ }
+ return key.MD5()
+ }
+
+ private fun getCacheDir(dir: String = ""): String {
+ val cacheDir = Environment.getExternalStorageDirectory().path + File.separator + ROOT_CACHE_DIR
+ return when (dir.isNotEmpty()) {
+ true -> "$cacheDir${File.separator}${dir.removeSuffix("/")}"
+ else -> cacheDir
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/cache/LRUDiskCache.kt b/kernel/src/main/java/com/magic/kernel/cache/LRUDiskCache.kt
new file mode 100644
index 0000000..b6236bc
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/cache/LRUDiskCache.kt
@@ -0,0 +1,148 @@
+package com.magic.kernel.cache
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import java.io.*
+import java.util.*
+import java.util.concurrent.*
+import kotlin.collections.LinkedHashMap
+
+/**
+ * 磁盘缓存
+ */
+class LRUDiskCache(val cacheDir: String, val maxCacheSize: Long = 1024 * 1024 * 1024) {
+
+ private var cachedSize: Long = 0
+ private val executorService: ExecutorService =
+ Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors())
+ private val mHandler = Handler(Looper.getMainLooper())
+
+ /** 用于按照最近最久未使用存储文件的 path 及 lastModified */
+ private val linkedHashMap: LinkedHashMap =
+ object : LinkedHashMap(16, 0.75f, true) {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
+ val shouldRemove = cachedSize > maxCacheSize
+ if (shouldRemove) {
+ val file = File(eldest?.key ?: "")
+ deleteFile(file) {}
+ }
+ return shouldRemove
+ }
+ }
+
+ init {
+ val file = File(cacheDir)
+ if (!file.exists()) file.mkdirs()
+ val futureTask = object : FutureTask>(Callable {
+ return@Callable readCache(file)
+ }) {
+ override fun done() {
+ get().forEach { linkedHashMap[it.value] = it.key }
+ }
+ }
+ executorService.submit(futureTask)
+ }
+
+ fun cache(path: String, content: ByteArray): String? =
+ try {
+ val file = File(path).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() }
+ val fileOutputStream = FileOutputStream(file)
+ BufferedOutputStream(fileOutputStream).use { it.write(content) }
+ file.path
+ } catch (e: Throwable) {
+ Log.e(LRUDiskCache::class.java.name, "cache fail: ${e.message}")
+ null
+ }
+
+ fun cache(path: String, content: ByteArray, callback: (String?) -> Unit) {
+ val futureTask = object : FutureTask(Callable { cache(path, content) }) {
+ override fun done() {
+ mHandler.post { callback(get()) }
+ }
+ }
+ executorService.submit(futureTask)
+ }
+
+ fun exists(path: String): String? = if (File(path).exists()) path else null
+
+ fun get(path: String): ByteArray? =
+ try {
+ BufferedInputStream(FileInputStream(File(path))).use { it.readBytes() }
+ } catch (_: Throwable) {
+ null
+ }
+
+ fun get(path: String, callback: (ByteArray?) -> Unit) {
+ val futureTask = object : FutureTask(Callable { get(path) }) {
+ override fun done() {
+ mHandler.post { callback(get()) }
+ }
+ }
+ executorService.submit(futureTask)
+ }
+
+ fun deleteFile(file: File) {
+ if (file.isDirectory) {
+ val files = file.listFiles() ?: arrayOf()
+ for (subFile in files) {
+ deleteFile(subFile)
+ }
+ } else {
+ cachedSize -= if (file.exists()) file.length() else 0
+ linkedHashMap.remove(file.path)
+ file.deleteOnExit()
+ }
+ }
+
+ fun deleteFile(file: File, callback: () -> Unit) {
+ val futureTask = object : FutureTask(Callable { deleteFile(file) }) {
+ override fun done() {
+ mHandler.post { callback() }
+ }
+ }
+ executorService.submit(futureTask)
+ }
+
+ /**
+ * 清除指定目录下的缓存,如果file == null,则清除所有缓存
+ */
+ fun clearCache(file: File? = null, callback: () -> Unit) {
+ val futureTask = object : FutureTask(Callable {
+ if (file == null) {
+ cachedSize = 0
+ File(cacheDir).deleteOnExit()
+ } else {
+ deleteFile(file)
+ }
+ }) {
+ override fun done() {
+ mHandler.post { callback() }
+ }
+ }
+ executorService.submit(futureTask)
+ }
+
+ /**
+ * 初始化缓存,将所有文件信息读取,并根据上次lastModified修改排序
+ */
+ private fun readCache(file: File): TreeMap {
+ val treeMap = TreeMap { o1, o2 -> (o1 - o2).toInt() }
+ if (file.isDirectory) {
+ val files = file.listFiles() ?: arrayOf()
+ for (subFile in files) {
+ treeMap.putAll(readCache(subFile))
+ }
+ } else {
+ treeMap[file.lastModified()] = file.path
+ }
+ return treeMap
+ }
+
+}
+
+
+
+
+
+
diff --git a/kernel/src/main/java/com/magic/kernel/cache/LRUMemoryCache.kt b/kernel/src/main/java/com/magic/kernel/cache/LRUMemoryCache.kt
new file mode 100644
index 0000000..54fd383
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/cache/LRUMemoryCache.kt
@@ -0,0 +1,63 @@
+package com.magic.kernel.cache
+
+import java.lang.ref.ReferenceQueue
+
+/**
+ * LRU 内存缓存
+ */
+class LRUMemoryCache(var maxCacheSize: Long = 256 * 1024 * 1024) {
+
+ private var cachedSize: Long = 0
+
+ private val linkedHashMap: LinkedHashMap
+
+ private val referenceQueue = ReferenceQueue()
+
+ init {
+ linkedHashMap = object : LinkedHashMap(16, 0.75f, true) {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
+ val shouldRemove = cachedSize > maxCacheSize
+ if (shouldRemove) {
+ clearRecycled()
+ System.gc()
+
+ cachedSize -= eldest?.value?.size ?: 0
+ }
+ return shouldRemove
+ }
+ }
+ }
+
+ fun cache(key: String, value: ByteArray) {
+ val entry = ByteArrayEntry(key, value, referenceQueue)
+ cache(key, entry)
+ }
+
+ fun cache(key: String, entry: ByteArrayEntry) {
+ cachedSize = entry.size
+ linkedHashMap[key] = entry
+ }
+
+ fun getEntry(key: String): ByteArrayEntry? = linkedHashMap[key]
+
+ fun getByteArray(key: String): ByteArray? = getEntry(key)?.get()
+
+ fun clearCache() {
+ linkedHashMap.clear()
+ cachedSize = 0
+ System.gc()
+ System.runFinalization()
+ }
+
+ private fun clearRecycled() {
+ var softReference: ByteArrayEntry? = referenceQueue.poll() as? ByteArrayEntry
+ while (softReference != null) {
+ if (softReference.get() == null) {
+ cachedSize -= softReference.size
+ linkedHashMap.remove(softReference.key)
+ }
+ softReference = referenceQueue.poll() as? ByteArrayEntry
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/Clazz.kt b/kernel/src/main/java/com/magic/kernel/core/Clazz.kt
new file mode 100644
index 0000000..e10ba82
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/Clazz.kt
@@ -0,0 +1,46 @@
+package com.magic.kernel.core
+
+object Clazz {
+ val Boolean = Boolean::class.java
+ val File = java.io.File::class.java
+ val FileInputStream = java.io.FileInputStream::class.java
+ val FileOutputStream = java.io.FileOutputStream::class.java
+ val Int = Int::class.java
+ val Short = Short::class.java
+ val Double = Double::class.java
+ val Iterator = java.util.Iterator::class.java
+ val Long = Long::class.java
+ val Map = Map::class.java
+ val Object = Object::class.java
+ val String = String::class.java
+
+ val Activity = android.app.Activity::class.java
+ val Bundle = android.os.Bundle::class.java
+ val Configuration = android.content.res.Configuration::class.java
+ val ContentValues = android.content.ContentValues::class.java
+ val Context = android.content.Context::class.java
+ val ContextMenu = android.view.ContextMenu::class.java
+ val ContextMenuInfo = android.view.ContextMenu.ContextMenuInfo::class.java
+ val HeaderViewListAdapter = android.widget.HeaderViewListAdapter::class.java
+ val Intent = android.content.Intent::class.java
+ val KeyEvent = android.view.KeyEvent::class.java
+ val ListAdapter = android.widget.ListAdapter::class.java
+ val ListView = android.widget.ListView::class.java
+ val Menu = android.view.Menu::class.java
+ val Message = android.os.Message::class.java
+ val MotionEvent = android.view.MotionEvent::class.java
+ val Notification = android.app.Notification::class.java
+ val NotificationManager = android.app.NotificationManager::class.java
+ val View = android.view.View::class.java
+ val ViewGroup = android.view.ViewGroup::class.java
+
+ val Cursor = android.database.Cursor::class.java
+
+ val ByteArray = ByteArray::class.java
+ val IntArray = IntArray::class.java
+ val ShortArray = ShortArray::class.java
+ var LongArray = kotlin.LongArray::class.java
+ val ObjectArray = Array::class.java
+ val StringArray = Array::class.java
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/Hooker.kt b/kernel/src/main/java/com/magic/kernel/core/Hooker.kt
new file mode 100644
index 0000000..593bf8f
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/Hooker.kt
@@ -0,0 +1,25 @@
+package com.magic.kernel.core
+
+/**
+ * 将一次 Hook 操作封装成对象, 防止对同样的函数反复下钩, 造成难以调查的BUG
+ * 这个类是线程安全的, 多个线程同时调用只会有一个线程成功下钩
+ * @property hooker 实际向 Xposed 框架注册钩子的回调函数
+ * @constructor 将一次 Hook 操作封装成一个 Hooker 对象
+ */
+class Hooker(private val hooker: () -> Unit) {
+ /**
+ * 用来防止重复 Hook 的标记
+ */
+ var hasHooked = false
+ private set
+
+ /**
+ * 尝试执行一次 Hook 操作, 如果已经钩过了就不再重复
+ */
+ @Synchronized fun hook() {
+ if (!hasHooked) {
+ hooker()
+ hasHooked = true
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/HookerCenter.kt b/kernel/src/main/java/com/magic/kernel/core/HookerCenter.kt
new file mode 100644
index 0000000..258eedb
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/HookerCenter.kt
@@ -0,0 +1,239 @@
+package com.magic.kernel.core
+
+import android.util.Log
+import com.magic.kernel.utils.XposedUtil
+import de.robv.android.xposed.XC_MethodHook
+import java.util.concurrent.ConcurrentHashMap
+import com.magic.kernel.helper.TryHelper.tryVerbosely
+import com.magic.kernel.helper.ReflecterHelper.findMethodsByExactName
+import com.magic.kernel.utils.ParallelUtil.parallelForEach
+import de.robv.android.xposed.XposedHelpers
+import java.lang.reflect.Method
+
+abstract class HookerCenter : IHookerProvider {
+
+ abstract val interfaces: List>
+
+ private val observers: MutableMap> = ConcurrentHashMap()
+
+ private fun Any.hasEvent(event: String) =
+ this::class.java.declaredMethods.any { it.name == event }
+
+ private fun register(event: String, observer: Any) {
+ if (observer.hasEvent(event)) {
+ val hooker = provideEventHooker(event)
+ if (hooker != null && !hooker.hasHooked) {
+ XposedUtil.postHooker(hooker)
+ }
+ val existing = observers[event] ?: emptySet()
+ observers[event] = existing + observer
+ }
+ }
+
+ fun register(iClazz: Class<*>, plugin: Any) {
+ iClazz.methods.forEach { method ->
+ register(method.name, plugin)
+ }
+ }
+
+ fun findObservers(event: String): Set? = observers[event]
+
+ /**
+ * 通知所有正在观察某个事件的观察者
+ *
+ * @param event 具体发生的事件
+ * @param action 对观察者进行通知的回调函数
+ */
+ inline fun notify(event: String, action: (Any) -> Unit) {
+ findObservers(event)?.forEach {
+ tryVerbosely { action(it) }
+ }
+ }
+
+ fun iMethodNotifyHooker(
+ clazz: Class<*>?, method: Method?,
+ iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null,
+ needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*>
+ ): Hooker =
+ iMethodHooker(
+ clazz, method?.name,
+ iClazz, iMethodBefore, iMethodAfter,
+ needObject, needResult, "notify", *parameterTypes
+ )
+
+ fun iMethodNotifyHooker(
+ clazz: Class<*>?, method: String?,
+ iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null,
+ needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*>
+ ): Hooker =
+ iMethodHooker(
+ clazz, method,
+ iClazz, iMethodBefore, iMethodAfter,
+ needObject, needResult, "notify", *parameterTypes
+ )
+ /**
+ * 通知所有正在观察某个事件的观察者(并行)
+ *
+ * @param event 具体发生的事件
+ * @param action 对观察者进行通知的回调函数
+ */
+ inline fun notifyParallel(event: String, crossinline action: (Any) -> Unit) {
+ findObservers(event)?.parallelForEach {
+ tryVerbosely { action(it) }
+ }
+ }
+
+ /**
+ * 通知所有正在观察某个事件的观察者, 并收集它们的反馈
+ *
+ * @param event 具体发生的事件
+ * @param action 对观察者进行通知的回调函数
+ */
+ inline fun notifyForResults(event: String, action: (Any) -> T?): List {
+ return findObservers(event)?.mapNotNull {
+ tryVerbosely { action(it) }
+ } ?: emptyList()
+ }
+
+ /**
+ * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认是否需要拦截该事件
+ *
+ * 如果有任何一个观察者返回了 true, 我们就认定当前事件是一个需要被拦截的事件. 例如当微信写文件的时候, 某个观察者
+ * 检查过文件路径后返回了 true, 那么框架就会拦截这次写文件操作, 向微信返回一个默认值
+ *
+ * @param event 具体发生的事件
+ * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象
+ * @param default 跳过函数调用之后, 仍然需要向 caller 提供一个返回值
+ * @param action 对观察者进行通知的回调函数
+ */
+ inline fun notifyForBypassFlags(
+ event: String,
+ param: XC_MethodHook.MethodHookParam,
+ default: Any? = null,
+ action: (Any) -> Boolean
+ ) {
+ val shouldBypass = notifyForResults(event, action).any()
+ if (shouldBypass) {
+ param.result = default
+ }
+ }
+
+ fun iMethodNotifyForBypassFlagsHooker(
+ clazz: Class<*>?, method: String?,
+ iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null,
+ needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*>
+ ): Hooker =
+ iMethodHooker(
+ clazz, method,
+ iClazz, iMethodBefore, iMethodAfter,
+ needObject, needResult, "notifyForBypassFlags", *parameterTypes
+ )
+
+
+ /**
+ * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认该对这次事件采取什么操作
+ *
+ * 在获取了观察者建议的操作之后, 我们会对这些操作的优先级进行排序, 从优先级最高的操作中选择一个予以采纳
+ *
+ * @param event 具体发生的事件
+ * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象
+ * @param action 对观察者进行通知的回调函数
+ */
+ inline fun notifyForOperations(
+ event: String,
+ param: XC_MethodHook.MethodHookParam,
+ action: (Any) -> Operation<*>
+ ) {
+ val operations = notifyForResults(event, action)
+ val result = operations.filter { it.returnEarly }.maxBy { it.priority }
+ if (result != null) {
+ if (result.value != null) {
+ param.result = result.value
+ }
+ if (result.error != null) {
+ param.throwable = result.error
+ }
+ }
+ }
+
+ /**
+ * hookMethod
+ */
+ private fun iMethodHooker(
+ clazz: Class<*>?, method: String?,
+ iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null,
+ needObject: Boolean = false, needResult: Boolean = false, notifyType: String = "notify",
+ vararg parameterTypes: Class<*>
+ ): Hooker {
+ return Hooker {
+ if (clazz == null || method == null) return@Hooker
+ XposedHelpers.findAndHookMethod(clazz, method, *parameterTypes,
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam?) {
+ if (iClazz == null || iMethodBefore == null) return
+ iInvoke(iClazz, iMethodBefore, needObject, needResult, param, notifyType)
+ }
+
+ override fun afterHookedMethod(param: MethodHookParam?) {
+ Log.e(HookerCenter::class.java.name, "afterHookedMethod: ${param}")
+ if (iClazz == null || iMethodAfter == null) return
+ iInvoke(iClazz, iMethodAfter, needObject, needResult, param, notifyType)
+ }
+ })
+ }
+ }
+
+ /**
+ * hookMethod
+ */
+ private fun iConstructorHooker(
+ clazz: Class<*>?,
+ iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null,
+ needObject: Boolean = false, needResult: Boolean = false, notifyType: String = "notify",
+ vararg parameterTypes: Class<*>
+ ): Hooker {
+ return Hooker {
+ if (clazz == null) return@Hooker
+ XposedHelpers.findAndHookConstructor(clazz, *parameterTypes,
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam?) {
+ if (iClazz == null || iMethodBefore == null) return
+ iInvoke(iClazz, iMethodBefore, needObject, needResult, param, notifyType)
+ }
+
+ override fun afterHookedMethod(param: MethodHookParam?) {
+ if (iClazz == null || iMethodAfter == null) return
+ Log.e(
+ HookerCenter::class.java.name,
+ "afterHook ${clazz.name} : $iMethodAfter"
+ )
+ iInvoke(iClazz, iMethodAfter, needObject, needResult, param, notifyType)
+ }
+ })
+ }
+ }
+
+ /**
+ * 调用Ixxx回调方法
+ */
+ private fun iInvoke(
+ iClazz: Class<*>, method: String, needObject: Boolean, needResult: Boolean,
+ param: XC_MethodHook.MethodHookParam?, notifyType: String
+ ) {
+ val iMethod = findMethodsByExactName(iClazz, method).firstOrNull()
+ var args = param?.args.orEmpty().toList().toTypedArray().toMutableList()
+ if (needObject && param?.thisObject != null) {
+ args.add(0, param!!.thisObject)
+ }
+ if (needResult) {
+ args.add(param!!.result)
+ }
+ when (notifyType) {
+ "notify" ->
+ notify(method) {
+ iMethod?.invoke(it, *args.toTypedArray())
+ }
+ else -> {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/IHookerProvider.kt b/kernel/src/main/java/com/magic/kernel/core/IHookerProvider.kt
new file mode 100644
index 0000000..f245401
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/IHookerProvider.kt
@@ -0,0 +1,9 @@
+package com.magic.kernel.core
+
+interface IHookerProvider {
+
+ fun provideStaticHookers(): List? = null
+
+ fun provideEventHooker(event: String): Hooker? = null
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/Operation.kt b/kernel/src/main/java/com/magic/kernel/core/Operation.kt
new file mode 100644
index 0000000..7584c5d
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/Operation.kt
@@ -0,0 +1,46 @@
+package com.magic.kernel.core
+
+/**
+ * 当插件监听到某个事件发生, 并拦截到相应的函数调用的时候, 插件可能会需要对拦截住的函数进行某些操作, 这个操作需要被封
+ * 装成一个 [Operation] 对象传递给 SpellBook 框架
+ */
+class Operation(
+ val value: T? = null,
+ val error: Throwable? = null,
+ val priority: Int = 0,
+ val returnEarly: Boolean = false
+) {
+ companion object {
+ /**
+ * 创建一个空操作, 表明自己什么也不做
+ */
+ @JvmStatic
+ fun nop(priority: Int = 0): Operation {
+ return Operation(priority = priority)
+ }
+
+ /**
+ * 创建一个打断操作, 跳过原函数的执行, 直接抛出一个异常
+ *
+ * @param error 要抛出的异常
+ * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的
+ * 情况下随机选择一个操作
+ */
+ @JvmStatic
+ fun interruption(error: Throwable, priority: Int = 0): Operation {
+ return Operation(error = error, priority = priority, returnEarly = true)
+ }
+
+ /**
+ * 创建一个替换操作, 跳过原函数的执行, 直接返回一个结果
+ *
+ * @param value 要返回的结果
+ * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的
+ * 情况下随机选择一个操作
+ */
+ @JvmStatic
+ fun replacement(value: T, priority: Int = 0): Operation {
+ return Operation(value = value, priority = priority, returnEarly = true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/core/Version.kt b/kernel/src/main/java/com/magic/kernel/core/Version.kt
new file mode 100644
index 0000000..cba5d74
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/Version.kt
@@ -0,0 +1,39 @@
+package com.magic.kernel.core
+
+/**
+ * 用于比较 Android 版本字符串的类
+ */
+class Version(private val versionName: String) {
+
+ private val version: List =
+ versionName.split('.').mapNotNull(String::toIntOrNull)
+
+ override fun toString() = versionName
+
+ override fun hashCode(): Int = version.hashCode()
+
+ override fun equals(other: Any?): Boolean = when (other) {
+ null -> false
+ !is Version -> false
+ else -> this.version == other.version
+ }
+
+ operator fun compareTo(other: Version): Int {
+ var result = 0
+ when {
+ this.version.size > other.version.size -> result = 1
+ this.version.size < other.version.size -> result = -1
+ }
+
+ var index = 0
+ while (index < this.version.size && index < other.version.size) {
+ when {
+ this.version[index] > other.version[index] -> return 1
+ this.version[index] < other.version[index] -> return -1
+ }
+ index++
+ }
+
+ return result
+ }
+}
diff --git a/kernel/src/main/java/com/magic/kernel/core/WaitChannel.kt b/kernel/src/main/java/com/magic/kernel/core/WaitChannel.kt
new file mode 100644
index 0000000..e1987ea
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/core/WaitChannel.kt
@@ -0,0 +1,36 @@
+package com.magic.kernel.core
+
+/**
+ * 实现的一个安全的 Wait Channel, 用来让若干线程安全地阻塞到事件结束
+ */
+class WaitChannel {
+ @Volatile private var done = false
+ private val channel = java.lang.Object()
+
+ private val current: Long
+ get() = System.currentTimeMillis()
+
+ fun wait(timeout: Long = 0L): Boolean {
+ if (done) return false
+
+ val start = current
+ synchronized(channel) {
+ // 处理可能的 spurious wakeup
+ while (!done && start + timeout > current) {
+ channel.wait(start + timeout - current)
+ }
+ return true
+ }
+ }
+
+ fun done() {
+ if (done) return
+
+ synchronized(channel) {
+ done = true
+ channel.notifyAll()
+ }
+ }
+
+ fun isDone() = done
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/helper/ExtensionHelper.kt b/kernel/src/main/java/com/magic/kernel/helper/ExtensionHelper.kt
new file mode 100644
index 0000000..fff6a49
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/helper/ExtensionHelper.kt
@@ -0,0 +1,38 @@
+package com.magic.kernel.helper
+
+import java.security.MessageDigest
+import java.text.SimpleDateFormat
+import java.util.*
+
+/** ------- String Extension ------ */
+fun String.firstLetterToLowerCase() =
+ if (this.isEmpty()) this else this.substring(0, 1).toLowerCase() + this.substring(1)
+
+fun String.MD5() = toHexString(MessageDigest.getInstance(("MD5")).digest(this.toByteArray()))
+
+fun String.SHA1() = toHexString(MessageDigest.getInstance("SHA-1").digest(this.toByteArray()))
+
+fun String.SHA256() =toHexString( MessageDigest.getInstance("SHA-256").digest(this.toByteArray()))
+
+/** ------- ByteArray Extension ------ */
+fun ByteArray.MD5() = toHexString(MessageDigest.getInstance("MD5").digest(this))
+
+fun ByteArray.SHA1() = toHexString(MessageDigest.getInstance("SHA-1").digest(this))
+
+fun ByteArray.SHA256() = toHexString(MessageDigest.getInstance("SHA-256").digest(this))
+
+/** ------- Date Extension ------ */
+fun Date.defaultFormat() = SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(this)
+
+fun toHexString(bytes: ByteArray) =
+ with(StringBuffer()) {
+ bytes.forEach {
+ val hex = it.toInt() and 0xFF
+ val hexString = Integer.toHexString(hex)
+ when (hexString.length) {
+ 1 -> this.append("0").append(hexString)
+ else -> this.append(hexString)
+ }
+ }
+ return@with this.toString()
+ }
diff --git a/kernel/src/main/java/com/magic/kernel/helper/ParserHelper.kt b/kernel/src/main/java/com/magic/kernel/helper/ParserHelper.kt
new file mode 100644
index 0000000..8dd3c28
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/helper/ParserHelper.kt
@@ -0,0 +1,333 @@
+package com.magic.kernel.helper
+
+import com.magic.kernel.utils.ParallelUtil.parallelForEach
+import java.io.Closeable
+import java.io.File
+import java.nio.Buffer
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.concurrent.ConcurrentHashMap
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+
+/**
+ * 参考了:https://github.com/Gh0u1L5/WechatSpellbook, 在此基础上修复了当packageName==""时搜索结果错误的问题
+ */
+object ParserHelper {
+
+ class ApkFile(apkFile: File) : Closeable {
+
+ private companion object {
+ private const val DEX_FILE = "classes.dex"
+ private const val DEX_ADDITIONAL = "classes%d.dex"
+ }
+
+ constructor(path: String) : this(File(path))
+
+ private val zipFile: ZipFile = ZipFile(apkFile)
+
+ private fun readEntry(entry: ZipEntry): ByteArray =
+ zipFile.getInputStream(entry).use { it.readBytes() }
+
+ override fun close() = zipFile.close()
+
+ private fun getDexFilePath(idx: Int) =
+ if (idx == 1) DEX_FILE else String.format(DEX_ADDITIONAL, idx)
+
+ private fun isDexFileExist(idx: Int): Boolean {
+ val path = getDexFilePath(idx)
+ return zipFile.getEntry(path) != null
+ }
+
+ private fun readDexFile(idx: Int): ByteArray {
+ val path = getDexFilePath(idx)
+ return readEntry(zipFile.getEntry(path))
+ }
+
+ val classTypes: ClassTrie by lazy {
+ var end = 2
+ while (isDexFileExist(end)) end++
+
+ val ret = ClassTrie()
+ (1 until end).parallelForEach { idx ->
+ val data = readDexFile(idx)
+ val buffer = ByteBuffer.wrap(data)
+ val parser =
+ DexParser(buffer)
+ ret += parser.parseClassTypes()
+ }
+ return@lazy ret.apply { mutable = false }
+ }
+ }
+
+
+ data class DexHeader(
+ var version: Int = 0,
+ var checksum: UInt = 0u,
+ var signature: ByteArray = ByteArray(kSHA1DigestLen),
+ var fileSize: UInt = 0u,
+ var headerSize: UInt = 0u,
+ var endianTag: UInt = 0u,
+ var linkSize: UInt = 0u,
+ var linkOff: UInt = 0u,
+ var mapOff: UInt = 0u,
+ var stringIdsSize: Int = 0,
+ var stringIdsOff: UInt = 0u,
+ var typeIdsSize: Int = 0,
+ var typeIdsOff: UInt = 0u,
+ var protoIdsSize: Int = 0,
+ var protoIdsOff: UInt = 0u,
+ var fieldIdsSize: Int = 0,
+ var fieldIdsOff: UInt = 0u,
+ var methodIdsSize: Int = 0,
+ var methodIdsOff: UInt = 0u,
+ var classDefsSize: Int = 0,
+ var classDefsOff: UInt = 0u,
+ var dataSize: Int = 0,
+ var dataOff: UInt = 0u
+ ) {
+ companion object {
+ const val kSHA1DigestLen = 20
+ }
+ }
+
+
+ class ClassTrie {
+
+ companion object {
+ private fun convertJVMTypeToClassName(type: String) =
+ type.substring(1, type.length - 1).replace('/', '.')
+ }
+
+ @Volatile
+ var mutable = true
+
+ private val root: TrieNode = TrieNode()
+
+ operator fun plusAssign(type: String) {
+ if (mutable) {
+ root.add(convertJVMTypeToClassName(type))
+ }
+ }
+
+ operator fun plusAssign(types: Array) = types.forEach { this += it }
+
+ fun search(packageName: String, depth: Int): List {
+ if (mutable) return emptyList()
+ return root.search(packageName, depth)
+ }
+
+ private class TrieNode {
+ val classes: MutableList = ArrayList(50)
+
+ val children: MutableMap = ConcurrentHashMap()
+
+ fun add(className: String) {
+ add(className, 0)
+ }
+
+ private fun add(className: String, pos: Int) {
+ val delimiterAt = className.indexOf('.', pos)
+ if (delimiterAt == -1) {
+ synchronized(this) {
+ classes.add(className)
+ }
+ return
+ }
+ val pkg = className.substring(pos, delimiterAt)
+ if (pkg !in children) {
+ children[pkg] =
+ TrieNode()
+ }
+ children[pkg]!!.add(className, delimiterAt + 1)
+ }
+
+ fun get(depth: Int = 0): List {
+ if (depth == 0) return classes
+ return children.flatMap { it.value.get(depth - 1) }
+ }
+
+ fun search(packageName: String, depth: Int): List {
+ return search(packageName, depth, 0)
+ }
+
+ private fun search(packageName: String, depth: Int, pos: Int): List {
+ val delimiterAt = packageName.indexOf('.', pos)
+ if (delimiterAt == -1) {
+ return when (packageName.isEmpty()) {
+ true -> classes
+ false -> children[packageName]?.get(depth) ?: emptyList()
+ }
+ }
+ val pkg = packageName.substring(pos, delimiterAt)
+ val next = children[pkg] ?: return emptyList()
+ return next.search(packageName, depth, delimiterAt + 1)
+ }
+ }
+ }
+
+ class DexParser(buffer: ByteBuffer) {
+ private val buffer: ByteBuffer = buffer.duplicate().apply {
+ order(ByteOrder.LITTLE_ENDIAN)
+ }
+
+ private fun ByteBuffer.readBytes(size: Int) = ByteArray(size).also { get(it) }
+
+ fun parseClassTypes(): Array {
+ val magic = String(buffer.readBytes(8))
+ if (!magic.startsWith("dex\n")) {
+ return arrayOf()
+ }
+ val version = Integer.parseInt(magic.substring(4, 7))
+ if (version < 35) {
+ throw Exception("Dex file version: $version is not supported")
+ }
+
+ val header = readDexHeader()
+ header.version = version
+
+ // read string offsets
+ val stringOffsets = readStringOffsets(header.stringIdsOff, header.stringIdsSize)
+
+ // read type ids
+ val typeIds = readTypeIds(header.typeIdsOff, header.typeIdsSize)
+
+ // read class ids
+ val classIds = readClassIds(header.classDefsOff, header.classDefsSize)
+
+
+ // read class types
+ return classIds.map {
+ val typeId = typeIds[it]
+ val offset = stringOffsets[typeId]
+ readStringAtOffset(offset)
+ }.toTypedArray()
+ }
+
+ private fun readDexHeader() = DexHeader().apply {
+ checksum = buffer.int.toUInt()
+
+ buffer.get(signature)
+
+ fileSize = buffer.int.toUInt()
+ headerSize = buffer.int.toUInt()
+
+ endianTag = buffer.int.toUInt()
+
+ linkSize = buffer.int.toUInt()
+ linkOff = buffer.int.toUInt()
+
+ mapOff = buffer.int.toUInt()
+
+ stringIdsSize = buffer.int
+ stringIdsOff = buffer.int.toUInt()
+
+ typeIdsSize = buffer.int
+ typeIdsOff = buffer.int.toUInt()
+
+ protoIdsSize = buffer.int
+ protoIdsOff = buffer.int.toUInt()
+
+ fieldIdsSize = buffer.int
+ fieldIdsOff = buffer.int.toUInt()
+
+ methodIdsSize = buffer.int
+ methodIdsOff = buffer.int.toUInt()
+
+ classDefsSize = buffer.int
+ classDefsOff = buffer.int.toUInt()
+
+ dataSize = buffer.int
+ dataOff = buffer.int.toUInt()
+ }
+
+ private fun readStringOffsets(stringIdsOff: UInt, stringIdsSize: Int): IntArray {
+ (buffer as Buffer).position(stringIdsOff.toInt())
+ return IntArray(stringIdsSize) {
+ buffer.int
+ }
+ }
+
+ private fun readTypeIds(typeIdsOff: UInt, typeIdsSize: Int): IntArray {
+ (buffer as Buffer).position(typeIdsOff.toInt())
+ return IntArray(typeIdsSize) {
+ buffer.int
+ }
+ }
+
+ private fun readClassIds(classDefsOff: UInt, classDefsSize: Int): Array {
+ (buffer as Buffer).position(classDefsOff.toInt())
+ return Array(classDefsSize) {
+ val classIdx = buffer.int
+ // access_flags, skip
+ buffer.int
+ // superclass_idx, skip
+ buffer.int
+ // interfaces_off, skip
+ buffer.int
+ // source_file_idx, skip
+ buffer.int
+ // annotations_off, skip
+ buffer.int
+ // class_data_off, skip
+ buffer.int
+ // static_values_off, skip
+ buffer.int
+
+ classIdx
+ }
+ }
+
+ private fun readStringAtOffset(offset: Int): String {
+ (buffer as Buffer).position(offset)
+ val len = readULEB128Int()
+ return readString(len)
+ }
+
+ private fun readULEB128Int(): Int {
+ var ret = 0
+
+ var count = 0
+ var byte: Int
+ do {
+ if (count > 4) {
+ throw Exception("read varints error.")
+ }
+ byte = buffer.get().toInt()
+ ret = ret or (byte and 0x7f shl count * 7)
+ count++
+ } while (byte and 0x80 != 0)
+
+ return ret
+ }
+
+ private fun readString(len: Int): String {
+ val chars = CharArray(len)
+
+ for (i in 0 until len) {
+ val a = buffer.get().toInt()
+ when {
+ a and 0x80 == 0 -> { // ascii char
+ chars[i] = a.toChar()
+ }
+ a and 0xe0 == 0xc0 -> { // read one more
+ val b = buffer.get().toInt()
+ chars[i] = (a and 0x1F shl 6 or (b and 0x3F)).toChar()
+ }
+ a and 0xf0 == 0xe0 -> {
+ val b = buffer.get().toInt()
+ val c = buffer.get().toInt()
+ chars[i] = (a and 0x0F shl 12 or (b and 0x3F shl 6) or (c and 0x3F)).toChar()
+ }
+ else -> {
+ // throw UTFDataFormatException()
+ }
+ }
+ }
+
+ return String(chars)
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/helper/ReflecterHelper.kt b/kernel/src/main/java/com/magic/kernel/helper/ReflecterHelper.kt
new file mode 100644
index 0000000..f4b4886
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/helper/ReflecterHelper.kt
@@ -0,0 +1,190 @@
+package com.magic.kernel.helper
+
+import android.util.Log
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.helper.ParserHelper.ClassTrie
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * 查找类、方法、属性,用于版本自动匹配
+ */
+object ReflecterHelper {
+
+ private val classCache: MutableMap = ConcurrentHashMap()
+ private val methodCache: MutableMap = ConcurrentHashMap()
+ private val fieldCache: MutableMap = ConcurrentHashMap()
+
+ @JvmStatic
+ fun shadowCopy(obj: Any, copy: Any, clazz: Class<*>? = obj::class.java) {
+ if (clazz == null) return
+ shadowCopy(obj, copy, clazz.superclass)
+ clazz.declaredFields.forEach {
+ it.isAccessible = true
+ it.set(copy, it.get(obj))
+ }
+ }
+
+ /** ------------- Class ------------ */
+ @JvmStatic
+ fun findClassIfExists(className: String, classLoader: ClassLoader): Class<*>? =
+ try {
+ if (classCache.containsKey(className)) {
+ classCache[className]?.firstOrNull()
+ } else {
+ Class.forName(className, false, classLoader).also {
+ classCache[className] = Classes(listOf(it))
+ }
+ }
+ } catch (throwable: Throwable) {
+ if (MagicGlobal.unitTestMode) {
+ throw throwable
+ }
+ null
+ }
+
+ @JvmStatic
+ fun findClassesInPackage(classLoader: ClassLoader, trie: ClassTrie, packageName: String, depth: Int = 0): Classes {
+ val key = "$depth-$packageName"
+ val cached = classCache[key]
+ if (cached != null) {
+ return cached
+ }
+ val classes = Classes(trie.search(packageName, depth).mapNotNull { name ->
+ findClassIfExists(name, classLoader)
+ })
+ return classes
+ }
+
+ /** ------------- Method ------------ */
+ @JvmStatic
+ fun findMethodsByExactName(clazz: Class<*>, methodName: String): Methods {
+ val fullMethodName = "${clazz.name}#$methodName#exactnname"
+ return when (methodCache[fullMethodName] != null) {
+ true -> methodCache[fullMethodName]!!
+ else -> Methods(clazz.declaredMethods.filter { return@filter it.name.equals(methodName, false) }
+ .onEach { it.isAccessible = true })
+ .also { methodCache[fullMethodName] = it }
+ }
+ }
+
+ @JvmStatic
+ fun findMethodIfExists(clazz: Class<*>, methodName: String, vararg parameterTypes: Class<*>): Method? =
+ try {
+ findMethodExact(clazz, methodName, *parameterTypes)
+ } catch (_: Throwable) {
+ null
+ }
+
+ @JvmStatic
+ private fun findMethodExact(clazz: Class<*>, methodName: String, vararg parameterTypes: Class<*>): Method {
+ val fullMethodName = "${clazz.name}#$methodName${getParametersString(*parameterTypes)}#exact"
+ if (fullMethodName in methodCache) {
+ return methodCache[fullMethodName]?.firstOrNull() ?: throw NoSuchMethodError(fullMethodName)
+ }
+ try {
+ val method = clazz.getMethod(methodName, *parameterTypes).apply {
+ isAccessible = true
+ }
+ return method.also { methodCache[fullMethodName] = Methods(listOf(method)) }
+ } catch (e: NoSuchMethodException) {
+ throw NoSuchMethodError(fullMethodName)
+ }
+ }
+
+ @JvmStatic
+ fun findMethodsByExactParameters(clazz: Class<*>, returnType: Class<*>?, vararg parameterTypes: Class<*>): Methods {
+ val fullMethodName = "${clazz.name}#${returnType?.name ?: ""}#${getParametersString(*parameterTypes)}#exactParameters"
+ var methods = clazz.declaredMethods.filter { method ->
+ if (returnType != null && returnType != method.returnType) return@filter false
+
+ val methodParameterTypes = method.parameterTypes
+ if (parameterTypes.size != methodParameterTypes.size) return@filter false
+
+ var match = true
+ for (i in parameterTypes.indices) {
+ if (parameterTypes[i] != methodParameterTypes[i]) {
+ match = false
+ break
+ }
+ }
+ return@filter match
+ }.apply {
+ for (method in this) {
+ method.isAccessible = true
+ Log.e(ReflecterHelper.javaClass.name, "findMethodsByExactParameters:${clazz.name} ${method.name}")
+ }
+ }
+ return Methods(methods)
+ }
+
+ @JvmStatic
+ private fun getParametersString(vararg clazzes: Class<*>): String =
+ "(" + clazzes.joinToString(",") { it.canonicalName ?: "" } + ")"
+
+ /** ------------- Field ------------ */
+ @JvmStatic
+ fun findFieldIfExists(clazz: Class<*>, fieldName: String): Field? {
+ val fullFieldName = "${clazz.name}#$fieldName"
+ return if (fieldCache.containsKey(fullFieldName)) {
+ fieldCache[fullFieldName]?.firstOrNull()
+ } else try {
+ clazz.getDeclaredField(fieldName)
+ } catch (_: NoSuchFieldException) {
+ null
+ }
+ }
+
+ /** 通过其他方式过滤类 */
+ class Classes( val classes: List>) {
+
+ fun filterByInterfaces(vararg interfaceClasses: Class<*>): Classes =
+ Classes(classes.filter {
+ for (i in interfaceClasses.indices) {
+ if (it.interfaces.contains(interfaceClasses[i])) return@filter true
+ }
+ return@filter false
+ })
+
+ fun firstOrNull(): Class<*>? {
+ if (classes.isNotEmpty()) {
+ val names = classes.map { it.canonicalName }
+ Log.e(ReflecterHelper.javaClass.name, "found a signature classes: $names")
+ }
+ return classes.firstOrNull()
+ }
+
+ }
+
+ /** 通过其他方式过滤方法 */
+ class Methods(private val methods: List) {
+
+ fun firstOrNull(): Method? {
+ if (methods.isNotEmpty()) {
+ val names = methods.map { it.name }
+ Log.e(ReflecterHelper.javaClass.name, "found a signature methods: $names")
+ }
+ return methods.firstOrNull()
+ }
+
+ }
+
+ /** 通过其他方式过滤属性 */
+ class Fields(private val fields: List) {
+
+ fun filterByModifiers(vararg modifiers: Int): Fields =
+ if (modifiers.size < 0) this else Fields(fields.filter { it.modifiers == modifiers.reduce { acc, i -> acc.or(i) } })
+
+ fun isNotEmpty(): Boolean = fields.isNotEmpty()
+
+ fun firstOrNull(): Field? {
+ if (fields.isNotEmpty()) {
+ val names = fields.map { it.name }
+ Log.e(ReflecterHelper.javaClass.name, "found a signature fields: $names")
+ }
+ return fields.firstOrNull()
+ }
+ }
+}
diff --git a/kernel/src/main/java/com/magic/kernel/helper/TryHelper.kt b/kernel/src/main/java/com/magic/kernel/helper/TryHelper.kt
new file mode 100644
index 0000000..1671754
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/helper/TryHelper.kt
@@ -0,0 +1,101 @@
+package com.magic.kernel.helper
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.FutureTask
+
+object TryHelper {
+
+ val mExecutorService: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
+ val mHandler: Handler = Handler(Looper.getMainLooper())
+
+ @JvmStatic
+ inline fun trySilently(func: () -> T?): T? =
+ try { func() } catch (t: Throwable) { null }
+
+ @JvmStatic
+ inline fun tryVerbosely(func: () -> T?): T? =
+ try { func() } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryVerbosely error: $t ${t.message}"); null
+ }
+
+ @JvmStatic
+ inline fun tryMainThreadly(delayMillis: Long = 0, crossinline func: () -> T?) =
+ mHandler.postDelayed({
+ try { func() } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryMainThreadly error: ${t.message}")
+ }
+ }, delayMillis)
+
+ @JvmStatic
+ inline fun tryMainThreadly(delayMillis: Long = 0, crossinline func: () -> T?, crossinline callback: (T?) -> Unit) =
+ mHandler.postDelayed({
+ callback(try { func() } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryMainThreadly callback error: ${t.message}"); null
+ })
+ }, delayMillis)
+
+ @JvmStatic
+ inline fun tryAsynchronously(crossinline func: () -> T?) =
+ mExecutorService.submit {
+ try { func() } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryMainThreadly error: ${t.message}")
+ }
+ }
+
+ @JvmStatic
+ inline fun tryAsynchronously(crossinline func: () -> T?, crossinline callback: (T?) -> Unit) {
+ val futureTask = object : FutureTask(Callable {
+ return@Callable try { func() } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryAsynchronously callback error: ${t.message}"); null
+ }
+ }) {
+ override fun done() {
+ callback(get())
+ }
+ }
+ mExecutorService.submit(futureTask)
+ }
+
+ @JvmStatic
+ inline fun tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair) =
+ mExecutorService.submit {
+ var currentTimes = 0
+ try {
+ while (currentTimes <= retryTimes) {
+ val result = func()
+ currentTimes = if (result.first) retryTimes + 1 else currentTimes + 1
+ }
+ } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryAsynchronously retryTimes error: ${t.message}")
+ }
+ }
+
+ @JvmStatic
+ inline fun tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair, crossinline callback: (T?) -> Unit){
+ val futureTask = object : FutureTask(Callable {
+ var currentTimes = 0
+ var t: T? = null
+ try {
+ while (currentTimes <= retryTimes) {
+ val result = func()
+ t = result.second
+ currentTimes = if (result.first) retryTimes + 1 else currentTimes + 1
+ }
+ } catch (t: Throwable) {
+ Log.e(TryHelper.javaClass.name, "tryAsynchronously retryTimes callback error: ${t.message}")
+ }
+ return@Callable t
+ }) {
+ override fun done() {
+ callback(get())
+ }
+ }
+ mExecutorService.submit(futureTask)
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/media/audio/AudioHelper.kt b/kernel/src/main/java/com/magic/kernel/media/audio/AudioHelper.kt
new file mode 100644
index 0000000..c6d2881
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/media/audio/AudioHelper.kt
@@ -0,0 +1,43 @@
+package com.magic.kernel.media.audio
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import com.magic.kernel.BuildConfig
+import java.io.File
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.FutureTask
+
+object AudioHelper {
+
+ private val mExecutorService: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
+ private val mHandler: Handler = Handler(Looper.getMainLooper())
+
+ /**
+ * 将文件编码成pcm音频源文件
+ */
+ fun encodeToPcm(sourcePath: String, destPath: String, start: Long = 0, callback: ((Boolean) -> Unit)? = null) {
+ }
+
+ /**
+ * 将文件编码成silk音频文件
+ */
+ fun encodeToSilk(sourcePath: String, destPath: String, start: Long, callback: ((Boolean) -> Unit)? = null) {
+ }
+
+ /**
+ * 解码silk音频格式文件到pcm
+ */
+ fun decodeSilkToPcm(sourcePath: String, destPath: String, callback: ((Boolean) -> Unit)? = null) {
+ }
+
+ /**
+ * 解码silk音频到Amr
+ */
+// fun decodeSilkToAmr(sourcePath: String, destPath: String, callback: ((Boolean) -> Unit)? = null) {
+//
+// }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/media/audio/MediaCodecHelper.kt b/kernel/src/main/java/com/magic/kernel/media/audio/MediaCodecHelper.kt
new file mode 100644
index 0000000..4c7d960
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/media/audio/MediaCodecHelper.kt
@@ -0,0 +1,108 @@
+package com.magic.kernel.media.audio
+
+import android.media.MediaCodec
+import android.media.MediaExtractor
+import android.media.MediaFormat
+import android.util.Log
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+/**
+ * @property path 音频原始路径
+ */
+class MediaCodecHelper(filePath: String) {
+ private val mMediaExtractor = MediaExtractor()
+ private var mMediaCodecDecoder: MediaCodec? = null
+ private var mMediaCodecEncoder: MediaCodec? = null
+ var mSampleRate = 8000
+ var mBitRate = 8000
+
+ companion object {
+ const val TIMEOUT_US = 100L
+ }
+
+ init {
+ try {
+ mMediaExtractor.setDataSource(filePath)
+ } catch (t: Throwable) {
+ Log.e(MediaCodecHelper.javaClass.name, "parseFrom t1: ${t.message}")
+ try {
+ mMediaExtractor.setDataSource(FileInputStream(filePath).fd)
+ } catch (t: Throwable) {
+ Log.e(MediaCodecHelper.javaClass.name, "parseFrom t1: ${t.message}")
+ }
+ }
+
+ // 音频媒体轨道只有一条,大于的则表示不是单一音频
+ if (mMediaExtractor.trackCount > 1) {
+ Log.e(MediaCodecHelper.javaClass.name, "parseFrom trackCount: ${mMediaExtractor.trackCount}")
+ }
+ }
+
+ fun initDecoder() {
+ for (i in 0..mMediaExtractor.trackCount) {
+ val mediaFormat = mMediaExtractor.getTrackFormat(i)
+ var mime = mediaFormat.getString(MediaFormat.KEY_MIME)
+ if (mime.equals("audio/ffmpeg", true)) {
+ mime = MediaFormat.MIMETYPE_AUDIO_MPEG
+ }
+ mediaFormat.setString(MediaFormat.KEY_MIME, mime)
+ if (mime.startsWith("audio", true)) {
+ mMediaExtractor.selectTrack(i)
+ mMediaCodecDecoder = MediaCodec.createDecoderByType(mime)
+ mMediaCodecDecoder?.configure(mediaFormat, null, null, 0); break
+ }
+ }
+ }
+
+ /**
+ * @param destPath 解码到文件
+ * @param start 从某个点开始
+ */
+ fun decode(destPath: String, start: Long = 0): Boolean {
+ if (mMediaCodecDecoder == null) return false
+ val decoder = mMediaCodecDecoder!!
+ if (start > 0) {
+ mMediaExtractor.seekTo(start * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
+ }
+ decoder.start()
+ var info = MediaCodec.BufferInfo()
+ var isEOS = false
+ val file = File(destPath).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() }
+ val fileOutputStream = FileOutputStream(file)
+ while (!isEOS) {
+ try {
+ val inIndex = decoder.dequeueInputBuffer(TIMEOUT_US)
+ if (inIndex > 0) {
+ when (val outIndex = decoder.dequeueOutputBuffer(info, TIMEOUT_US)) {
+ }
+ if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
+ fileOutputStream.close()
+ Log.e(MediaCodecHelper::class.java.name, "解码完成: BUFFER_FLAG_END_OF_STREAM")
+ break
+ }
+ }
+ } catch (e: Throwable) {
+ return false
+ }
+ }
+ return true
+ }
+
+ fun initEncoder() {
+
+ }
+
+ // 先默认转换为wav
+ fun encode(mime: String, destPath: String) {
+
+ }
+
+ fun destory() {
+ mMediaCodecDecoder?.stop()
+ mMediaCodecDecoder?.release()
+ mMediaExtractor?.release()
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/media/image/BitmapHelper.kt b/kernel/src/main/java/com/magic/kernel/media/image/BitmapHelper.kt
new file mode 100644
index 0000000..d2859a7
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/media/image/BitmapHelper.kt
@@ -0,0 +1,256 @@
+package com.magic.kernel.media.image
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.BitmapFactory.Options
+import android.graphics.Matrix
+import android.media.ExifInterface
+import android.util.Log
+import com.magic.kernel.cache.LRUCache
+import java.io.*
+import java.nio.ByteBuffer
+import java.nio.channels.Channels
+import java.nio.channels.ReadableByteChannel
+
+object BitmapHelper {
+
+ /**
+ * 获取到图片的方向
+ * @param path 图片路径
+ * @return
+ */
+ fun getDegress(path: String?): Float {
+ var degree = 0F
+ try {
+ val exifInterface = ExifInterface(path)
+ val orientation: Int = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+ when (orientation) {
+ ExifInterface.ORIENTATION_ROTATE_90 -> degree = 90F
+ ExifInterface.ORIENTATION_ROTATE_180 -> degree = 180F
+ ExifInterface.ORIENTATION_ROTATE_270 -> degree = 270F
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ return degree
+ }
+
+ /**
+ * 旋转图片
+ * @param bitmap 图片
+ * @param degress 旋转角度
+ * @return
+ */
+ fun rotateBitmap(bitmap: Bitmap?, degress: Float): Bitmap? {
+ var bitmap: Bitmap? = bitmap
+ if (bitmap != null) {
+ val m = Matrix()
+ m.postRotate(degress)
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, m, true)
+ return bitmap
+ }
+ return bitmap
+ }
+
+ /**
+ * 计算需要缩放的SampleSize
+ * @param options
+ * @param rqsW
+ * @param rqsH
+ * @return
+ */
+ fun caculateInSampleSize(options: Options, rqsW: Int, rqsH: Int): Int {
+ val height: Int = options.outHeight
+ val width: Int = options.outWidth
+ var inSampleSize = 1
+ if (rqsW == 0 || rqsH == 0) return 1
+ if (height > rqsH || width > rqsW) {
+ val heightRatio: Int = Math.round(height.toFloat() / rqsH.toFloat())
+ val widthRatio: Int = Math.round(width.toFloat() / rqsW.toFloat())
+ inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
+ }
+ return inSampleSize
+ }
+
+ /**
+ * 压缩指定路径的图片,并得到图片对象
+ * @param path
+ * @param rqsW
+ * @param rqsH
+ * @return
+ */
+ fun compressBitmap(path: String?, rqsW: Int, rqsH: Int): Bitmap {
+ val options = Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeFile(path, options)
+ options.inSampleSize = caculateInSampleSize(options, rqsW, rqsH)
+ options.inJustDecodeBounds = false
+ return BitmapFactory.decodeFile(path, options)
+ }
+
+ /**
+ *
+ * @param descriptor
+ * @param resW
+ * @param resH
+ * @return
+ */
+ fun compressBitmap(descriptor: FileDescriptor?, resW: Int, resH: Int): Bitmap {
+ val options = Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeFileDescriptor(descriptor, null, options)
+ options.inSampleSize = caculateInSampleSize(options, resW, resH)
+ options.inJustDecodeBounds = false
+ return BitmapFactory.decodeFileDescriptor(descriptor, null, options)
+ }
+
+ /**
+ * 压缩指定路径图片,并将其保存在缓存目录中,通过isDelSrc判定是否删除源文件,并获取到缓存后的图片路径
+ * @param srcPath
+ * @param rqsW
+ * @param rqsH
+ * @param isDelSrc
+ * @return
+ */
+ fun compressBitmap(srcPath: String?, rqsW: Int, rqsH: Int, isDelSrc: Boolean): String? {
+ var bitmap: Bitmap? = compressBitmap(srcPath, rqsW, rqsH)
+ val srcFile = File(srcPath)
+ val desPath: String = LRUCache.cachePath("image", srcFile.name)
+ val degree = getDegress(srcPath)
+ return try {
+ if (degree != 0F) bitmap = rotateBitmap(bitmap, degree)
+ val file = File(desPath)
+ val fos = FileOutputStream(file)
+ bitmap!!.compress(Bitmap.CompressFormat.PNG, 70, fos)
+ fos.close()
+ if (isDelSrc) srcFile.deleteOnExit()
+ desPath
+ } catch (e: Exception) {
+ Log.d(BitmapHelper.javaClass.name, "compressBitmap error: ${e.message}"); null
+ }
+ }
+
+ /**
+ * 压缩某个输入流中的图片,可以解决网络输入流压缩问题,并得到图片对象
+ * @param is
+ * @param reqsW
+ * @param reqsH
+ * @return
+ */
+ fun compressBitmap(`is`: InputStream, reqsW: Int, reqsH: Int): Bitmap? {
+ return try {
+ val baos = ByteArrayOutputStream()
+ val channel: ReadableByteChannel = Channels.newChannel(`is`)
+ val buffer: ByteBuffer = ByteBuffer.allocate(1024)
+ while (channel.read(buffer) !== -1) {
+ buffer.flip()
+ while (buffer.hasRemaining()) baos.write(buffer.array())
+ buffer.clear()
+ }
+ val bts: ByteArray = baos.toByteArray()
+ val bitmap: Bitmap = compressBitmap(bts, reqsW, reqsH)
+ `is`.close()
+ channel.close()
+ baos.close()
+ bitmap
+ } catch (e: Exception) {
+ Log.d(BitmapHelper.javaClass.name, "compressBitmap-is-reqsw-reqsh error: ${e.message}")
+ null
+ }
+ }
+
+ /**
+ * 压缩制定byte[]图片,并得到压缩后的图像
+ * @param bts
+ * @param reqsW
+ * @param reqsH
+ * @return
+ */
+ fun compressBitmap(bts: ByteArray, reqsW: Int, reqsH: Int): Bitmap {
+ val options = Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeByteArray(bts, 0, bts.size, options)
+ options.inSampleSize = caculateInSampleSize(options, reqsW, reqsH)
+ options.inJustDecodeBounds = false
+ return BitmapFactory.decodeByteArray(bts, 0, bts.size, options)
+ }
+
+ /**
+ * 压缩已存在的图片对象,并返回压缩后的图片
+ * @param bitmap
+ * @param reqsW
+ * @param reqsH
+ * @return
+ */
+ fun compressBitmap(bitmap: Bitmap, reqsW: Int, reqsH: Int): Bitmap {
+ return try {
+ val baos = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
+ val bts: ByteArray = baos.toByteArray()
+ val res: Bitmap = compressBitmap(bts, reqsW, reqsH)
+ baos.close()
+ res
+ } catch (e: IOException) {
+ Log.d(BitmapHelper.javaClass.name, "compressBitmap-is-reqsw-reqsh error: ${e.message}")
+ bitmap
+ }
+ }
+
+ /**
+ * 压缩资源图片,并返回图片对象
+ * @param res [Resources]
+ * @param resID
+ * @param reqsW
+ * @param reqsH
+ * @return
+ */
+ fun compressBitmap(res: Resources?, resID: Int, reqsW: Int, reqsH: Int): Bitmap {
+ val options = Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeResource(res, resID, options)
+ options.inSampleSize = caculateInSampleSize(options, reqsW, reqsH)
+ options.inJustDecodeBounds = false
+ return BitmapFactory.decodeResource(res, resID, options)
+ }
+
+ /**
+ * 基于质量的压缩算法, 此方法未 解决压缩后图像失真问题
+ *
可先调用比例压缩适当压缩图片后,再调用此方法可解决上述问题
+ * @param bitmap
+ * @param maxBytes
+ * @return
+ */
+ fun compressBitmap(bitmap: Bitmap, maxBytes: Long): Bitmap? {
+ return try {
+ val baos = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
+ var options = 90
+ while (baos.toByteArray().size > maxBytes) {
+ baos.reset()
+ bitmap.compress(Bitmap.CompressFormat.PNG, options, baos)
+ options -= 10
+ }
+ val bts: ByteArray = baos.toByteArray()
+ val bmp: Bitmap = BitmapFactory.decodeByteArray(bts, 0, bts.size)
+ baos.close()
+ bmp
+ } catch (e: IOException) {
+ Log.d(BitmapHelper.javaClass.name, "compressBitmap-bitmap-maxbytes error: ${e.message}")
+ null
+ }
+ }
+
+ /**
+ * 得到制定路径图片的options
+ * @param srcPath
+ * @return Options [Options]
+ */
+ fun getBitmapOptions(srcPath: String?): Options {
+ val options = Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeFile(srcPath, options)
+ return options
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/okhttp/HttpClients.kt b/kernel/src/main/java/com/magic/kernel/okhttp/HttpClients.kt
new file mode 100644
index 0000000..e770adf
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/okhttp/HttpClients.kt
@@ -0,0 +1,49 @@
+package com.magic.kernel.okhttp
+
+import android.util.Log
+import com.magic.kernel.cache.LRUCache
+import com.magic.kernel.helper.MD5
+import com.magic.kernel.helper.TryHelper.tryMainThreadly
+import okhttp3.*
+import java.io.IOException
+
+object HttpClients {
+
+ /**
+ * 下载资源文件,这里由于企业微信发送文件必须是本地路径,故缓存策略固定为DISK
+ * @param urlString 下载地址
+ * @param userInfo 用户传递的其他数据,将会在回调中原样返回
+ * @param type 文件类型
+ * @param iDownloadCallback 下载回调
+ * @param iProgressRequestCallback 上传进度
+ * @param iProgressResponseCallback 下载进度
+ */
+ fun download(
+ urlString: String, type: IHttpConfigs.Type = IHttpConfigs.Type.DEFAULT,
+ iDownloadCallback: IDownloadCallback,
+ iProgressRequestCallback: IProgressRequestCallback? = null,
+ iProgressResponseCallback: IProgressResponseCallback? = null
+ ) {
+ val clientBuilder = OkHttpClient.Builder()
+ .addInterceptor(Interceptors.getRetryInterceptor())
+ .addInterceptor(Interceptors.getCacheInterceptor(IHttpConfigs.CachePolicy.DISK, type))
+
+ val request = Request.Builder().url(urlString).build()
+ clientBuilder.build().newCall(request).enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ tryMainThreadly {
+ iDownloadCallback(null, type)
+ }
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ Log.e(HttpClients.javaClass.name, "onResponse : ${response.message}")
+ val cacheKey = call.request().url.toString().MD5()
+ tryMainThreadly {
+ iDownloadCallback(LRUCache.cacheDiskPath(type.value, cacheKey), type)
+ }
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/okhttp/ICallbacks.kt b/kernel/src/main/java/com/magic/kernel/okhttp/ICallbacks.kt
new file mode 100644
index 0000000..bf06608
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/okhttp/ICallbacks.kt
@@ -0,0 +1,14 @@
+package com.magic.kernel.okhttp
+
+/** 上传进度回调 */
+typealias IProgressRequestCallback = (bytesWriten: Long, bytesTotal: Long, done: Boolean) -> Unit
+
+/** 下载进度回调 */
+typealias IProgressResponseCallback = (bytesRead: Long, bytesTotal: Long, done: Boolean) -> Unit
+
+/** 下载回调 */
+typealias IDownloadCallback = (localPath: String?, type: IHttpConfigs.Type) -> Unit
+
+/** 下载回调 */
+typealias IDownloadCallback2 = (bArr: ByteArray?, localPath: String?, type: IHttpConfigs.Type) -> Unit
+
diff --git a/kernel/src/main/java/com/magic/kernel/okhttp/IHttpConfigs.kt b/kernel/src/main/java/com/magic/kernel/okhttp/IHttpConfigs.kt
new file mode 100644
index 0000000..0ce04b4
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/okhttp/IHttpConfigs.kt
@@ -0,0 +1,24 @@
+package com.magic.kernel.okhttp
+
+interface IHttpConfigs {
+
+ /** 缓存策略 */
+ enum class CachePolicy {
+ NONE, MEMORY, DISK, ALL
+ }
+
+ /** 文件类型 */
+ enum class Type(var value: String) {
+ DEFAULT("file"),
+ FILE("file"),
+ IMAGE("image"),
+ VOICE("voice"),
+ VIDEO("video")
+ }
+
+ /** 请求方法 常用配置 */
+ enum class HttpMethod {
+ GET, POST, DELETE, PUT,
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/okhttp/Interceptors.kt b/kernel/src/main/java/com/magic/kernel/okhttp/Interceptors.kt
new file mode 100644
index 0000000..57c7cee
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/okhttp/Interceptors.kt
@@ -0,0 +1,81 @@
+package com.magic.kernel.okhttp
+
+import com.magic.kernel.cache.LRUCache
+import com.magic.kernel.helper.MD5
+import okhttp3.Interceptor
+import okhttp3.Protocol
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+
+object Interceptors {
+
+ /** 重试 */
+ fun getRetryInterceptor(maxRetryTimes: Int = 3): Interceptor =
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ var retryTimes = 0
+
+ val request = chain.request()
+ var response = chain.proceed(request)
+
+ while (!response.isSuccessful && retryTimes < maxRetryTimes) {
+ retryTimes += 1
+ response = chain.proceed(request)
+ }
+ return response
+ }
+ }
+
+ /**
+ * 缓存拦截器
+ * @param cachePolicy IConfigs.CachePolicy
+ * @param type IConfigs.Type
+ */
+ fun getCacheInterceptor(cachePolicy: IHttpConfigs.CachePolicy = IHttpConfigs.CachePolicy.ALL, type: IHttpConfigs.Type = IHttpConfigs.Type.DEFAULT) =
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val cacheKey = chain.request().url.toString().MD5()
+ var cacheBytes: ByteArray? =
+ when (cachePolicy) {
+ IHttpConfigs.CachePolicy.NONE -> null
+ IHttpConfigs.CachePolicy.MEMORY -> LRUCache.getByteArrayFromMemory(cacheKey)
+ IHttpConfigs.CachePolicy.DISK -> LRUCache.getFromDisk(type.value, cacheKey)
+ IHttpConfigs.CachePolicy.ALL -> {
+ var bytes = LRUCache.getByteArrayFromMemory(cacheKey)
+ when (bytes == null) {
+ true -> LRUCache.getFromDisk(type.name, cacheKey)
+ false -> bytes
+ }
+ }
+ }
+
+ return when (cacheBytes != null) {
+ true -> {
+ Response.Builder()
+ .request(chain.request())
+ .protocol(Protocol.HTTP_1_0)
+ .code(200)
+ .message("cache response success")
+ .body(cacheBytes.toResponseBody())
+ .build()
+ }
+ false -> {
+ val response = chain.proceed(chain.request())
+ val bytes = response.body?.bytes()
+ if (bytes != null) {
+ when (cachePolicy) {
+ IHttpConfigs.CachePolicy.NONE -> {}
+ IHttpConfigs.CachePolicy.MEMORY -> LRUCache.cacheInMemory(cacheKey, content = bytes)
+ IHttpConfigs.CachePolicy.DISK -> LRUCache.cacheInDisk(type.value, cacheKey, content = bytes)
+ IHttpConfigs.CachePolicy.ALL -> {
+ LRUCache.cacheInMemory(cacheKey, content = bytes)
+ LRUCache.cacheInDisk(type.value, cacheKey, content = bytes)
+ }
+ }
+ }
+ response
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/utils/CmdUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/CmdUtil.kt
new file mode 100644
index 0000000..54b9c76
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/CmdUtil.kt
@@ -0,0 +1,86 @@
+package com.magic.kernel.utils
+
+import java.io.BufferedReader
+import java.io.DataOutputStream
+import java.io.IOException
+import java.io.InputStreamReader
+
+object CmdUtil {
+
+ private const val CMD_SU = "su"
+ private const val CMD_SH = "sh"
+ private const val CMD_EXIT = "exit\n"
+ private const val CMD_LINE_END = "\n"
+
+ data class Result(var result: Int, var successMsg: String? = null, var errorMsg: String? = null)
+
+ val isRoot: Boolean
+ get() = exec(command = "echo root", isNeedResultMsg = false).result == 0
+
+ fun exec(command: String, isRoot: Boolean = true, isNeedResultMsg: Boolean = true): Result =
+ exec(listOf(command), isRoot, isNeedResultMsg)
+
+ fun exec(commands: List?, isRoot: Boolean = true, isNeedResultMsg: Boolean = true): Result =
+ exec((commands ?: listOf()).toTypedArray(), isRoot, isNeedResultMsg)
+
+ fun exec(commands: Array?, isRoot: Boolean, isNeedResultMsg: Boolean = true): Result {
+ var result = -1
+ if (commands == null || commands.isEmpty()) {
+ return Result(result, null, null)
+ }
+ var process: Process? = null
+ var successResult: BufferedReader? = null
+ var errorResult: BufferedReader? = null
+ var successMsg: StringBuilder? = null
+ var errorMsg: StringBuilder? = null
+ var os: DataOutputStream? = null
+ try {
+ process = Runtime.getRuntime().exec(if (isRoot) CMD_SU else CMD_SH)
+ os = DataOutputStream(process.outputStream)
+ for (command in commands) {
+ if (command == null) {
+ continue
+ }
+ // donnot use os.writeBytes(commmand), avoid chinese charset error
+ os.write(command.toByteArray())
+ os.writeBytes(CMD_LINE_END)
+ os.flush()
+ }
+ os.writeBytes(CMD_EXIT)
+ os.flush()
+ result = process.waitFor()
+ // get command result
+ if (isNeedResultMsg) {
+ successMsg = StringBuilder()
+ errorMsg = StringBuilder()
+ successResult = BufferedReader(InputStreamReader(process.inputStream))
+ errorResult = BufferedReader(InputStreamReader(process.errorStream))
+ var s: String?
+ while (successResult.readLine().also { s = it } != null) {
+ successMsg.append(s)
+ }
+ while (errorResult.readLine().also { s = it } != null) {
+ errorMsg.append(s)
+ }
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ try {
+ os?.close()
+ successResult?.close()
+ errorResult?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ process?.destroy()
+ }
+ return Result(
+ result,
+ successMsg?.toString(),
+ errorMsg?.toString()
+ )
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/utils/FileUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/FileUtil.kt
new file mode 100644
index 0000000..67871e2
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/FileUtil.kt
@@ -0,0 +1,241 @@
+package com.magic.kernel.utils
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Environment
+import android.os.SystemClock
+import de.robv.android.xposed.XposedBridge
+import java.io.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+object FileUtil {
+
+ @JvmStatic
+ private fun isAccessExternal(context: Context): Boolean =
+ (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
+ && context.externalCacheDir != null)
+
+ @JvmStatic
+ fun getCacheDir(context: Context): String {
+ val cacheDir = File(if (isAccessExternal(context)) context.externalCacheDir?.path else context.cacheDir?.path)
+ if (!cacheDir.exists()) cacheDir.mkdirs()
+ return cacheDir.path
+ }
+
+ @JvmStatic
+ fun mkDirIfNotExists(context: Context, dirPath: String?): String {
+ val cacheDir = getCacheDir(context)
+ val file = File(cacheDir, dirPath)
+ if (!file.exists()) file.mkdirs()
+ return file.path
+ }
+
+ @JvmStatic
+ fun writeBytesToDisk(path: String, content: ByteArray) {
+ val file = File(path).also { it.parentFile.mkdirs() }
+ val fout = FileOutputStream(file)
+ BufferedOutputStream(fout).use { it.write(content) }
+ }
+
+ @JvmStatic
+ fun readBytesFromDisk(path: String): ByteArray {
+ val fin = FileInputStream(path)
+ return BufferedInputStream(fin).use { it.readBytes() }
+ }
+
+ @JvmStatic
+ fun writeObjectToDisk(path: String, obj: Serializable) {
+ val out = ByteArrayOutputStream()
+ ObjectOutputStream(out).use {
+ it.writeObject(obj)
+ }
+ writeBytesToDisk(path, out.toByteArray())
+ }
+
+ @JvmStatic
+ fun readObjectFromDisk(path: String): Any? {
+ val bytes = readBytesFromDisk(path)
+ val ins = ByteArrayInputStream(bytes)
+ return ObjectInputStream(ins).use {
+ it.readObject()
+ }
+ }
+
+ @JvmStatic
+ fun writeInputStreamToDisk(path: String, ins: InputStream, bufferSize: Int = 8192) {
+ val file = File(path)
+ file.parentFile.mkdirs()
+ val fout = FileOutputStream(file)
+ BufferedOutputStream(fout).use {
+ val buffer = ByteArray(bufferSize)
+ var length = ins.read(buffer)
+ while (length != -1) {
+ it.write(buffer, 0, length)
+ length = ins.read(buffer)
+ }
+ }
+ }
+
+ @JvmStatic
+ fun writeBitmapToDisk(path: String, bitmap: Bitmap) {
+ val out = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+ writeBytesToDisk(path, out.toByteArray())
+ }
+
+ @JvmStatic
+ inline fun writeOnce(path: String, writeCallback: (String) -> Unit) {
+ val file = File(path)
+ if (!file.exists()) {
+ writeCallback(path)
+ return
+ }
+ val bootAt = System.currentTimeMillis() - SystemClock.elapsedRealtime()
+ val modifiedAt = file.lastModified()
+ if (modifiedAt < bootAt) {
+ writeCallback(path)
+ }
+ }
+
+ @JvmStatic
+ fun createTimeTag(): String {
+ val formatter = SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.getDefault())
+ return formatter.format(Calendar.getInstance().time)
+ }
+
+ @JvmStatic
+ fun notifyNewMediaFile(path: String, context: Context?) {
+ val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
+ context?.sendBroadcast(intent.apply {
+ data = Uri.fromFile(File(path))
+ })
+ }
+
+ fun copyAssets(context: Context, appDir: String, dir: String, cover: Boolean = false) {
+ val assetManager = context.assets
+ var files: Array? = null
+ try {
+ files = assetManager.list(dir)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+
+ if (files != null) {
+ File(appDir + File.separator + dir + File.separator).mkdirs()
+ for (filename in files) {
+ var `in`: InputStream? = null
+ var out: OutputStream? = null
+ try {
+ `in` = assetManager.open(dir + File.separator + filename)
+ val outFile = File(appDir + File.separator + dir + File.separator + filename)
+ if (outFile.exists()) {
+ if (!cover) {
+ continue
+ } else {
+ outFile.delete()
+ }
+ }
+ out = FileOutputStream(outFile)
+ copyFile(`in`, out)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ if (`in` != null) {
+ try {
+ `in`!!.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+
+ }
+ if (out != null) {
+ try {
+ out!!.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ fun copyFile(`in`: InputStream, out: OutputStream) {
+ val buffer = ByteArray(1024)
+ var read: Int
+ do {
+ read = `in`.read(buffer)
+ if (read == -1) {
+ break
+ }
+ out.write(buffer, 0, read)
+ } while (true)
+ }
+
+ fun write(fileName: String, content: String, append: Boolean = false) {
+ var writer: FileWriter? = null
+ try {
+ // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
+ writer = FileWriter(fileName, append)
+ writer.write(content)
+ } catch (e: IOException) {
+ XposedBridge.log(e)
+ } finally {
+ try {
+ writer?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ fun bytesToFile(bytes: ByteArray?, result: File?) {
+ val bos =
+ BufferedOutputStream(FileOutputStream(result))
+ bos.write(bytes)
+ bos.flush()
+ bos.close()
+ }
+
+ @Throws(IOException::class)
+ fun toByteArray(input: InputStream): ByteArray {
+ val output = ByteArrayOutputStream()
+ copy(input, output)
+ return output.toByteArray()
+ }
+
+ @Throws(IOException::class)
+ fun copy(input: InputStream, output: OutputStream): Int {
+ val count = copyLarge(input, output)
+ return if (count > Int.MAX_VALUE) {
+ -1
+ } else count.toInt()
+ }
+
+ @Throws(IOException::class)
+ fun copy(sourcePath: String, destPath: String): Int {
+ val sourceFile = File(sourcePath)
+ if (!sourceFile.exists()) return -1
+ val destFile = File(destPath).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() }
+ return copy(FileInputStream(sourceFile), FileOutputStream(destFile))
+ }
+
+ private const val DEFAULT_BUFFER_SIZE = 1024 * 4
+ @Throws(IOException::class)
+ fun copyLarge(input: InputStream, output: OutputStream): Long {
+ val buffer =
+ ByteArray(DEFAULT_BUFFER_SIZE)
+ var count: Long = 0
+ var n = 0
+ while (-1 != input.read(buffer).also { n = it }) {
+ output.write(buffer, 0, n)
+ count += n.toLong()
+ }
+ return count
+ }
+}
diff --git a/kernel/src/main/java/com/magic/kernel/utils/LogUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/LogUtil.kt
new file mode 100644
index 0000000..e136da9
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/LogUtil.kt
@@ -0,0 +1,88 @@
+package cc.sdkutil.controller.util
+
+import android.util.Log
+
+/**
+ * Created by wangcong on 14-12-26.
+ * 在控制台打印Log,发布版本时在Application中设置答应Log 为false
+ */
+object LogUtil {
+
+ private var isDebug = true
+
+ private const val TAG = "LogUtil"
+ fun i(msg: String?) {
+ if (isDebug) Log.i(TAG, msg)
+ }
+
+ fun d(msg: String?) {
+ if (isDebug) Log.d(TAG, msg)
+ }
+
+ fun e(msg: String?) {
+ if (isDebug) Log.e(TAG, msg)
+ }
+
+ fun v(msg: String?) {
+ if (isDebug) Log.v(TAG, msg)
+ }
+
+ fun i(_class: Class<*>, msg: String?) {
+ if (isDebug) Log.i(_class.getName(), msg)
+ }
+
+ fun d(_class: Class<*>, msg: String?) {
+ if (isDebug) Log.d(_class.getName(), msg)
+ }
+
+ fun e(_class: Class<*>, msg: String?) {
+ if (isDebug) Log.e(_class.getName(), msg)
+ }
+
+ fun v(_class: Class<*>, msg: String?) {
+ if (isDebug) Log.v(_class.getName(), msg)
+ }
+
+ fun i(tag: String?, msg: String?) {
+ if (isDebug) Log.i(tag, msg)
+ }
+
+ fun d(tag: String?, msg: String?) {
+ if (isDebug) Log.d(tag, msg)
+ }
+
+ fun d(_class: Class<*>?, methodName: String?, msg: String?) {
+ if (isDebug && (_class != null || methodName != null) && msg != null) Log.d(_class?.name + "--" + methodName, msg)
+ }
+
+ fun e(tag: String?, msg: String?) {
+ if (isDebug) Log.e(tag, msg)
+ }
+
+ fun v(tag: String?, msg: String?) {
+ if (isDebug) Log.v(tag, msg)
+ }
+
+ /**
+ * 此方法用于框架内部调试
+ * @param debug
+ * @param clazz
+ * @param method
+ * @param msg
+ */
+ fun d(debug: Boolean, clazz: Class<*>, method: String, msg: String?) {
+ if (!isDebug) return
+ if (debug && msg != null) Log.d(clazz.getName().toString() + " -- " + method, msg)
+ }
+
+ /**
+ *
+ * @param debug
+ * @param clazz
+ * @param msg
+ */
+ fun d(debug: Boolean, clazz: Class<*>, msg: String?) {
+ if (!isDebug) return
+ if (debug && msg != null) Log.d(clazz.getName(), msg)
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/utils/MirrorUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/MirrorUtil.kt
new file mode 100644
index 0000000..1aa8739
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/MirrorUtil.kt
@@ -0,0 +1,67 @@
+package com.magic.kernel.utils
+
+object MirrorUtil {
+ /**
+ * 返回一个 Object 所声明的所有成员变量(不含基类成员)
+ */
+ @JvmStatic fun collectFields(instance: Any): List> {
+ return instance::class.java.declaredFields.filter { field ->
+ field.name != "INSTANCE" && field.name != "\$\$delegatedProperties"
+ }.map { field ->
+ field.isAccessible = true
+ val key = field.name.removeSuffix("\$delegate")
+ val value = field.get(instance)
+ key to value
+ }
+ }
+
+ /**
+ * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置
+ */
+ @JvmStatic fun generateReport(instances: List): List> {
+ return instances.map { instance ->
+ collectFields(instance).map {
+ "${instance::class.java.canonicalName}.${it.first}" to it.second.toString()
+ }
+ }.flatten().sortedBy { it.first }
+ }
+
+ /**
+ * 将一个用于单元测试的惰性求值对象还原到未求值的状态
+ *
+ * WARN: 仅供单元测试使用
+ */
+ @JvmStatic fun clearUnitTestLazyFields(instance: Any) {
+ instance::class.java.declaredFields.forEach { field ->
+ if (Lazy::class.java.isAssignableFrom(field.type)) {
+ field.isAccessible = true
+ val lazyObject = field.get(instance)
+// if (lazyObject is MagicWxGlobal.UnitTestLazyImpl<*>) {
+// lazyObject.refresh()
+// }
+ }
+ }
+ }
+
+ /**
+ * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置
+ *
+ * 如果某个自动适配表达式还没有进行求值的话, 该函数会强制进行一次求值
+ *
+ * WARN: 仅供单元测试使用
+ */
+ @JvmStatic fun generateReportWithForceEval(instances: List): List> {
+ return instances.map { instance ->
+ collectFields(instance).map {
+ val value = it.second
+ if (value is Lazy<*>) {
+ if (!value.isInitialized()) {
+ value.value
+ }
+ }
+ "${instance::class.java.canonicalName}.${it.first}" to it.second.toString()
+ }
+ }.flatten() // 为了 Benchmark 的准确性, 不对结果进行排序
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/utils/ParallelUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/ParallelUtil.kt
new file mode 100644
index 0000000..6833e4e
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/ParallelUtil.kt
@@ -0,0 +1,52 @@
+package com.magic.kernel.utils
+
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlin.concurrent.thread
+import com.magic.kernel.helper.TryHelper.tryVerbosely
+
+object ParallelUtil {
+
+ val processors: Int = Runtime.getRuntime().availableProcessors()
+
+ @JvmStatic
+ fun createThreadPool(nThread: Int = processors): ExecutorService =
+ Executors.newFixedThreadPool(nThread)
+
+ @JvmStatic
+ inline fun List.parallelMap(crossinline transform: (T) -> R): List {
+ val sectionSize = size / processors
+
+ val main = List(processors) { mutableListOf() }
+ (0 until processors).map { section ->
+ thread(start = true) {
+ for (offset in 0 until sectionSize) {
+ val idx = section * sectionSize + offset
+ main[section].add(transform(this[idx]))
+ }
+ }
+ }.forEach { it.join() }
+
+ val rest = (0 until size % processors).map { offset ->
+ val idx = processors * sectionSize + offset
+ transform(this[idx])
+ }
+
+ return main.flatten() + rest
+ }
+
+ @JvmStatic
+ inline fun Iterable.parallelForEach(crossinline action: (T) -> Unit) {
+ val pool = createThreadPool()
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val item = iterator.next()
+ pool.execute {
+ tryVerbosely { action(item) } // 避免进程崩溃
+ }
+ }
+ pool.shutdown()
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)
+ }
+}
\ No newline at end of file
diff --git a/kernel/src/main/java/com/magic/kernel/utils/XposedUtil.kt b/kernel/src/main/java/com/magic/kernel/utils/XposedUtil.kt
new file mode 100644
index 0000000..87c7b78
--- /dev/null
+++ b/kernel/src/main/java/com/magic/kernel/utils/XposedUtil.kt
@@ -0,0 +1,40 @@
+package com.magic.kernel.utils
+
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import com.magic.kernel.core.Hooker
+import com.magic.kernel.helper.TryHelper.tryVerbosely
+import com.magic.kernel.helper.TryHelper.trySilently
+
+object XposedUtil {
+
+ private val workerPool = ParallelUtil.createThreadPool()
+
+ private val managerThread = HandlerThread("HookHandler").apply { start() }
+
+ private val managerHandler: Handler = Handler(managerThread.looper)
+
+ @JvmStatic
+ private inline fun tryHook(crossinline hook: () -> Unit) {
+ when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
+ tryVerbosely(hook)
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> {
+ workerPool.execute { tryVerbosely(hook) }
+ }
+ else -> {
+ workerPool.execute { trySilently(hook) }
+ }
+ }
+ }
+
+ @JvmStatic
+ fun postHooker(hooker: Hooker) {
+ managerHandler.post {
+ hooker.hook()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kernel/src/main/jniLibs/arm64-v8a/libsilk.so b/kernel/src/main/jniLibs/arm64-v8a/libsilk.so
new file mode 100755
index 0000000..63946df
Binary files /dev/null and b/kernel/src/main/jniLibs/arm64-v8a/libsilk.so differ
diff --git a/kernel/src/main/jniLibs/armeabi-v7a/libsilk.so b/kernel/src/main/jniLibs/armeabi-v7a/libsilk.so
new file mode 100755
index 0000000..63946df
Binary files /dev/null and b/kernel/src/main/jniLibs/armeabi-v7a/libsilk.so differ
diff --git a/kernel/src/main/res/values/strings.xml b/kernel/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ee4b4cb
--- /dev/null
+++ b/kernel/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ kernel
+
diff --git a/kernel/src/test/java/com/magic/kernel/ExampleUnitTest.kt b/kernel/src/test/java/com/magic/kernel/ExampleUnitTest.kt
new file mode 100644
index 0000000..14eaf83
--- /dev/null
+++ b/kernel/src/test/java/com/magic/kernel/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.magic.kernel
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/local.properties b/local.properties
new file mode 100644
index 0000000..0baeff2
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,8 @@
+## This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Tue Mar 24 09:18:45 CST 2020
+sdk.dir=/Users/wangcong/Library/Android/sdk
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..fdf1d9e
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app', ':kernel', ':shared', ':wechat', ':wework'
+rootProject.name='ExampleWework'
diff --git a/shared/.gitignore b/shared/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/shared/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/shared/build.gradle b/shared/build.gradle
new file mode 100644
index 0000000..499046e
--- /dev/null
+++ b/shared/build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'consumer-rules.pro'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation project(":kernel")
+ compileOnly 'de.robv.android.xposed:api:82'
+ compileOnly 'de.robv.android.xposed:api:82:sources'
+}
diff --git a/shared/consumer-rules.pro b/shared/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/shared/proguard-rules.pro b/shared/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/shared/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/shared/shared.iml b/shared/shared.iml
new file mode 100644
index 0000000..c2d61f6
--- /dev/null
+++ b/shared/shared.iml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shared/src/androidTest/java/com/magic/shared/ExampleInstrumentedTest.kt b/shared/src/androidTest/java/com/magic/shared/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..236ab4d
--- /dev/null
+++ b/shared/src/androidTest/java/com/magic/shared/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.magic.shared
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.magic.shared.test", appContext.packageName)
+ }
+}
diff --git a/shared/src/main/AndroidManifest.xml b/shared/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..08bb1a7
--- /dev/null
+++ b/shared/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/shared/src/main/java/com/magic/shared/apis/SharedEngine.kt b/shared/src/main/java/com/magic/shared/apis/SharedEngine.kt
new file mode 100644
index 0000000..56b4a9d
--- /dev/null
+++ b/shared/src/main/java/com/magic/shared/apis/SharedEngine.kt
@@ -0,0 +1,13 @@
+package com.magic.shared.apis
+
+import com.magic.kernel.core.HookerCenter
+import com.magic.shared.hookers.ActivityHookers
+
+object SharedEngine {
+
+ var hookerCenters: List = listOf(
+ ActivityHookers
+// DatabaseHookers,
+// FileHookers
+ )
+}
diff --git a/shared/src/main/java/com/magic/shared/hookers/ActivityHookers.kt b/shared/src/main/java/com/magic/shared/hookers/ActivityHookers.kt
new file mode 100644
index 0000000..aedca1b
--- /dev/null
+++ b/shared/src/main/java/com/magic/shared/hookers/ActivityHookers.kt
@@ -0,0 +1,47 @@
+package com.magic.shared.hookers
+
+import com.magic.kernel.core.Clazz
+import com.magic.kernel.core.HookerCenter
+import com.magic.shared.hookers.interfaces.IActivityHooker
+
+object ActivityHookers : HookerCenter() {
+
+ override val interfaces: List>
+ get() = listOf(IActivityHooker::class.java)
+
+ override fun provideEventHooker(event: String) = when (event) {
+ "onActivityCreating",
+ "onActivityCreated" ->
+ iMethodNotifyHooker(
+ clazz = Clazz.Activity,
+ method = "onCreate",
+ iClazz = IActivityHooker::class.java,
+ iMethodBefore = "onActivityCreating",
+ iMethodAfter = "onActivityCreated",
+ needObject = true,
+ parameterTypes = *arrayOf(Clazz.Bundle)
+ )
+ "onActivityStarting",
+ "onActivityStarted" ->
+ iMethodNotifyHooker(
+ clazz = Clazz.Activity,
+ method = "onStart",
+ iClazz = IActivityHooker::class.java,
+ iMethodBefore = "onActivityStarting",
+ iMethodAfter = "onActivityStarted",
+ needObject = true
+ )
+ "onActivityResuming",
+ "onActivityResumed" ->
+ iMethodNotifyHooker(
+ clazz = Clazz.Activity,
+ method = "onResume",
+ iClazz = IActivityHooker::class.java,
+ iMethodBefore = "onActivityResuming",
+ iMethodAfter = "onActivityResumed",
+ needObject = true
+ )
+ else -> throw IllegalArgumentException("Unknown event: $event")
+ }
+
+}
\ No newline at end of file
diff --git a/shared/src/main/java/com/magic/shared/hookers/interfaces/IActivityHooker.kt b/shared/src/main/java/com/magic/shared/hookers/interfaces/IActivityHooker.kt
new file mode 100644
index 0000000..3e269c7
--- /dev/null
+++ b/shared/src/main/java/com/magic/shared/hookers/interfaces/IActivityHooker.kt
@@ -0,0 +1,29 @@
+package com.magic.shared.hookers.interfaces
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+
+interface IActivityHooker {
+
+ /**
+ * onCreate
+ */
+ fun onActivityCreating(activity: Activity, savedInstanceState: Bundle?) {}
+ fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
+
+ /**
+ * onStart
+ */
+ fun onActivityStarting(activity: Activity) {}
+ fun onActivityStarted(activity: Activity) {}
+
+ /**
+ * onResume
+ */
+ fun onActivityResuming(activity: Activity) {}
+ fun onActivityResumed(activity: Activity) {}
+
+ fun onActivityResulting(activity: Activity, requestCode: Int, resultCode: Int, data: Intent) {}
+ fun onActivityResulted(activity: Activity, requestCode: Int, resultCode: Int, data: Intent) {}
+}
\ No newline at end of file
diff --git a/shared/src/main/res/values/strings.xml b/shared/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e3d8143
--- /dev/null
+++ b/shared/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ shared
+
diff --git a/shared/src/test/java/com/magic/shared/ExampleUnitTest.kt b/shared/src/test/java/com/magic/shared/ExampleUnitTest.kt
new file mode 100644
index 0000000..62b9afa
--- /dev/null
+++ b/shared/src/test/java/com/magic/shared/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.magic.shared
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/sources/demo-1.png b/sources/demo-1.png
new file mode 100644
index 0000000..5465726
Binary files /dev/null and b/sources/demo-1.png differ
diff --git a/sources/demo-2.jpeg b/sources/demo-2.jpeg
new file mode 100644
index 0000000..0de28e4
Binary files /dev/null and b/sources/demo-2.jpeg differ
diff --git a/sources/my_contact_01.png b/sources/my_contact_01.png
new file mode 100644
index 0000000..0177087
Binary files /dev/null and b/sources/my_contact_01.png differ
diff --git a/sources/my_contact_02.png b/sources/my_contact_02.png
new file mode 100644
index 0000000..a78be1c
Binary files /dev/null and b/sources/my_contact_02.png differ
diff --git a/wechat/.gitignore b/wechat/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/wechat/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/wechat/build.gradle b/wechat/build.gradle
new file mode 100644
index 0000000..499046e
--- /dev/null
+++ b/wechat/build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'consumer-rules.pro'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation project(":kernel")
+ compileOnly 'de.robv.android.xposed:api:82'
+ compileOnly 'de.robv.android.xposed:api:82:sources'
+}
diff --git a/wechat/consumer-rules.pro b/wechat/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/wechat/proguard-rules.pro b/wechat/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/wechat/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/wechat/src/androidTest/java/com/magic/wechat/ExampleInstrumentedTest.kt b/wechat/src/androidTest/java/com/magic/wechat/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..4b937a0
--- /dev/null
+++ b/wechat/src/androidTest/java/com/magic/wechat/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.magic.wechat
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.magic.wechat.test", appContext.packageName)
+ }
+}
diff --git a/wechat/src/main/AndroidManifest.xml b/wechat/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bc505b7
--- /dev/null
+++ b/wechat/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/wechat/src/main/res/values/strings.xml b/wechat/src/main/res/values/strings.xml
new file mode 100644
index 0000000..44382a7
--- /dev/null
+++ b/wechat/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ wechat
+
diff --git a/wechat/src/test/java/com/magic/wechat/ExampleUnitTest.kt b/wechat/src/test/java/com/magic/wechat/ExampleUnitTest.kt
new file mode 100644
index 0000000..8aee5f6
--- /dev/null
+++ b/wechat/src/test/java/com/magic/wechat/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.magic.wechat
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/wechat/wechat.iml b/wechat/wechat.iml
new file mode 100644
index 0000000..64ac72e
--- /dev/null
+++ b/wechat/wechat.iml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wework/.gitignore b/wework/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/wework/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/wework/build.gradle b/wework/build.gradle
new file mode 100644
index 0000000..29e1766
--- /dev/null
+++ b/wework/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'consumer-rules.pro'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation project(":kernel")
+ implementation project(":shared")
+ implementation 'com.google.code.gson:gson:2.8.6'
+ compileOnly 'de.robv.android.xposed:api:82'
+ compileOnly 'de.robv.android.xposed:api:82:sources'
+}
diff --git a/wework/consumer-rules.pro b/wework/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/wework/proguard-rules.pro b/wework/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/wework/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/wework/src/androidTest/java/com/magic/wework/ExampleInstrumentedTest.kt b/wework/src/androidTest/java/com/magic/wework/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..96c8d18
--- /dev/null
+++ b/wework/src/androidTest/java/com/magic/wework/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.magic.wework
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.magic.wework.test", appContext.packageName)
+ }
+}
diff --git a/wework/src/main/AndroidManifest.xml b/wework/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..196debf
--- /dev/null
+++ b/wework/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/wework/src/main/java/com/magic/wework/apis/WwEngine.kt b/wework/src/main/java/com/magic/wework/apis/WwEngine.kt
new file mode 100644
index 0000000..0080307
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/apis/WwEngine.kt
@@ -0,0 +1,18 @@
+package com.magic.wework.apis
+
+import com.magic.kernel.core.HookerCenter
+import com.magic.wework.hookers.*
+
+object WwEngine {
+
+ var hookerCenters: List = listOf(
+ ApplicationHookers,
+// ContactHookers,
+ ConversationHookers
+// CustomerHookers,
+// NotificationHookers
+// DepartmentHookers
+// UserLabelHookers
+ )
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/Application.kt b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/Application.kt
new file mode 100644
index 0000000..836cd01
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/Application.kt
@@ -0,0 +1,15 @@
+package com.magic.wework.apis.com.tencent.wework.foundation.logic
+
+import de.robv.android.xposed.XposedHelpers
+import com.magic.wework.mirror.com.tencent.wework.foundation.logic.Classes.Application
+import com.magic.wework.mirror.com.tencent.wework.foundation.logic.Methods
+
+/**
+ * com.tencent.wework.foundation.logic.Application
+ */
+object Application {
+
+ fun getInstance(): Any =
+ XposedHelpers.callStaticMethod(Application, Methods.Application.getInstance.name)
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/ConversationService.kt b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/ConversationService.kt
new file mode 100644
index 0000000..0bb0d40
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/logic/ConversationService.kt
@@ -0,0 +1,36 @@
+package com.magic.wework.apis.com.tencent.wework.foundation.logic
+
+import com.magic.wework.mirror.com.tencent.wework.foundation.logic.Classes.ConversationService
+import com.magic.wework.mirror.com.tencent.wework.foundation.logic.Methods
+import de.robv.android.xposed.XposedHelpers
+
+object ConversationService {
+
+ /**
+ * @return
+ */
+ private fun getService(): Any =
+ XposedHelpers.callStaticMethod(
+ ConversationService,
+ Methods.ConversationService.getService.name
+ )
+
+ /**
+ * 获取所有会话
+ * @param conversation 可以为空
+ * @param type 类型 -1 为全部
+ *
+ * @return []com.tencent.wework.foundation.model.Conversation
+ */
+
+ /**
+ * @param com.tencent.wework.foundation.observer.IConversationListObserver
+ */
+ fun addObserver(iConversationListObserver: Any) =
+ XposedHelpers.callMethod(
+ getService(),
+ Methods.ConversationService.AddObserver.name,
+ iConversationListObserver
+ )
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Conversation.kt b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Conversation.kt
new file mode 100644
index 0000000..5ff844a
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Conversation.kt
@@ -0,0 +1,37 @@
+package com.magic.wework.apis.com.tencent.wework.foundation.model
+
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Methods
+import de.robv.android.xposed.XposedHelpers
+
+/**
+ * @property original com.tencent.wework.foundation.model.Conversation
+ */
+data class Conversation(var original: Any) {
+
+ /**
+ * 针对
+ */
+ companion object {
+
+ /**
+ * @return
+ */
+ fun getInfo(original: Any): Any =
+ XposedHelpers.callMethod(original, Methods.Conversation.getInfo.name)
+
+ }
+
+// fun getInfo(): WwConversation.Conversation =
+// WwConversation.Conversation.parseFrom(Companion.getInfo(original))
+//
+// fun getLocalId(): Long = getInfo().id
+//
+// fun getFinancialDisagreeVids(): LongArray = Companion.getFinancialDisagreeVids(original)
+//
+// fun getShowTime(): Long = Companion.getShowTime(original)
+//
+// fun getSortTime(): Long = Companion.getSortTime(original)
+//
+// fun containMember(userId: Long) = getUserList(longArrayOf(userId)).isNotEmpty()
+
+}
diff --git a/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Message.kt b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Message.kt
new file mode 100644
index 0000000..f80016a
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/apis/com/tencent/wework/foundation/model/Message.kt
@@ -0,0 +1,21 @@
+package com.magic.wework.apis.com.tencent.wework.foundation.model
+
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Methods
+import de.robv.android.xposed.XposedHelpers
+
+/**
+ * @param original com.tencent.wework.foundation.model.Message
+ */
+data class Message(var original: Any) {
+
+ companion object {
+
+ /**
+ * @return
+ */
+ fun getInfo(original: Any): Any =
+ XposedHelpers.callMethod(original, Methods.Message.getInfo.name)
+
+ }
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/hookers/ApplicationHookers.kt b/wework/src/main/java/com/magic/wework/hookers/ApplicationHookers.kt
new file mode 100644
index 0000000..0678c1a
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/hookers/ApplicationHookers.kt
@@ -0,0 +1,11 @@
+package com.magic.wework.hookers
+
+import com.magic.kernel.core.HookerCenter
+import com.magic.wework.hookers.interfaces.IApplicationHooker
+
+object ApplicationHookers : HookerCenter() {
+
+ override val interfaces: List>
+ get() = listOf(IApplicationHooker::class.java)
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/hookers/ConversationHookers.kt b/wework/src/main/java/com/magic/wework/hookers/ConversationHookers.kt
new file mode 100644
index 0000000..ee7aa31
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/hookers/ConversationHookers.kt
@@ -0,0 +1,108 @@
+package com.magic.wework.hookers
+
+import android.util.Log
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.core.HookerCenter
+import com.magic.kernel.core.Hooker
+import com.magic.kernel.MagicGlobal.classLoader
+import com.magic.kernel.core.Clazz
+import com.magic.kernel.helper.ReflecterHelper.findMethodsByExactName
+import com.magic.wework.apis.com.tencent.wework.foundation.logic.ConversationService
+import com.magic.wework.hookers.interfaces.IConversationHooker
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Classes.Conversation
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Classes.Message
+import com.magic.wework.mirror.com.tencent.wework.foundation.observer.Classes.IConversationObserverImpl
+import com.magic.wework.mirror.com.tencent.wework.foundation.observer.Classes.IConversationListObserver
+import com.magic.wework.mirror.com.tencent.wework.foundation.observer.Methods.IConversationObserver
+import java.lang.reflect.Array
+import java.lang.reflect.Proxy
+
+object ConversationHookers : HookerCenter() {
+
+ override val interfaces: List>
+ get() = listOf(IConversationHooker::class.java)
+
+ override fun provideEventHooker(event: String): Hooker? {
+ return when (event) {
+ "onReconvergeConversation",
+ "onReloadConvsProperty",
+ "onSyncStateChanged",
+ "onAddConversations",
+ "onExitConversation" ->
+ iConversationListObserverHooker
+
+ "onSetReadReceipt",
+ "onAddMembers",
+ "onChangeOwner",
+ "onDraftDidChange",
+ "onModifyName",
+ "onPropertyChanged",
+ "onRemoveAllMessages",
+ "onRemoveMembers",
+ "onSetAllBan",
+ "onSetCollect",
+ "onSetConfirmAddMember",
+ "onSetMembersBan",
+ "onSetOwnerManager",
+ "onSetShield",
+ "onSetTop",
+ "onTypingStateUpdate" ->
+ iMethodNotifyHooker(
+ clazz = IConversationObserverImpl,
+ method = IConversationObserver.getMethodByName(event),
+ iClazz = IConversationHooker::class.java,
+ iMethodAfter = event,
+ parameterTypes = *arrayOf(Conversation)
+ )
+ "onAddMessages" ->
+ iMethodNotifyHooker(
+ clazz = IConversationObserverImpl,
+ method = IConversationObserver.getMethodByName(event),
+ iClazz = IConversationHooker::class.java,
+ iMethodAfter = event,
+ parameterTypes = *arrayOf(Conversation, Array.newInstance(Message, 0).javaClass, Clazz.Boolean)
+ )
+ "onMessageStateChange" ->
+ iMethodNotifyHooker(
+ clazz = IConversationObserverImpl,
+ method = IConversationObserver.getMethodByName(event),
+ iClazz = IConversationHooker::class.java,
+ iMethodAfter = event,
+ parameterTypes = *arrayOf(Conversation, Message, Clazz.Int)
+ )
+ "onUnReadCountChanged" ->
+ iMethodNotifyHooker(
+ clazz = IConversationObserverImpl,
+ method = IConversationObserver.getMethodByName(event),
+ iClazz = IConversationHooker::class.java,
+ iMethodAfter = event,
+ parameterTypes = *arrayOf(Conversation, Clazz.Int, Clazz.Int)
+ )
+ else -> {
+ if (MagicGlobal.unitTestMode) {
+ throw IllegalArgumentException("Unknown event: $event")
+ }
+ Log.e(ConversationHookers::class.java.name, "function not found: ${event}")
+ return null
+ }
+ }
+ }
+
+ private val iConversationListObserverHooker = Hooker {
+ val observer = Proxy.newProxyInstance(
+ classLoader,
+ arrayOf(IConversationListObserver)
+ ) { _, method, args ->
+ val iMethodName = (method.name)
+ notify(iMethodName) {
+ val iMethod = findMethodsByExactName(
+ IConversationHooker::class.java,
+ iMethodName
+ ).firstOrNull()
+ iMethod?.invoke(it, *args.orEmpty())
+ }
+ }
+ ConversationService.addObserver(observer)
+ }
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/hookers/interfaces/IApplicationHooker.kt b/wework/src/main/java/com/magic/wework/hookers/interfaces/IApplicationHooker.kt
new file mode 100644
index 0000000..df9dc7a
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/hookers/interfaces/IApplicationHooker.kt
@@ -0,0 +1,5 @@
+package com.magic.wework.hookers.interfaces
+
+interface IApplicationHooker {
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/hookers/interfaces/IConversationHooker.kt b/wework/src/main/java/com/magic/wework/hookers/interfaces/IConversationHooker.kt
new file mode 100644
index 0000000..eefd600
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/hookers/interfaces/IConversationHooker.kt
@@ -0,0 +1,57 @@
+package com.magic.wework.hookers.interfaces
+
+interface IConversationHooker {
+
+ fun onReconvergeConversation() {}
+
+ fun onReloadConvsProperty() {}
+
+ fun onSyncStateChanged(i: Int, i2: Int) {}
+
+ fun onAddConversations(conversationArr: Array) {}
+
+ fun onExitConversation(conversation: Any) {}
+
+ fun onSetReadReceipt(conversation: Any) {}
+
+ fun onAddMembers(conversation: Any) {}
+
+ fun onAddMessages(conversation: Any, messageArr: Array, z: Boolean) {}
+
+ fun onChangeOwner(conversation: Any) {}
+
+ fun onDraftDidChange(conversation: Any) {}
+
+ fun onMessageStateChange(conversation: Any, message: Any, i: Int) {}
+
+ fun onMessageUpdate(conversation: Any, message: Any) {}
+
+ fun onModifyName(conversation: Any) {}
+
+ fun onPropertyChanged(conversation: Any) {}
+
+ fun onRemoveAllMessages(conversation: Any) {}
+
+ fun onRemoveMembers(conversation: Any) {}
+
+ fun onRemoveMessages(conversation: Any, message: Any) {}
+
+ fun onSetAllBan(conversation: Any) {}
+
+ fun onSetCollect(conversation: Any) {}
+
+ fun onSetConfirmAddMember(conversation: Any) {}
+
+ fun onSetMembersBan(conversation: Any) {}
+
+ fun onSetOwnerManager(conversation: Any) {}
+
+ fun onSetShield(conversation: Any) {}
+
+ fun onSetTop(conversation: Any) {}
+
+ fun onTypingStateUpdate(conversation: Any) {}
+
+ fun onUnReadCountChanged(conversation: Any, i: Int, i2: Int) {}
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Classes.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Classes.kt
new file mode 100644
index 0000000..29a6b40
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Classes.kt
@@ -0,0 +1,21 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.logic
+
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.MagicGlobal.classLoader
+import com.magic.kernel.helper.ReflecterHelper.findClassIfExists
+
+object Classes {
+ private val packageName =
+ "${MagicGlobal.packageName}.${javaClass.name.replaceBeforeLast("foundation", "")}".removeSuffix(".${javaClass.simpleName}")
+
+ val Application: Class<*> by lazy("${javaClass.name}.Application") {
+ findClassIfExists("$packageName.Application", classLoader!!)
+ }
+
+ val ConversationService: Class<*> by lazy("${javaClass.name}.ConversationService") {
+ findClassIfExists("$packageName.ConversationService", classLoader!!)
+ }
+
+}
+
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Methods.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Methods.kt
new file mode 100644
index 0000000..fe6604a
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/logic/Methods.kt
@@ -0,0 +1,38 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.logic
+
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.helper.ReflecterHelper.findMethodIfExists
+import com.magic.kernel.helper.ReflecterHelper.findMethodsByExactParameters
+import com.magic.wework.mirror.com.tencent.wework.foundation.observer.Classes.IConversationListObserver
+import java.lang.reflect.Method
+
+object Methods {
+
+ /** --------- Application -------- */
+ object Application {
+
+ val getInstance: Method by lazy("${javaClass.name}.getInstance") {
+ findMethodIfExists(Classes.Application, "getInstance")
+ }
+
+ }
+
+ object ConversationService {
+
+ val getService: Method by lazy("${javaClass.name}.gs") {
+ findMethodsByExactParameters(
+ Classes.ConversationService, Classes.ConversationService
+ ).firstOrNull()
+ }
+
+ val AddObserver: Method by lazy("${javaClass.name}.ao") {
+ findMethodsByExactParameters(
+ Classes.ConversationService,
+ null,
+ IConversationListObserver
+ ).firstOrNull()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Classes.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Classes.kt
new file mode 100644
index 0000000..6cad7f8
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Classes.kt
@@ -0,0 +1,20 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.model
+
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.MagicGlobal.classLoader
+import com.magic.kernel.helper.ReflecterHelper.findClassIfExists
+
+object Classes {
+ private val packageName = "${MagicGlobal.packageName}.${javaClass.name.replaceBeforeLast("foundation", "")}".removeSuffix(".${javaClass.simpleName}")
+
+ val Conversation: Class<*> by lazy("${javaClass.name}.Conversation") {
+ findClassIfExists("$packageName.Conversation", classLoader!!)
+ }
+
+ val Message: Class<*> by lazy("${javaClass.name}.Message") {
+ findClassIfExists("$packageName.Message", classLoader!!)
+ }
+
+}
+
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Methods.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Methods.kt
new file mode 100644
index 0000000..6a43925
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/model/Methods.kt
@@ -0,0 +1,25 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.model
+
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.helper.ReflecterHelper.findMethodIfExists
+import java.lang.reflect.Method
+
+object Methods {
+
+ object Conversation {
+
+ val getInfo: Method by lazy("${javaClass.name}.getInfo") {
+ findMethodIfExists(Classes.Conversation, "getInfo")
+ }
+
+ }
+
+ object Message {
+
+ val getInfo: Method by lazy("${javaClass.name}.getInfo") {
+ findMethodIfExists(Classes.Message, "getInfo")
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Classes.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Classes.kt
new file mode 100644
index 0000000..e808e70
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Classes.kt
@@ -0,0 +1,35 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.observer
+
+import com.magic.kernel.MagicGlobal
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.MagicGlobal.classes
+import com.magic.kernel.MagicGlobal.classLoader
+import com.magic.kernel.helper.ReflecterHelper.findClassIfExists
+import com.magic.kernel.helper.ReflecterHelper.findClassesInPackage
+
+object Classes {
+ private val packageName =
+ "${MagicGlobal.packageName}.${javaClass.name.replaceBeforeLast("foundation", "").removeSuffix(".${javaClass.simpleName}")}"
+
+ val IConversationListObserver: Class<*> by lazy("${javaClass.name}.IConversationListObserver") {
+ findClassIfExists("$packageName.IConversationListObserver", classLoader!!)
+ }
+
+ val IConversationListObserverImpl: Class<*> by lazy("${javaClass.name}.icloi_fct$32hch$44") {
+ findClassesInPackage(classLoader!!, classes!!, "")
+ .filterByInterfaces(IConversationListObserver)
+ .firstOrNull()
+ }
+
+ val IConversationObserver: Class<*> by lazy("${javaClass.name}.IConversationObserver") {
+ findClassIfExists("$packageName.IConversationObserver", classLoader!!)
+ }
+
+ val IConversationObserverImpl: Class<*> by lazy("${javaClass.canonicalName}.icoifct$22") {
+ findClassesInPackage(classLoader!!, classes!!, "")
+ .filterByInterfaces(IConversationObserver)
+ .firstOrNull()
+ }
+
+}
+
diff --git a/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Methods.kt b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Methods.kt
new file mode 100644
index 0000000..27d1c60
--- /dev/null
+++ b/wework/src/main/java/com/magic/wework/mirror/com/tencent/wework/foundation/observer/Methods.kt
@@ -0,0 +1,247 @@
+package com.magic.wework.mirror.com.tencent.wework.foundation.observer
+
+import com.magic.kernel.core.Clazz
+import com.magic.kernel.MagicGlobal.lazy
+import com.magic.kernel.helper.ReflecterHelper.findMethodIfExists
+import com.magic.kernel.helper.ReflecterHelper.findMethodsByExactParameters
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Classes.Conversation
+import com.magic.wework.mirror.com.tencent.wework.foundation.model.Classes.Message
+import java.lang.reflect.Array
+import java.lang.reflect.Method
+
+object Methods {
+
+ object IConversationObserver {
+
+ fun getMethodByName(name: String): Method? {
+ return when (name) {
+ "onSetReadReceipt" -> OnSetReadReceipt
+ "onAddMembers" -> onAddMembers
+ "onAddMessages" -> onAddMessages
+ "onChangeOwner" -> onChangeOwner
+ "onDraftDidChange" -> onDraftDidChange
+ "onMessageStateChange" -> onMessageStateChange
+ "onMessageUpdate" -> onMessageUpdate
+ "onModifyName" -> onModifyName
+ "onPropertyChanged" -> onPropertyChanged
+ "onRemoveAllMessages" -> onRemoveAllMessages
+ "onRemoveMembers" -> onRemoveMembers
+ "onRemoveMessages" -> onRemoveMessages
+ "onSetAllBan" -> onSetAllBan
+ "onSetCollect" -> onSetCollect
+ "onSetConfirmAddMember" -> onSetConfirmAddMember
+ "onSetMembersBan" -> onSetMembersBan
+ "onSetOwnerManager" -> onSetOwnerManager
+ "onSetShield" -> onSetShield
+ "onSetTop" -> onSetTop
+ "onTypingStateUpdate" -> onTypingStateUpdate
+ "onUnReadCountChanged" -> onUnReadCountChanged
+ else -> null
+ }
+ }
+
+ val OnSetReadReceipt: Method by lazy("${javaClass.name}.OnSetReadReceipt") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "OnSetReadReceipt", Conversation)
+ }
+
+ val onAddMembers: Method by lazy("${javaClass.name}.onAddMembers") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onAddMembers", Conversation)
+ }
+
+ val onAddMessages: Method by lazy("${javaClass.name}.onAddMessages") {
+// findMethodIfExists(Classes.IConversationObserverImpl, "onAddMessages", Conversation)
+ findMethodsByExactParameters(
+ Classes.IConversationObserverImpl,
+ null,
+ Conversation,
+ Array.newInstance(Message, 0).javaClass,
+ Clazz.Boolean
+ ).firstOrNull()
+ }
+
+ val onChangeOwner: Method by lazy("${javaClass.name}.onChangeOwner") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onChangeOwner", Conversation)
+ }
+
+ val onDraftDidChange: Method by lazy("${javaClass.name}.onDraftDidChange") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onDraftDidChange", Conversation)
+ }
+
+ val onMessageStateChange: Method by lazy("${javaClass.name}.onMessageStateChange") {
+ findMethodsByExactParameters(
+ Classes.IConversationObserverImpl,
+ null,
+ Conversation,
+ Message,
+ Clazz.Int
+ ).firstOrNull()
+ }
+
+ val onMessageUpdate: Method by lazy("${javaClass.name}.onMessageUpdate") {
+ findMethodsByExactParameters(
+ Classes.IConversationObserverImpl,
+ null,
+ Conversation,
+ Message
+ ).firstOrNull()
+ }
+
+ val onModifyName: Method by lazy("${javaClass.name}.onModifyName") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onModifyName", Conversation)
+ }
+
+ val onPropertyChanged: Method by lazy("${javaClass.name}.onPropertyChanged") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onPropertyChanged", Conversation)
+ }
+
+ val onRemoveAllMessages: Method by lazy("${javaClass.name}.onRemoveAllMessages") {
+ findMethodIfExists(
+ Classes.IConversationObserverImpl,
+ "onRemoveAllMessages",
+ Conversation
+ )
+ }
+
+ val onRemoveMembers: Method by lazy("${javaClass.name}.onRemoveMembers") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onRemoveMembers", Conversation)
+ }
+
+ val onRemoveMessages: Method by lazy("${javaClass.name}.onRemoveMessages") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onRemoveMessages", Conversation)
+ }
+
+ val onSetAllBan: Method by lazy("${javaClass.name}.onSetAllBan") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetAllBan", Conversation)
+ }
+
+ val onSetCollect: Method by lazy("${javaClass.name}.onSetCollect") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetCollect", Conversation)
+ }
+
+ val onSetConfirmAddMember: Method by lazy("${javaClass.name}.onSetConfirmAddMember") {
+ findMethodIfExists(
+ Classes.IConversationObserverImpl,
+ "onSetConfirmAddMember",
+ Conversation
+ )
+ }
+
+ val onSetMembersBan: Method by lazy("${javaClass.name}.onSetMembersBan") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetMembersBan", Conversation)
+ }
+
+ val onSetOwnerManager: Method by lazy("${javaClass.name}.onSetOwnerManager") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetOwnerManager", Conversation)
+ }
+
+ val onSetShield: Method by lazy("${javaClass.name}.onSetShield") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetShield", Conversation)
+ }
+
+ val onSetTop: Method by lazy("${javaClass.name}.onSetTop") {
+ findMethodIfExists(Classes.IConversationObserverImpl, "onSetTop", Conversation)
+ }
+
+ val onTypingStateUpdate: Method by lazy("${javaClass.name}.onTypingStateUpdate") {
+ findMethodIfExists(
+ Classes.IConversationObserverImpl,
+ "onTypingStateUpdate",
+ Conversation
+ )
+ }
+
+ val onUnReadCountChanged: Method by lazy("${javaClass.name}.ourcc") {
+ findMethodsByExactParameters(
+ Classes.IConversationObserverImpl,
+ null,
+ Conversation,
+ Clazz.Int,
+ Clazz.Int
+ ).firstOrNull()
+ }
+
+ }
+
+ object IConversationListObserver {
+
+ fun getMethodByName(name: String): Method? {
+ return when (name) {
+ "onReconvergeConversation" -> onReconvergeConversation
+ "onReloadConvsProperty" -> onReloadConvsProperty
+ "onSyncStateChanged" -> onSyncStateChanged
+ "onAddConversations" -> onAddConversations
+ "onExitConversation" -> onExitConversation
+ else -> null
+ }
+ }
+
+ val onReconvergeConversation: Method by lazy("${javaClass.name}.OnReconvergeConversation") {
+ findMethodIfExists(Classes.IConversationListObserverImpl, "OnReconvergeConversation")
+ }
+
+ val onReloadConvsProperty: Method by lazy("${javaClass.name}.OnReloadConvsProperty") {
+ findMethodIfExists(Classes.IConversationListObserverImpl, "OnReloadConvsProperty")
+ }
+
+ val onSyncStateChanged: Method by lazy("${javaClass.name}.OnSyncStateChanged") {
+ findMethodIfExists(
+ Classes.IConversationListObserverImpl,
+ "OnSyncStateChanged",
+ Clazz.Int, Clazz.Int
+ )
+ }
+
+ val onAddConversations: Method by lazy("${javaClass.name}.onAddConversations") {
+ findMethodIfExists(Classes.IConversationListObserverImpl, "onAddConversations")
+ }
+
+ val onExitConversation: Method by lazy("${javaClass.name}.onExitConversation") {
+ findMethodIfExists(
+ Classes.IConversationListObserverImpl,
+ "onExitConversation",
+ Conversation
+ )
+ }
+ }
+
+ object IEnterpriseCustomerServiceObserver {
+
+ fun getMethodByName(name: String): Method? =
+ javaClass.declaredFields.filter {
+ it.name.equals(
+ name,
+ true
+ )
+ }.firstOrNull()?.get(this) as? Method
+
+ val OnCustomerListChange: Method by lazy("${javaClass.name}.OnCustomerListChange") {
+ findMethodIfExists(Classes.IConversationObserver, "OnCustomerListChange", Conversation)
+ }
+
+ val OnCustomerStaffListChange: Method by lazy("${javaClass.name}.OnCustomerStaffListChange") {
+ findMethodIfExists(
+ Classes.IConversationObserver,
+ "OnCustomerStaffListChange",
+ Conversation
+ )
+ }
+
+ val OnMyAdminServiceGroupsChanged: Method by lazy("${javaClass.name}.OnMyAdminServiceGroupsChanged") {
+ findMethodIfExists(
+ Classes.IConversationObserver,
+ "OnMyAdminServiceGroupsChanged",
+ Conversation
+ )
+ }
+
+ val OnServiceGroupListChanged: Method by lazy("${javaClass.name}.OnServiceGroupListChanged") {
+ findMethodIfExists(
+ Classes.IConversationObserver,
+ "OnServiceGroupListChanged",
+ Conversation
+ )
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/wework/src/main/res/values/strings.xml b/wework/src/main/res/values/strings.xml
new file mode 100644
index 0000000..dfd8612
--- /dev/null
+++ b/wework/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ wework
+
diff --git a/wework/src/test/java/com/magic/wework/ExampleUnitTest.kt b/wework/src/test/java/com/magic/wework/ExampleUnitTest.kt
new file mode 100644
index 0000000..bc51af3
--- /dev/null
+++ b/wework/src/test/java/com/magic/wework/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.magic.wework
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/wework/wework.iml b/wework/wework.iml
new file mode 100644
index 0000000..5c727b8
--- /dev/null
+++ b/wework/wework.iml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file