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,欢迎交流 + +![demo-1](sources/demo-1.png) +![demo-2](sources/demo-2.jpeg) + +### 注意: +为了避免某些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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file