This commit is contained in:
dev
2020-12-02 10:40:01 +08:00
parent d4a4e8ef35
commit 2afd558a23
132 changed files with 6587 additions and 2 deletions

23
.gitignore vendored Normal file
View File

@@ -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*

21
LICENSE Normal file
View File

@@ -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.

View File

@@ -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+姓
<img src="sources/my_contact_01.png" width="160" height="160"/><img src="sources/my_contact_02.png" width="160" height="160"/>

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

184
app/app.iml Normal file

File diff suppressed because one or more lines are too long

86
app/build.gradle Normal file
View File

@@ -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
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.magic.xmagichooker" >
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 网络处理 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 控制屏幕开关 -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- 安装其他应用 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="辅助插件" />
<meta-data
android:name="xposedminversion"
android:value="54" />
<activity android:name=".MainActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1 @@
com.magic.xmagichooker.Hooker

View File

@@ -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, "开始启动个人微信插件")
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<Any>) {
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<Any>, 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")
// }
}

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">XMagicHooker</string>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -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)
}
}

35
build.gradle Normal file
View File

@@ -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
}

28
gradle.properties Normal file
View File

@@ -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

View File

@@ -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

172
gradlew vendored Executable file
View File

@@ -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" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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"
}
]
}

1
kernel/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

75
kernel/build.gradle Normal file
View File

@@ -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'
}
}

View File

182
kernel/kernel.iml Normal file
View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":kernel" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":kernel" />
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.5.3" />
<option name="LAST_KNOWN_AGP_VERSION" value="3.5.3" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="PROJECT_TYPE" value="1" />
</configuration>
</facet>
<facet type="kotlin-language" name="Kotlin">
<configuration version="3" platform="JVM 1.6" allPlatforms="JVM [1.6]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xallow-no-source-files" />
</compilerSettings>
<compilerArguments>
<option name="destination" value="$MODULE_DIR$/build/tmp/kotlin-classes/debug" />
<option name="classpath" value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/de.robv.android.xposed/api/82/35866b507b360d4789ff389ad7386b6e8bbf6cc4/api-82.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/de.robv.android.xposed/api/82/2030f71764b06b2f39fa1a85660690aa834cfd84/api-82-sources.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-android-extensions-runtime/1.3.50/bec16087637a7cafe54894e73d38037977cb30d2/kotlin-android-extensions-runtime-1.3.50.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.3.50/50ad05ea1c2595fb31b800e76db464d08d599af3/kotlin-stdlib-jdk7-1.3.50.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/017f02232106f85c718c5d9c563a2a70/core-ktx-1.1.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/4.4.0/9b435219a19a850f5463e7b3a1f2a121fa6c56b2/okhttp-4.4.0.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/com.squareup.okio/okio/2.4.3/d946f785445d73c6bc99bbd778576d2576e37ea9/okio-jvm-2.4.3.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.61/4702105e97f7396ae41b113fdbdc180ec1eb1e36/kotlin-stdlib-1.3.61.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.61/65abb71d5afb850b68be03987b08e2c864ca3110/kotlin-stdlib-common-1.3.61.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/aaeb4ac383e46bb146f322ae9032f1e8/appcompat-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/52397f4a71f30dffd3659283f56c49cd/fragment-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/e5cd563b027052f2e3144f14db97c1d3/appcompat-resources-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/91dd8524d7698478ac8f047acd5f85d0/drawerlayout-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/030ed8ffbf8d3c58633260185d89ea67/viewpager-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/45f8209ee39e13f3cab9d7b0e8f651dd/loader-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/ac3d32c9cf68316fc66a5ce4d79c2301/activity-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/b27e6623db3562f63e67c3fa3e43e40a/vectordrawable-animated-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/9061978759f410c9baef4ddcb3096803/vectordrawable-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/8e21dbc21a31aecace5ce662d6572f9c/customview-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/264bd0e6e1b5a347721ed7f2deda2aba/core-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/260010174060e6b78f60e2af12ec4ee9/cursoradapter-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/d934669c66fd1ce97a07881131ddeb81/versionedparcelable-1.1.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.collection/collection/1.1.0/1f27220b47669781457de0d600849a5de0e89909/collection-1.1.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/49ecb08f235a45c347f2a70ae8f7da78/lifecycle-viewmodel-2.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/98f4ce2537445291cbdb29ffd6d548aa/lifecycle-runtime-2.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/19accbd8e07c1fca16f1a2044d0fe930/savedstate-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/11901a9795fa2327e5f523e133b881cf/lifecycle-livedata-2.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/ad662121c850b3f32692c894384c3de6/lifecycle-livedata-core-2.0.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.1.0/c67e7807d9cd6c329b9d0218b2ec4e505dd340b7/lifecycle-common-2.1.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/1c3ca424765cca9bbaf5b684b6898978/interpolator-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/03f516a6bc22fee39783350c275ccf75/core-runtime-2.0.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.arch.core/core-common/2.1.0/b3152fc64428c9354344bd89848ecddc09b6f07e/core-common-2.1.0.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.annotation/annotation/1.1.0/e3a6fb2f40e3a3842e6b7472628ba4ce416ea4c8/annotation-1.1.0.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.6/9180733b7df8542621dc12e21e87557e8c99b8cb/gson-2.8.6.jar:/Users/wangcong/Development/Android/XMagicHookerShare/kernel/build/intermediates/compile_only_not_namespaced_r_class_jar/debug/R.jar:/Users/wangcong/Library/Android/sdk/platforms/android-29/android.jar" />
<option name="noStdlib" value="true" />
<option name="noReflect" value="true" />
<option name="moduleName" value="kernel_debug" />
<option name="languageVersion" value="1.3" />
<option name="apiVersion" value="1.3" />
<option name="pluginOptions">
<array>
<option value="plugin:org.jetbrains.kotlin.android:experimental=false" />
<option value="plugin:org.jetbrains.kotlin.android:enabled=true" />
<option value="plugin:org.jetbrains.kotlin.android:defaultCacheImplementation=hashMap" />
</array>
</option>
<option name="pluginClasspaths">
<array>
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.intellij.deps/trove4j/1.0.20181211/216c2e14b070f334479d800987affe4054cd563f/trove4j-1.0.20181211.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-android-extensions/1.3.50/f16428b9ce307d0f5842bd8ed9af1e43a141edd3/kotlin-android-extensions-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable/1.3.50/1251c1768e5769b06c2487d6f6cf8acf6efb8960/kotlin-compiler-embeddable-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-daemon-embeddable/1.3.50/5cb93bb33f4c6f833ead0beca4c831668e00cf52/kotlin-daemon-embeddable-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-reflect/1.3.50/b499f22fd7c3e9c2e5b6c4005221fa47fc7f9a7a/kotlin-reflect-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/1.3.50/59492b8dfb92522ba0ddb5dd1c4d0ef0a4fca1af/kotlin-script-runtime-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.50/3d9cd3e1bc7b92e95f43d45be3bfbcf38e36ab87/kotlin-stdlib-common-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.50/b529d1738c7e98bbfa36a4134039528f2ce78ebf/kotlin-stdlib-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar" />
</array>
</option>
<option name="errors">
<ArgumentParseErrors />
</option>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7">
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Gradle: junit:junit:4.12@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-integration:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-library:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-core:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: net.sf.kxml:kxml2:2.3.0@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.squareup:javawriter:2.1.1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.google.code.findbugs:jsr305:2.0.1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.ext:junit:1.1.1@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.espresso:espresso-core:3.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:runner:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:core:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:monitor:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.espresso:espresso-idling-resource:3.2.0@aar" level="project" />
<orderEntry type="library" name="Gradle: de.robv.android.xposed:api:82@jar" level="project" />
<orderEntry type="library" name="Gradle: de.robv.android.xposed:api:82:sources@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.50@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50@jar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp:4.4.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:2.4.3@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib:1.3.61@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-common:1.3.61@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:13.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.collection:collection:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.google.code.gson:gson:2.8.6@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.core:core-ktx:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.fragment:fragment:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.viewpager:viewpager:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.loader:loader:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.activity:activity:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.customview:customview:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.core:core:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.savedstate:savedstate:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.interpolator:interpolator:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-runtime:2.0.0@aar" level="project" />
</component>
</module>

21
kernel/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.magic.kernel" />

View File

@@ -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 <T> lazy(name: String, crossinline initializer: () -> T?): Lazy<T> {
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<out T>(private val initializer: () -> T) : Lazy<T>,
java.io.Serializable {
@Volatile
private var lazyValue: Lazy<T> = 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()
}
}
}
}

View File

@@ -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<Any>?, centers: List<HookerCenter>) {
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<Any>?, centers: List<HookerCenter>) {
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<Any>?) {
val providers = plugins?.filter { it is IHookerProvider } ?: listOf()
providers.parallelForEach {
(it as IHookerProvider).provideStaticHookers()?.forEach { hooker ->
if (!hooker.hasHooked) {
XposedUtil.postHooker(hooker)
}
}
}
}
}

View File

@@ -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!!)
}
}

View File

@@ -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
}
}
}

View File

@@ -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<in ByteArray>,
val timestamp: Long = System.currentTimeMillis()): SoftReference<ByteArray>(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()
}
}

View File

@@ -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<String, Any>? = null, content: ByteArray): String? =
diskCache.cache(cachePath(dir, key, params), content)
fun cacheInDisk(dir: String = "", key: String, params: Map<String, Any>? = null, content: ByteArray, callback: (String?) -> Unit) =
diskCache.cache(cachePath(dir, key, params), content, callback)
fun getFromDisk(dir: String = "", key: String, params: Map<String, Any>? = null): ByteArray? =
diskCache.get(cachePath(dir, key, params))
fun getFromDisk(dir: String = "", key: String, params: Map<String, Any>?, callback: (ByteArray?) -> Unit) =
diskCache.get(cachePath(dir, key, params), callback)
fun cacheDiskPath(dir: String = "", key: String, params: Map<String, Any>? = null): String? =
diskCache.exists(cachePath(dir, key, params))
/** ---- 磁盘缓存 ---- */
fun cacheInMemory(key: String, params: Map<String, Any>? = null, content: ByteArray) =
memoryCache.cache(realKey(key, params), content)
fun getEntryFromMemory(key: String, params: Map<String, Any>? = null): ByteArrayEntry? =
memoryCache.getEntry(realKey(key, params))
fun getByteArrayFromMemory(key: String, params: Map<String, Any>? = null): ByteArray? =
memoryCache.getByteArray(realKey(key, params))
fun cachePath(dir: String, key: String, params: Map<String, Any>? = null, md5: Boolean = false): String =
getCacheDir(dir) + File.separator + realKey(key, params, md5)
private fun realKey(key: String, params: Map<String, Any>? = 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
}
}
}

View File

@@ -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<String, Long> =
object : LinkedHashMap<String, Long>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Long>?): 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<TreeMap<Long, String>>(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<String?>(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<ByteArray?>(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<Unit>(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<Unit>(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<Long, String> {
val treeMap = TreeMap<Long, String> { 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
}
}

View File

@@ -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<String, ByteArrayEntry>
private val referenceQueue = ReferenceQueue<ByteArray>()
init {
linkedHashMap = object : LinkedHashMap<String, ByteArrayEntry>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, ByteArrayEntry>?): 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
}
}
}

View File

@@ -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<Any>::class.java
val StringArray = Array<String>::class.java
}

View File

@@ -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
}
}
}

View File

@@ -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<Class<*>>
private val observers: MutableMap<String, Set<Any>> = 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<Any>? = 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 <T : Any> notifyForResults(event: String, action: (Any) -> T?): List<T> {
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 -> {}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.magic.kernel.core
interface IHookerProvider {
fun provideStaticHookers(): List<Hooker>? = null
fun provideEventHooker(event: String): Hooker? = null
}

View File

@@ -0,0 +1,46 @@
package com.magic.kernel.core
/**
* 当插件监听到某个事件发生, 并拦截到相应的函数调用的时候, 插件可能会需要对拦截住的函数进行某些操作, 这个操作需要被封
* 装成一个 [Operation] 对象传递给 SpellBook 框架
*/
class Operation<out T>(
val value: T? = null,
val error: Throwable? = null,
val priority: Int = 0,
val returnEarly: Boolean = false
) {
companion object {
/**
* 创建一个空操作, 表明自己什么也不做
*/
@JvmStatic
fun <T> nop(priority: Int = 0): Operation<T> {
return Operation(priority = priority)
}
/**
* 创建一个打断操作, 跳过原函数的执行, 直接抛出一个异常
*
* @param error 要抛出的异常
* @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的
* 情况下随机选择一个操作
*/
@JvmStatic
fun <T> interruption(error: Throwable, priority: Int = 0): Operation<T> {
return Operation(error = error, priority = priority, returnEarly = true)
}
/**
* 创建一个替换操作, 跳过原函数的执行, 直接返回一个结果
*
* @param value 要返回的结果
* @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的
* 情况下随机选择一个操作
*/
@JvmStatic
fun <T> replacement(value: T, priority: Int = 0): Operation<T> {
return Operation(value = value, priority = priority, returnEarly = true)
}
}
}

View File

@@ -0,0 +1,39 @@
package com.magic.kernel.core
/**
* 用于比较 Android 版本字符串的类
*/
class Version(private val versionName: String) {
private val version: List<Int> =
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
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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<String>) = types.forEach { this += it }
fun search(packageName: String, depth: Int): List<String> {
if (mutable) return emptyList()
return root.search(packageName, depth)
}
private class TrieNode {
val classes: MutableList<String> = ArrayList(50)
val children: MutableMap<String, TrieNode> = 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<String> {
if (depth == 0) return classes
return children.flatMap { it.value.get(depth - 1) }
}
fun search(packageName: String, depth: Int): List<String> {
return search(packageName, depth, 0)
}
private fun search(packageName: String, depth: Int, pos: Int): List<String> {
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<String> {
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<Int> {
(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)
}
}
}

View File

@@ -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<String, Classes> = ConcurrentHashMap()
private val methodCache: MutableMap<String, Methods> = ConcurrentHashMap()
private val fieldCache: MutableMap<String, Fields> = 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<Class<*>>) {
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<Method>) {
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<Field>) {
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()
}
}
}

View File

@@ -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 <T : Any> trySilently(func: () -> T?): T? =
try { func() } catch (t: Throwable) { null }
@JvmStatic
inline fun <T : Any> tryVerbosely(func: () -> T?): T? =
try { func() } catch (t: Throwable) {
Log.e(TryHelper.javaClass.name, "tryVerbosely error: $t ${t.message}"); null
}
@JvmStatic
inline fun <T: Any> 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 <T: Any> 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 <T: Any> tryAsynchronously(crossinline func: () -> T?) =
mExecutorService.submit {
try { func() } catch (t: Throwable) {
Log.e(TryHelper.javaClass.name, "tryMainThreadly error: ${t.message}")
}
}
@JvmStatic
inline fun <T: Any> tryAsynchronously(crossinline func: () -> T?, crossinline callback: (T?) -> Unit) {
val futureTask = object : FutureTask<T?>(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 <T: Any> tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair<Boolean, T?>) =
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 <T: Any> tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair<Boolean, T?>, crossinline callback: (T?) -> Unit){
val futureTask = object : FutureTask<T?>(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)
}
}

View File

@@ -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) {
//
// }
}

View File

@@ -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()
}
}

View File

@@ -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)
}
/**
* 基于质量的压缩算法, 此方法未 解决压缩后图像失真问题
* <br></br> 可先调用比例压缩适当压缩图片后,再调用此方法可解决上述问题
* @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
}
}

View File

@@ -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)
}
}
})
}
}

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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<String>?, isRoot: Boolean = true, isNeedResultMsg: Boolean = true): Result =
exec((commands ?: listOf()).toTypedArray(), isRoot, isNeedResultMsg)
fun exec(commands: Array<String>?, 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()
)
}
}

View File

@@ -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<String>? = 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
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,67 @@
package com.magic.kernel.utils
object MirrorUtil {
/**
* 返回一个 Object 所声明的所有成员变量(不含基类成员)
*/
@JvmStatic fun collectFields(instance: Any): List<Pair<String, Any>> {
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<Any>): List<Pair<String, String>> {
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<Any>): List<Pair<String, String>> {
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 的准确性, 不对结果进行排序
}
}

View File

@@ -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 <T, R> List<T>.parallelMap(crossinline transform: (T) -> R): List<R> {
val sectionSize = size / processors
val main = List(processors) { mutableListOf<R>() }
(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 <T> Iterable<T>.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)
}
}

View File

@@ -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()
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">kernel</string>
</resources>

View File

@@ -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)
}
}

8
local.properties Normal file
View File

@@ -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

2
settings.gradle Normal file
View File

@@ -0,0 +1,2 @@
include ':app', ':kernel', ':shared', ':wechat', ':wework'
rootProject.name='ExampleWework'

1
shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

40
shared/build.gradle Normal file
View File

@@ -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'
}

View File

21
shared/proguard-rules.pro vendored Normal file
View File

@@ -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

180
shared/shared.iml Normal file
View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":shared" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":shared" />
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.5.3" />
<option name="LAST_KNOWN_AGP_VERSION" value="3.5.3" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="PROJECT_TYPE" value="1" />
</configuration>
</facet>
<facet type="kotlin-language" name="Kotlin">
<configuration version="3" platform="JVM 1.6" allPlatforms="JVM [1.6]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xallow-no-source-files" />
</compilerSettings>
<compilerArguments>
<option name="destination" value="$MODULE_DIR$/build/tmp/kotlin-classes/debug" />
<option name="classpath" value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/de.robv.android.xposed/api/82/35866b507b360d4789ff389ad7386b6e8bbf6cc4/api-82.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/de.robv.android.xposed/api/82/2030f71764b06b2f39fa1a85660690aa834cfd84/api-82-sources.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-android-extensions-runtime/1.3.50/bec16087637a7cafe54894e73d38037977cb30d2/kotlin-android-extensions-runtime-1.3.50.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.3.50/50ad05ea1c2595fb31b800e76db464d08d599af3/kotlin-stdlib-jdk7-1.3.50.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/017f02232106f85c718c5d9c563a2a70/core-ktx-1.1.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.61/4702105e97f7396ae41b113fdbdc180ec1eb1e36/kotlin-stdlib-1.3.61.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.61/65abb71d5afb850b68be03987b08e2c864ca3110/kotlin-stdlib-common-1.3.61.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/aaeb4ac383e46bb146f322ae9032f1e8/appcompat-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/52397f4a71f30dffd3659283f56c49cd/fragment-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/e5cd563b027052f2e3144f14db97c1d3/appcompat-resources-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/91dd8524d7698478ac8f047acd5f85d0/drawerlayout-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/030ed8ffbf8d3c58633260185d89ea67/viewpager-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/45f8209ee39e13f3cab9d7b0e8f651dd/loader-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/ac3d32c9cf68316fc66a5ce4d79c2301/activity-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/b27e6623db3562f63e67c3fa3e43e40a/vectordrawable-animated-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/9061978759f410c9baef4ddcb3096803/vectordrawable-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/8e21dbc21a31aecace5ce662d6572f9c/customview-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/264bd0e6e1b5a347721ed7f2deda2aba/core-1.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/260010174060e6b78f60e2af12ec4ee9/cursoradapter-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/d934669c66fd1ce97a07881131ddeb81/versionedparcelable-1.1.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.collection/collection/1.1.0/1f27220b47669781457de0d600849a5de0e89909/collection-1.1.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/49ecb08f235a45c347f2a70ae8f7da78/lifecycle-viewmodel-2.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/98f4ce2537445291cbdb29ffd6d548aa/lifecycle-runtime-2.1.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/19accbd8e07c1fca16f1a2044d0fe930/savedstate-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/11901a9795fa2327e5f523e133b881cf/lifecycle-livedata-2.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/ad662121c850b3f32692c894384c3de6/lifecycle-livedata-core-2.0.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.1.0/c67e7807d9cd6c329b9d0218b2ec4e505dd340b7/lifecycle-common-2.1.0.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/1c3ca424765cca9bbaf5b684b6898978/interpolator-1.0.0-api.jar:/Users/wangcong/.gradle/caches/transforms-2/files-2.1/03f516a6bc22fee39783350c275ccf75/core-runtime-2.0.0-api.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.arch.core/core-common/2.1.0/b3152fc64428c9354344bd89848ecddc09b6f07e/core-common-2.1.0.jar:/Users/wangcong/.gradle/caches/modules-2/files-2.1/androidx.annotation/annotation/1.1.0/e3a6fb2f40e3a3842e6b7472628ba4ce416ea4c8/annotation-1.1.0.jar:/Users/wangcong/Development/Android/XMagicHookerShare/kernel/build/intermediates/compile_library_classes/debug/classes.jar:/Users/wangcong/Development/Android/XMagicHookerShare/shared/build/intermediates/compile_only_not_namespaced_r_class_jar/debug/R.jar:/Users/wangcong/Library/Android/sdk/platforms/android-29/android.jar" />
<option name="noStdlib" value="true" />
<option name="noReflect" value="true" />
<option name="moduleName" value="shared_debug" />
<option name="languageVersion" value="1.3" />
<option name="apiVersion" value="1.3" />
<option name="pluginOptions">
<array>
<option value="plugin:org.jetbrains.kotlin.android:experimental=false" />
<option value="plugin:org.jetbrains.kotlin.android:enabled=true" />
<option value="plugin:org.jetbrains.kotlin.android:defaultCacheImplementation=hashMap" />
</array>
</option>
<option name="pluginClasspaths">
<array>
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.intellij.deps/trove4j/1.0.20181211/216c2e14b070f334479d800987affe4054cd563f/trove4j-1.0.20181211.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-android-extensions/1.3.50/f16428b9ce307d0f5842bd8ed9af1e43a141edd3/kotlin-android-extensions-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable/1.3.50/1251c1768e5769b06c2487d6f6cf8acf6efb8960/kotlin-compiler-embeddable-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-daemon-embeddable/1.3.50/5cb93bb33f4c6f833ead0beca4c831668e00cf52/kotlin-daemon-embeddable-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-reflect/1.3.50/b499f22fd7c3e9c2e5b6c4005221fa47fc7f9a7a/kotlin-reflect-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/1.3.50/59492b8dfb92522ba0ddb5dd1c4d0ef0a4fca1af/kotlin-script-runtime-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.50/3d9cd3e1bc7b92e95f43d45be3bfbcf38e36ab87/kotlin-stdlib-common-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.50/b529d1738c7e98bbfa36a4134039528f2ce78ebf/kotlin-stdlib-1.3.50.jar" />
<option value="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar" />
</array>
</option>
<option name="errors">
<ArgumentParseErrors />
</option>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7">
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Gradle: junit:junit:4.12@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-integration:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-library:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-core:1.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: net.sf.kxml:kxml2:2.3.0@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.squareup:javawriter:2.1.1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.google.code.findbugs:jsr305:2.0.1@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.ext:junit:1.1.1@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.espresso:espresso-core:3.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:runner:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:core:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test:monitor:1.2.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: androidx.test.espresso:espresso-idling-resource:3.2.0@aar" level="project" />
<orderEntry type="library" name="Gradle: de.robv.android.xposed:api:82@jar" level="project" />
<orderEntry type="library" name="Gradle: de.robv.android.xposed:api:82:sources@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.50@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib:1.3.61@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-common:1.3.61@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:13.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.collection:collection:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.core:core-ktx:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.fragment:fragment:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.viewpager:viewpager:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.loader:loader:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.activity:activity:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.customview:customview:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.core:core:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.savedstate:savedstate:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.interpolator:interpolator:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-runtime:2.0.0@aar" level="project" />
<orderEntry type="module" module-name="kernel" />
</component>
</module>

View File

@@ -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)
}
}

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.magic.shared" />

View File

@@ -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<HookerCenter> = listOf(
ActivityHookers
// DatabaseHookers,
// FileHookers
)
}

View File

@@ -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<Class<*>>
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")
}
}

View File

@@ -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) {}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">shared</string>
</resources>

View File

@@ -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)
}
}

BIN
sources/demo-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

BIN
sources/demo-2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
sources/my_contact_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
sources/my_contact_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

1
wechat/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

Some files were not shown because too many files have changed in this diff Show More