init
23
.gitignore
vendored
Normal 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
@@ -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.
|
||||
81
README.md
@@ -1,2 +1,79 @@
|
||||
# wework
|
||||
【xposed wework wechat 企业微信 微信 逆向】自动抢回复 会话 自动通过 好友列表 群管理 机器人 SDK ,底层需要 Xposed 或 VirtualXposed 等Hooking框架的支持,如果你手机安装有xposed框架,那么可以下载源码直接运行
|
||||
|
||||
一个使用Kotlin编写的半开源企业插件框架,底层需要 Xposed 或 VirtualXposed 等Hooking框架的支持,目前项目主要针对wework进行逆向学习。
|
||||
|
||||
### 最重要事情
|
||||
|
||||
**【免责声明】:**
|
||||
此系列文章主要关于xposed的相关学习,以下所提及到的所有方式皆为学习,如有他人使用本系列学习文章中所提及的知识点用于其他非法用途,本人不承担由此造成的任何后果!!
|
||||
|
||||
|
||||
### 全部功能文档列表,持续更新
|
||||
|
||||
当前适配版本从2.8.12 ~ **3.0.36**最新版
|
||||
|
||||
**主要功能列表**
|
||||
|
||||
- 好友、联系人、用户相关
|
||||
|
||||
> - 联系人列表监听:好友数量变化、联系人变化、同步状态变化等
|
||||
> - 修改联系人备注修改:备注名、企业名、电话、描述、备注的名片或图片
|
||||
> - 内部成员备注修改:备注名、描述
|
||||
> - 联系人操作:通过id获取、添加联系人、删除联系人、搜索联系人、通过好友申请、拒绝添加、删除申请、标记客户等等
|
||||
> - 用户信息:获取公司信息、获取二维码、获取不同风格的二维码、修改头像、修改职务等
|
||||
> - 部门联系人:获取所有部门信息、获取部门内部信息、获取组织架构、获取父级部门、获取子部门等
|
||||
|
||||
- 会话相关
|
||||
|
||||
> - 群列表监听:同步状态、添加到群内、退出群聊
|
||||
> - 群会话监听:添加成员、群主变化、收到消息、群名称变化、成员变化等等
|
||||
> - 会话操作:获取列表、退出群聊、创建群聊、解散群聊、修改群名称、修改群内昵称、邀请成员、移除成员、搜索会话及联系人、获取群二维码等等
|
||||
> - 群操作:设置群主、设置入群验证、设置禁言、置顶、保存到通讯录、设置免打扰等等
|
||||
> - 会话信息:最近消息、成员信息、会话名称、头像、判定是否包含某成员、会话扩展信息、判定是否为微信用户等等
|
||||
|
||||
- 消息相关
|
||||
|
||||
> - 发送消息:包括但不限于文字、图片、语音、视频、文件、小程序、链接、地理位置等
|
||||
> - 接收消息:包括但不限于文字、图片、语音、视频、文件、小程序、链接、地理位置等
|
||||
> - 文件消息自动下载:图片、语音、文件、视频
|
||||
|
||||
- 企业微信与微信公用
|
||||
|
||||
> - Activity
|
||||
> - 文件操作(写入、读取)
|
||||
> - 数据库操作(增删改查)
|
||||
|
||||
- 基础核心功能
|
||||
|
||||
> - APK自动解析
|
||||
> - 异步批处理
|
||||
> - 二级缓存
|
||||
> - 网络请求
|
||||
> - 重试策略
|
||||
> - 文件下载
|
||||
> - 自动缓存
|
||||
> - 反射查找
|
||||
> - silk音频编解码
|
||||
|
||||
|
||||
当然还有更多的功能不仅限于上述,更多可以查阅我针对企业微信的xposed学习的成果,这些成果的部分我将在后续通过讲解并上传
|
||||
|
||||
为了保证执行的可靠稳定性,针对上述功能**在客户端**设计了关于指令的队列处理,解决了很多复杂场景下的问题
|
||||
|
||||
如果需要查阅具体接口文档可以与我联系申请查看,我将毫无保留的开放设计理念和文档
|
||||
|
||||
### SDK已经可以使用了
|
||||
以下是根据SDK开发出来的demo,欢迎交流
|
||||
|
||||

|
||||

|
||||
|
||||
### 注意:
|
||||
为了避免某些xxx风险,我只是持续做一些分享,但并不会将完整代码上传,我所上传的基础核心,基本上你都可以在我所写的文章及有Android基础之上一步一步的去实现
|
||||
|
||||
### 联系我
|
||||
|
||||
如果你在学习过程中遇到问题,你可以直接提交issue,或者直接联系我,请添加时备注:xposed、wework+姓
|
||||
|
||||
<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
@@ -0,0 +1 @@
|
||||
/build
|
||||
184
app/app.iml
Normal file
86
app/build.gradle
Normal 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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
56
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
1
app/src/main/assets/xposed_init
Normal file
@@ -0,0 +1 @@
|
||||
com.magic.xmagichooker.Hooker
|
||||
76
app/src/main/java/com/magic/xmagichooker/Hooker.kt
Normal 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, "开始启动个人微信插件")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
app/src/main/java/com/magic/xmagichooker/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
470
app/src/main/java/com/magic/xmagichooker/Plugins.kt
Normal 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")
|
||||
// }
|
||||
}
|
||||
34
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal 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>
|
||||
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
20
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
6
app/src/main/res/values/colors.xml
Normal 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>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">XMagicHooker</string>
|
||||
</resources>
|
||||
11
app/src/main/res/values/styles.xml
Normal 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>
|
||||
17
app/src/test/java/com/magic/xmagichooker/ExampleUnitTest.kt
Normal 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
@@ -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
@@ -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
|
||||
|
||||
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
|
||||
41
kernel/.cxx/ndk_locator_record.json
Normal 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
@@ -0,0 +1 @@
|
||||
/build
|
||||
75
kernel/build.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
|
||||
0
kernel/consumer-rules.pro
Normal file
182
kernel/kernel.iml
Normal 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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
2
kernel/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.magic.kernel" />
|
||||
122
kernel/src/main/java/com/magic/kernel/MagicGlobal.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
109
kernel/src/main/java/com/magic/kernel/MagicHooker.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
kernel/src/main/java/com/magic/kernel/async/AsyncHandler.kt
Normal 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!!)
|
||||
}
|
||||
}
|
||||
66
kernel/src/main/java/com/magic/kernel/async/CrashHandler.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
kernel/src/main/java/com/magic/kernel/cache/ByteArrayEntry.kt
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
73
kernel/src/main/java/com/magic/kernel/cache/LRUCache.kt
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
148
kernel/src/main/java/com/magic/kernel/cache/LRUDiskCache.kt
vendored
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
63
kernel/src/main/java/com/magic/kernel/cache/LRUMemoryCache.kt
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
kernel/src/main/java/com/magic/kernel/core/Clazz.kt
Normal 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
|
||||
|
||||
}
|
||||
25
kernel/src/main/java/com/magic/kernel/core/Hooker.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
239
kernel/src/main/java/com/magic/kernel/core/HookerCenter.kt
Normal 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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.magic.kernel.core
|
||||
|
||||
interface IHookerProvider {
|
||||
|
||||
fun provideStaticHookers(): List<Hooker>? = null
|
||||
|
||||
fun provideEventHooker(event: String): Hooker? = null
|
||||
|
||||
}
|
||||
46
kernel/src/main/java/com/magic/kernel/core/Operation.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
kernel/src/main/java/com/magic/kernel/core/Version.kt
Normal 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
|
||||
}
|
||||
}
|
||||
36
kernel/src/main/java/com/magic/kernel/core/WaitChannel.kt
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
333
kernel/src/main/java/com/magic/kernel/helper/ParserHelper.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
190
kernel/src/main/java/com/magic/kernel/helper/ReflecterHelper.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
101
kernel/src/main/java/com/magic/kernel/helper/TryHelper.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
49
kernel/src/main/java/com/magic/kernel/okhttp/HttpClients.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
14
kernel/src/main/java/com/magic/kernel/okhttp/ICallbacks.kt
Normal 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
|
||||
|
||||
24
kernel/src/main/java/com/magic/kernel/okhttp/IHttpConfigs.kt
Normal 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,
|
||||
}
|
||||
|
||||
}
|
||||
81
kernel/src/main/java/com/magic/kernel/okhttp/Interceptors.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
kernel/src/main/java/com/magic/kernel/utils/CmdUtil.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
241
kernel/src/main/java/com/magic/kernel/utils/FileUtil.kt
Normal 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
|
||||
}
|
||||
}
|
||||
88
kernel/src/main/java/com/magic/kernel/utils/LogUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
67
kernel/src/main/java/com/magic/kernel/utils/MirrorUtil.kt
Normal 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 的准确性, 不对结果进行排序
|
||||
}
|
||||
|
||||
}
|
||||
52
kernel/src/main/java/com/magic/kernel/utils/ParallelUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
40
kernel/src/main/java/com/magic/kernel/utils/XposedUtil.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
BIN
kernel/src/main/jniLibs/arm64-v8a/libsilk.so
Executable file
BIN
kernel/src/main/jniLibs/armeabi-v7a/libsilk.so
Executable file
3
kernel/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">kernel</string>
|
||||
</resources>
|
||||
17
kernel/src/test/java/com/magic/kernel/ExampleUnitTest.kt
Normal 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
@@ -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
@@ -0,0 +1,2 @@
|
||||
include ':app', ':kernel', ':shared', ':wechat', ':wework'
|
||||
rootProject.name='ExampleWework'
|
||||
1
shared/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
40
shared/build.gradle
Normal 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'
|
||||
}
|
||||
0
shared/consumer-rules.pro
Normal file
21
shared/proguard-rules.pro
vendored
Normal 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
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
2
shared/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.magic.shared" />
|
||||
13
shared/src/main/java/com/magic/shared/apis/SharedEngine.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
3
shared/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">shared</string>
|
||||
</resources>
|
||||
17
shared/src/test/java/com/magic/shared/ExampleUnitTest.kt
Normal 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
|
After Width: | Height: | Size: 230 KiB |
BIN
sources/demo-2.jpeg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
sources/my_contact_01.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
sources/my_contact_02.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
1
wechat/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||