diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5f999158..ea2048e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,7 +23,7 @@
+ android:value="93" />
(R.id.main_button).setOnClickListener {
+ modulePrefs.apply {
+ putString("data", "这是存储的数据")
+ putBoolean("test_key", true)
+ putString("test_key_name", "存储数据成功,包名:$packageName")
+ }
+ Toast.makeText(this, "存储完成", Toast.LENGTH_SHORT).show()
+ }
+ // for test
AlertDialog.Builder(this)
.setTitle("Hook 方法返回值测试")
- .setMessage(test() + "\n变量:$a\n模块是否已激活:${YukiHookModuleStatus.isActive()}")
+ .setMessage(test() + "\n变量:$a\n模块数据:${xptest()}\n模块是否已激活:${YukiHookModuleStatus.isActive()}")
.setPositiveButton("下一个") { _, _ ->
AlertDialog.Builder(this)
.setTitle("Hook 方法参数测试")
@@ -90,6 +101,10 @@ class MainActivity : AppCompatActivity() {
@Keep
private fun test() = "正常显示的一行文字"
+ // for test
+ @Keep
+ private fun xptest() = "这里是正常的文字"
+
// for test
@Keep
private fun test(string: String) = string
diff --git a/app/src/main/java/com/highcapable/yukihookapi/demo/hook/inject/MainInjecter.kt b/app/src/main/java/com/highcapable/yukihookapi/demo/hook/inject/MainInjecter.kt
index 04998bbb..dcb50da1 100644
--- a/app/src/main/java/com/highcapable/yukihookapi/demo/hook/inject/MainInjecter.kt
+++ b/app/src/main/java/com/highcapable/yukihookapi/demo/hook/inject/MainInjecter.kt
@@ -70,6 +70,13 @@ class MainInjecter : YukiHookXposedInitProxy {
}.set(instance, "这段文字被修改成功了")
}
}
+ injectMember {
+ method {
+ name = "xptest"
+ returnType = StringType
+ }
+ replaceTo(prefs.getString(key = "data", default = "获取 Hook:没数据"))
+ }
injectMember {
method {
name = "toast"
@@ -150,7 +157,11 @@ class MainInjecter : YukiHookXposedInitProxy {
AlertDialog.Builder(instance())
.setCancelable(false)
.setTitle("测试 Hook")
- .setMessage("Hook 已成功")
+ .setMessage(
+ "Hook 已成功\n" +
+ "test_key:${prefs.getBoolean("test_key")}\n" +
+ "test_key_name:${prefs.getString("test_key_name", "默认值")}"
+ )
.setPositiveButton("OK") { _, _ ->
Toast.makeText(instance(), "Hook Success", Toast.LENGTH_SHORT).show()
}.show()
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 4fc24441..d9310287 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,18 +1,22 @@
-
+ android:gravity="center"
+ android:orientation="vertical"
+ tools:context=".MainActivity"
+ tools:ignore="HardcodedText">
+ android:layout_marginBottom="10dp"
+ android:text="Hello World!" />
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi_ksp_xposed/YukiHookXposedProcessor.kt b/yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi_ksp_xposed/YukiHookXposedProcessor.kt
index 90a2bec8..d872b34c 100644
--- a/yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi_ksp_xposed/YukiHookXposedProcessor.kt
+++ b/yukihookapi-ksp-xposed/src/api/kotlin/com/highcapable/yukihookapi_ksp_xposed/YukiHookXposedProcessor.kt
@@ -226,6 +226,7 @@ class YukiHookXposedProcessor : SymbolProcessorProvider {
" object : XC_MethodReplacement() {\n" +
" override fun replaceHookedMethod(param: MethodHookParam?) = true\n" +
" })\n" +
+ " YukiHookAPI.modulePackageName = \"$realPackageName\"\n" +
" YukiHookAPI.onXposedLoaded(lpparam)\n" +
" }\n" +
"}").toByteArray()
diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt
index b99e8d6c..7576c824 100644
--- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt
+++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt
@@ -33,6 +33,7 @@ import android.app.Application
import android.content.Context
import com.highcapable.yukihookapi.YukiHookAPI.configs
import com.highcapable.yukihookapi.YukiHookAPI.encase
+import com.highcapable.yukihookapi.annotation.DoNotUseField
import com.highcapable.yukihookapi.annotation.DoNotUseMethod
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
import com.highcapable.yukihookapi.hook.factory.hasClass
@@ -57,6 +58,14 @@ object YukiHookAPI {
/** Xposed Hook API 方法体回调 */
private var packageParamCallback: (PackageParam.() -> Unit)? = null
+ /**
+ * 预设的 Xposed 模块包名
+ *
+ * - ⚡请勿手动修改 - 会引发未知异常
+ */
+ @DoNotUseField
+ var modulePackageName = ""
+
/**
* 配置 YukiHookAPI
*/
diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt
index 164ca26b..d7bcede9 100644
--- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt
+++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt
@@ -37,6 +37,7 @@ import android.os.Process
import com.highcapable.yukihookapi.YukiHookAPI
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
import com.highcapable.yukihookapi.hook.param.PackageParam
+import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs
import com.highcapable.yukihookapi.hook.xposed.proxy.YukiHookXposedInitProxy
import java.io.BufferedReader
import java.io.File
@@ -55,6 +56,12 @@ fun YukiHookXposedInitProxy.encase(initiate: PackageParam.() -> Unit) = YukiHook
*/
fun YukiHookXposedInitProxy.encase(vararg hooker: YukiBaseHooker) = YukiHookAPI.encase(hooker = hooker)
+/**
+ * 获取模块的存取对象
+ * @return [YukiHookModulePrefs]
+ */
+val Context.modulePrefs get() = YukiHookModulePrefs(context = this)
+
/**
* 获取当前进程名称
* @return [String]
diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt
index 2d9f4015..362b814b 100644
--- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt
+++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt
@@ -34,6 +34,7 @@ import com.highcapable.yukihookapi.annotation.DoNotUseMethod
import com.highcapable.yukihookapi.hook.core.YukiHookCreater
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper
+import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs
/**
* 装载 Hook 的目标 APP 入口对象实现类
@@ -75,6 +76,13 @@ open class PackageParam(private var baseParam: PackageParamWrapper? = null) {
*/
val isFirstApplication get() = packageName == processName
+ /**
+ * 获得当前使用的存取数据对象缓存实例
+ *
+ * @return [YukiHookModulePrefs]
+ */
+ val prefs by lazy { YukiHookModulePrefs() }
+
/**
* 赋值并克隆另一个 [PackageParam]
*
diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/prefs/YukiHookModulePrefs.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/prefs/YukiHookModulePrefs.kt
new file mode 100644
index 00000000..cb9a453a
--- /dev/null
+++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/prefs/YukiHookModulePrefs.kt
@@ -0,0 +1,308 @@
+/**
+ * MIT License
+ *
+ * Copyright (C) 2022 HighCapable
+ *
+ * This file is part of YukiHookAPI.
+ *
+ * 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.
+ *
+ * This file is Created by fankes on 2022/2/8.
+ */
+@file:Suppress(
+ "EXPERIMENTAL_API_USAGE", "SetWorldReadable", "CommitPrefEdits",
+ "DEPRECATION", "WorldReadableFiles", "unused"
+)
+
+package com.highcapable.yukihookapi.hook.xposed.prefs
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.highcapable.yukihookapi.YukiHookAPI
+import de.robv.android.xposed.XSharedPreferences
+import java.io.File
+
+/**
+ * 实现 Xposed 模块的数据存取
+ *
+ * 对接 [SharedPreferences] 和 [XSharedPreferences]
+ *
+ * 在不同环境智能选择存取使用的对象
+ *
+ * - 请注意此功能为实验性功能 - 仅在 LSPosed 环境测试通过
+ *
+ * - 使用 LSPosed 环境请在 AndroidManifests.xml 中将 "xposedminversion" 最低设置为 93
+ *
+ * - 详见 [New XSharedPreferences](https://github.com/LSPosed/LSPosed/wiki/New-XSharedPreferences#for-the-module)
+ *
+ * - 未使用 LSposed 环境请将你的模块 API 降至 26 以下 - YukiHookAPI 将会尝试使用 [makeWorldReadable] 但仍有可能不成功
+ *
+ * - ⚡当你在模块中存取数据的时候 [context] 必须不能是空的
+ * @param context 上下文实例 - 默认空
+ */
+class YukiHookModulePrefs(private val context: Context? = null) {
+
+ /** 存储名称 - 包名 + _preferences */
+ private val prefsName get() = "${YukiHookAPI.modulePackageName.ifBlank { context?.packageName ?: "" }}_preferences"
+
+ /** 是否为 Xposed 环境 */
+ private val isXposedEnvironment = YukiHookAPI.hasXposedBridge
+
+ /** 缓存数据 */
+ private var xPrefCacheKeyValueStrings = HashMap()
+
+ /** 缓存数据 */
+ private var xPrefCacheKeyValueBooleans = HashMap()
+
+ /** 缓存数据 */
+ private var xPrefCacheKeyValueInts = HashMap()
+
+ /** 缓存数据 */
+ private var xPrefCacheKeyValueLongs = HashMap()
+
+ /** 缓存数据 */
+ private var xPrefCacheKeyValueFloats = HashMap()
+
+ /**
+ * 获得 [XSharedPreferences] 对象
+ * @return [XSharedPreferences]
+ */
+ private val xPref by lazy {
+ XSharedPreferences(YukiHookAPI.modulePackageName, prefsName).apply {
+ makeWorldReadable()
+ reload()
+ }
+ }
+
+ /**
+ * 获得 [SharedPreferences] 对象
+ * @return [SharedPreferences]
+ */
+ private val sPref by lazy {
+ try {
+ context?.getSharedPreferences(prefsName, Context.MODE_WORLD_READABLE)
+ ?: error("If you want to use module prefs,you must set the context instance first")
+ } catch (_: Throwable) {
+ context?.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
+ ?: error("If you want to use module prefs,you must set the context instance first")
+ }
+ }
+
+ /** 设置全局可读可写 */
+ private fun makeWorldReadable() = runCatching {
+ File(File(context!!.applicationInfo.dataDir, "shared_prefs"), "$prefsName.xml").apply {
+ setReadable(true, false)
+ setExecutable(true, false)
+ }
+ }
+
+ /**
+ * 获取 [String] 键值
+ *
+ * - 智能识别对应环境读取键值数据
+ * @param key 键值名称
+ * @param default 默认数据 - ""
+ * @return [String]
+ */
+ fun getString(key: String, default: String = "") =
+ (if (isXposedEnvironment)
+ xPrefCacheKeyValueStrings[key].let {
+ (it ?: xPref.getString(key, default) ?: default).let { value ->
+ xPrefCacheKeyValueStrings[key] = value
+ value
+ }
+ }
+ else sPref.getString(key, default) ?: default).let {
+ makeWorldReadable()
+ it
+ }
+
+ /**
+ * 获取 [Boolean] 键值
+ *
+ * - 智能识别对应环境读取键值数据
+ * @param key 键值名称
+ * @param default 默认数据 - false
+ * @return [Boolean]
+ */
+ fun getBoolean(key: String, default: Boolean = false) =
+ (if (isXposedEnvironment)
+ xPrefCacheKeyValueBooleans[key].let {
+ it ?: xPref.getBoolean(key, default).let { value ->
+ xPrefCacheKeyValueBooleans[key] = value
+ value
+ }
+ }
+ else sPref.getBoolean(key, default)).let {
+ makeWorldReadable()
+ it
+ }
+
+ /**
+ * 获取 [Int] 键值
+ *
+ * - 智能识别对应环境读取键值数据
+ * @param key 键值名称
+ * @param default 默认数据 - 0
+ * @return [Int]
+ */
+ fun getInt(key: String, default: Int = 0) =
+ (if (isXposedEnvironment)
+ xPrefCacheKeyValueInts[key].let {
+ it ?: xPref.getInt(key, default).let { value ->
+ xPrefCacheKeyValueInts[key] = value
+ value
+ }
+ }
+ else sPref.getInt(key, default)).let {
+ makeWorldReadable()
+ it
+ }
+
+ /**
+ * 获取 [Float] 键值
+ *
+ * - 智能识别对应环境读取键值数据
+ * @param key 键值名称
+ * @param default 默认数据 - 0f
+ * @return [Float]
+ */
+ fun getFloat(key: String, default: Float = 0f) =
+ (if (isXposedEnvironment)
+ xPrefCacheKeyValueFloats[key].let {
+ it ?: xPref.getFloat(key, default).let { value ->
+ xPrefCacheKeyValueFloats[key] = value
+ value
+ }
+ }
+ else sPref.getFloat(key, default)).let {
+ makeWorldReadable()
+ it
+ }
+
+ /**
+ * 获取 [Long] 键值
+ *
+ * - 智能识别对应环境读取键值数据
+ * @param key 键值名称
+ * @param default 默认数据 - 0L
+ * @return [Long]
+ */
+ fun getLong(key: String, default: Long = 0L) =
+ (if (isXposedEnvironment)
+ xPrefCacheKeyValueLongs[key].let {
+ it ?: xPref.getLong(key, default).let { value ->
+ xPrefCacheKeyValueLongs[key] = value
+ value
+ }
+ }
+ else sPref.getLong(key, default)).let {
+ makeWorldReadable()
+ it
+ }
+
+ /**
+ * 移除全部包含 [key] 的存储数据
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ */
+ fun remove(key: String) {
+ if (isXposedEnvironment) return
+ sPref.edit().remove(key).apply()
+ makeWorldReadable()
+ }
+
+ /**
+ * 存储 [String] 键值
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ * @param value 键值数据
+ */
+ fun putString(key: String, value: String) {
+ if (isXposedEnvironment) return
+ sPref.edit().putString(key, value).apply()
+ makeWorldReadable()
+ }
+
+ /**
+ * 存储 [Boolean] 键值
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ * @param value 键值数据
+ */
+ fun putBoolean(key: String, value: Boolean) {
+ if (isXposedEnvironment) return
+ sPref.edit().putBoolean(key, value).apply()
+ makeWorldReadable()
+ }
+
+ /**
+ * 存储 [Int] 键值
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ * @param value 键值数据
+ */
+ fun putInt(key: String, value: Int) {
+ if (isXposedEnvironment) return
+ sPref.edit().putInt(key, value).apply()
+ makeWorldReadable()
+ }
+
+ /**
+ * 存储 [Float] 键值
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ * @param value 键值数据
+ */
+ fun putFloat(key: String, value: Float) {
+ if (isXposedEnvironment) return
+ sPref.edit().putFloat(key, value).apply()
+ makeWorldReadable()
+ }
+
+ /**
+ * 存储 [Long] 键值
+ *
+ * - 在模块 [Context] 环境中使用
+ *
+ * - ⚡在 [XSharedPreferences] 环境下只读 - 无法使用
+ * @param key 键值名称
+ * @param value 键值数据
+ */
+ fun putLong(key: String, value: Long) {
+ if (isXposedEnvironment) return
+ sPref.edit().putLong(key, value).apply()
+ makeWorldReadable()
+ }
+}
\ No newline at end of file