From fd54628fd708c88a3fd47163677a753f9c9ed2dc Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Fri, 26 Aug 2022 23:46:30 +0800 Subject: [PATCH] Modify move documentation and replace applyTheme to applyModuleTheme --- docs/api/public/YukiHookFactory.md | 271 +----------------- docs/guide/special-feature.md | 251 ++++++++++++++++ .../hook/factory/YukiHookFactory.kt | 12 +- 3 files changed, 260 insertions(+), 274 deletions(-) diff --git a/docs/api/public/YukiHookFactory.md b/docs/api/public/YukiHookFactory.md index eb114716..f70cd732 100644 --- a/docs/api/public/YukiHookFactory.md +++ b/docs/api/public/YukiHookFactory.md @@ -132,67 +132,6 @@ fun Resources.injectModuleAppResources() 注入的资源作用域仅限当前 `Context` 或 `Resources`,你需要在每个用到宿主 `Context` 或 `Resources` 的地方重复调用此方法进行注入才能使用。 -为防止资源 ID 互相冲突,你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。 - -- Kotlin Gradle DSL - -```kotlin -androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64") -``` - -- Groovy - -```groovy -aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64' -``` - -!> 提供的示例资源 ID 值仅供参考,为了防止当前宿主存在多个 Xposed 模块,建议自定义你自己的资源 ID。 - -!> 只能在 (Xposed) 宿主环境使用此功能,其它环境下使用将不生效且会打印警告信息。 - -**功能示例** - -在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注入当前模块资源。 - -> 示例如下 - -```kotlin -injectMember { - method { - name = "onCreate" - param(BundleClass) - } - afterHook { - instance().also { - // <方案1> 通过 Context 注入模块资源 - it.injectModuleAppResources() - // <方案2> 直接得到宿主 Resources 注入模块资源 - it.resources.injectModuleAppResources() - // 直接使用模块资源 ID - it.getString(R.id.app_name) - } - } -} -``` - -你还可以直接在 `AppLifecycle` 中注入当前模块资源。 - -> 示例如下 - -```kotlin -onAppLifecycle { - onCreate { - // 全局注入模块资源,但仅限于全局生命周期,类似 ImageView.setImageResource 这样的方法在 Activity 中需要单独注入 - // <方案1> 通过 Context 注入模块资源 - injectModuleAppResources() - // <方案2> 直接得到宿主 Resources 注入模块资源 - resources.injectModuleAppResources() - // 直接使用模块资源 ID - getString(R.id.app_name) - } -} -``` - ### Context.registerModuleAppActivities *- ext-method* ```kotlin @@ -213,136 +152,10 @@ fun Context.registerModuleAppActivities(proxy: Any?) 你要将需要在宿主启动的 `Activity` 继承于 `ModuleAppActivity` 或 `ModuleAppCompatActivity`。 -为防止资源 ID 互相冲突,你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。 - -- Kotlin Gradle DSL +### Context.applyModuleTheme *- ext-method* ```kotlin -androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64") -``` - -- Groovy - -```groovy -aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64' -``` - -!> 提供的示例资源 ID 值仅供参考,为了防止当前宿主存在多个 Xposed 模块,建议自定义你自己的资源 ID。 - -!> 只能在 (Xposed) 宿主环境使用此功能,其它环境下使用将不生效且会打印警告信息。 - -**功能示例** - -在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注册当前模块的 `Activity` 代理。 - -> 示例如下 - -```kotlin -injectMember { - method { - name = "onCreate" - param(BundleClass) - } - afterHook { - instance().registerModuleAppActivities() - } -} -``` - -你还可以直接在 `AppLifecycle` 中注册当前模块的 `Activity` 代理。 - -> 示例如下 - -```kotlin -onAppLifecycle { - onCreate { - registerModuleAppActivities() - } -} -``` - -如果没有填写 `proxy` 参数,API 将会根据当前 `Context` 自动获取当前宿主的启动入口 `Activity` 进行代理。 - -通常情况下,它是有效的,但是以上情况在一些 APP 中会失效,例如一些 `Activity` 会在注册清单上加入启动参数,那么我们就需要使用另一种解决方案。 - -若未注册的 `Activity` 不能被正确启动,我们可以手动拿到宿主的 `AndroidManifest.xml` 进行分析,来得到一个注册过的 `Activity` 标签,获取其中的 `name`。 - -你需要选择一个当前宿主可能用不到的、不需要的 `Activity` 作为一个“傀儡”将其进行代理,通常是有效的。 - -比如我们已经找到了能够被代理的合适 `Activity`。 - -> 示例如下 - -```xml - -``` - -根据其中的 `name`,我们只需要在方法中加入这个参数进行注册即可。 - -> 示例如下 - -```kotlin -registerModuleAppActivities(proxy = "com.demo.test.activity.TestActivity") -``` - -另一种情况,如果你对宿主的类编写了一个 `stub`,那么你可以直接通过 `Class` 对象来进行注册。 - -> 示例如下 - -```kotlin -registerModuleAppActivities(TestActivity::class.java) -``` - -注册完成后,请将你需要使用宿主启动的模块中的 `Activity` 继承于 `ModuleAppActivity` 或 `ModuleAppCompatActivity`。 - -这些 `Activity` 现在无需注册即可无缝存活于宿主中。 - -> 示例如下 - -```kotlin -class HostTestActivity : ModuleAppActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // 模块资源已被自动注入,可以直接使用 xml 装载布局 - setContentView(R.layout.activity_main) - } -} -``` - -若你需要继承于 `ModuleAppCompatActivity`,你需要手动设置 AppCompat 主题。 - -> 示例如下 - -```kotlin -class HostTestActivity : ModuleAppCompatActivity() { - - // 这里的主题名称仅供参考,请填写你模块中已有的主题名称 - override val moduleTheme get() = R.style.Theme_AppCompat - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // 模块资源已被自动注入,可以直接使用 xml 装载布局 - setContentView(R.layout.activity_main) - } -} -``` - -以上步骤全部完成后,你就可以在 (Xposed) 宿主环境任意存在 `Context` 的地方愉快地调用 `startActivity` 了。 - -> 示例如下 - -```kotlin -val context: Context = ... // 假设这就是你的 Context -context.startActivity(context, HostTestActivity::class.java) -``` - -### Context.applyTheme *- ext-method* - -```kotlin -fun Context.applyTheme(theme: Int, configuration: Configuration?): ModuleContextThemeWrapper +fun Context.applyModuleTheme(theme: Int, configuration: Configuration?): ModuleContextThemeWrapper ``` **变更记录** @@ -351,90 +164,12 @@ fun Context.applyTheme(theme: Int, configuration: Configuration?): ModuleContext **功能描述** -> 生成一个 `ContextThemeWrapper` 代理以应用主题资源。 +> 生成一个 `ContextThemeWrapper` 代理以应用当前 Xposed 模块的主题资源。 在 Hook APP (宿主) 中使用此方法会自动调用 `injectModuleAppResources` 注入当前 Xposed 模块的资源。 如果在 Hook APP (宿主) 中使用此方法发生 `ClassCastException`,请手动设置 `configuration`。 -为防止资源 ID 互相冲突,你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。 - -- Kotlin Gradle DSL - -```kotlin -androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64") -``` - -- Groovy - -```groovy -aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64' -``` - -!> 提供的示例资源 ID 值仅供参考,为了防止当前宿主存在多个 Xposed 模块,建议自定义你自己的资源 ID。 - -**功能示例** - -有时候,我们需要使用 `MaterialAlertDialogBuilder` 来美化自己在宿主中的对话框,但是拿不到 AppCompat 主题就无法创建。 - -- 会得到如下异常 - -``` -The style on this component requires your app theme to be Theme.AppCompat (or a descendant). -``` - -这时,我们想在宿主被 Hook 的当前 `Activity` 中使用 `MaterialAlertDialogBuilder` 来创建对话框,就可以有如下方法。 - -> 示例如下 - -```kotlin -injectMember { - method { - name = "onCreate" - param(BundleClass) - } - afterHook { - // 使用 applyTheme 创建一个当前模块中的主题资源 - val appCompatContext = instance().applyTheme(R.style.Theme_AppCompat) - // 直接使用这个包装了模块主题后的 Context 创建对话框 - MaterialAlertDialogBuilder(appCompatContext) - .setTitle("AppCompat 主题对话框") - .setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。") - .setPositiveButton("确定", null) - .show() - } -} -``` - -你还可以对当前 `Context` 通过 `uiMode` 设置原生的夜间模式和日间模式,至少需要 Android 10 及以上系统版本支持且当前主题包含夜间模式相关元素。 - -> 示例如下 - -```kotlin -injectMember { - method { - name = "onCreate" - param(BundleClass) - } - afterHook { - // 定义当前模块中的主题资源 - var appCompatContext: ModuleContextThemeWrapper - // <方案1> 直接得到 Configuration 对象设置 - appCompatContext = instance().applyTheme(R.style.Theme_AppCompat).applyConfiguration { uiMode = Configuration.UI_MODE_NIGHT_YES } - // <方案2> 创建一个新的 Configuration 对象,但会破坏当前宿主中原有的字体缩放大小等设置,你需要手动重新传递 densityDpi 等参数 - appCompatContext = instance().applyTheme(R.style.Theme_AppCompat, Configuration().apply { uiMode = Configuration.UI_MODE_NIGHT_YES }) - // 直接使用这个包装了模块主题后的 Context 创建对话框 - MaterialAlertDialogBuilder(appCompatContext) - .setTitle("AppCompat 主题对话框") - .setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。") - .setPositiveButton("确定", null) - .show() - } -} -``` - -这样,我们就可以在宿主中非常简单地使用 `MaterialAlertDialogBuilder` 创建对话框了。 - ### ~~isSupportResourcesHook *- field*~~ **变更记录** diff --git a/docs/guide/special-feature.md b/docs/guide/special-feature.md index d96366eb..4c8e3b06 100644 --- a/docs/guide/special-feature.md +++ b/docs/guide/special-feature.md @@ -1492,5 +1492,256 @@ loadApp(name = "com.example.demo") { 详情请参考 [AppLifecycle](api/document?id=applifecycle-class)。 +## 宿主资源注入扩展功能 + +> 这是一个将模块资源、`Activity` 组件以及 `Context` 主题注入到宿主的扩展功能。 + +在使用以下功能之前,为防止资源 ID 互相冲突,你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。 + +- Kotlin Gradle DSL + +```kotlin +android { + androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64") +} +``` + +- Groovy + +```groovy +android { + aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64' +} +``` + +!> 提供的示例资源 ID 值仅供参考,不可使用 `0x7f`,默认为 `0x64`,为了防止当前宿主存在多个 Xposed 模块,建议自定义你自己的资源 ID。 + +### 注入模块资源 (Resources) + +在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注入当前模块资源。 + +> 示例如下 + +```kotlin +injectMember { + method { + name = "onCreate" + param(BundleClass) + } + afterHook { + instance().also { + // <方案1> 通过 Context 注入模块资源 + it.injectModuleAppResources() + // <方案2> 直接得到宿主 Resources 注入模块资源 + it.resources.injectModuleAppResources() + // 直接使用模块资源 ID + it.getString(R.id.app_name) + } + } +} +``` + +你还可以直接在 `AppLifecycle` 中注入当前模块资源。 + +> 示例如下 + +```kotlin +onAppLifecycle { + onCreate { + // 全局注入模块资源,但仅限于全局生命周期,类似 ImageView.setImageResource 这样的方法在 Activity 中需要单独注入 + // <方案1> 通过 Context 注入模块资源 + injectModuleAppResources() + // <方案2> 直接得到宿主 Resources 注入模块资源 + resources.injectModuleAppResources() + // 直接使用模块资源 ID + getString(R.id.app_name) + } +} +``` + +详情请参考 [Context+Resources.injectModuleAppResources](api/document?id=contextresourcesinjectmoduleappresources-ext-method)。 + +### 注册模块 Activity + +在 Android 系统中所有应用的 `Activity` 启动时,都需要在 `AndroidManifest.xml` 中进行注册,在 Hook 过程中,如果我们想通过宿主来直接启动模块中未注册的 `Activity` 要怎么做呢? + +在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注册当前模块的 `Activity` 代理。 + +> 示例如下 + +```kotlin +injectMember { + method { + name = "onCreate" + param(BundleClass) + } + afterHook { + instance().registerModuleAppActivities() + } +} +``` + +你还可以直接在 `AppLifecycle` 中注册当前模块的 `Activity` 代理。 + +> 示例如下 + +```kotlin +onAppLifecycle { + onCreate { + registerModuleAppActivities() + } +} +``` + +如果没有填写 `proxy` 参数,API 将会根据当前 `Context` 自动获取当前宿主的启动入口 `Activity` 进行代理。 + +通常情况下,它是有效的,但是以上情况在一些 APP 中会失效,例如一些 `Activity` 会在注册清单上加入启动参数,那么我们就需要使用另一种解决方案。 + +若未注册的 `Activity` 不能被正确启动,我们可以手动拿到宿主的 `AndroidManifest.xml` 进行分析,来得到一个注册过的 `Activity` 标签,获取其中的 `name`。 + +你需要选择一个当前宿主可能用不到的、不需要的 `Activity` 作为一个“傀儡”将其进行代理,通常是有效的。 + +比如我们已经找到了能够被代理的合适 `Activity`。 + +> 示例如下 + +```xml + +``` + +根据其中的 `name`,我们只需要在方法中加入这个参数进行注册即可。 + +> 示例如下 + +```kotlin +registerModuleAppActivities(proxy = "com.demo.test.activity.TestActivity") +``` + +另一种情况,如果你对宿主的类编写了一个 `stub`,那么你可以直接通过 `Class` 对象来进行注册。 + +> 示例如下 + +```kotlin +registerModuleAppActivities(TestActivity::class.java) +``` + +注册完成后,请将你需要使用宿主启动的模块中的 `Activity` 继承于 `ModuleAppActivity` 或 `ModuleAppCompatActivity`。 + +这些 `Activity` 现在无需注册即可无缝存活于宿主中。 + +> 示例如下 + +```kotlin +class HostTestActivity : ModuleAppActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // 模块资源已被自动注入,可以直接使用 xml 装载布局 + setContentView(R.layout.activity_main) + } +} +``` + +若你需要继承于 `ModuleAppCompatActivity`,你需要手动设置 AppCompat 主题。 + +> 示例如下 + +```kotlin +class HostTestActivity : ModuleAppCompatActivity() { + + // 这里的主题名称仅供参考,请填写你模块中已有的主题名称 + override val moduleTheme get() = R.style.Theme_AppCompat + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // 模块资源已被自动注入,可以直接使用 xml 装载布局 + setContentView(R.layout.activity_main) + } +} +``` + +以上步骤全部完成后,你就可以在 (Xposed) 宿主环境任意存在 `Context` 的地方愉快地调用 `startActivity` 了。 + +> 示例如下 + +```kotlin +val context: Context = ... // 假设这就是你的 Context +context.startActivity(context, HostTestActivity::class.java) +``` + +详情请参考 [Context.registerModuleAppActivities](api/document?id=contextregistermoduleappactivities-ext-method)。 + +### 创建 ContextThemeWrapper 代理 + +有时候,我们需要使用 `MaterialAlertDialogBuilder` 来美化自己在宿主中的对话框,但是拿不到 AppCompat 主题就无法创建。 + +- 会得到如下异常 + +``` +The style on this component requires your app theme to be Theme.AppCompat (or a descendant). +``` + +这时,我们想在宿主被 Hook 的当前 `Activity` 中使用 `MaterialAlertDialogBuilder` 来创建对话框,就可以有如下方法。 + +> 示例如下 + +```kotlin +injectMember { + method { + name = "onCreate" + param(BundleClass) + } + afterHook { + // 使用 applyModuleTheme 创建一个当前模块中的主题资源 + val appCompatContext = instance().applyModuleTheme(R.style.Theme_AppCompat) + // 直接使用这个包装了模块主题后的 Context 创建对话框 + MaterialAlertDialogBuilder(appCompatContext) + .setTitle("AppCompat 主题对话框") + .setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。") + .setPositiveButton("确定", null) + .show() + } +} +``` + +你还可以对当前 `Context` 通过 `uiMode` 设置原生的夜间模式和日间模式,至少需要 Android 10 及以上系统版本支持且当前主题包含夜间模式相关元素。 + +> 示例如下 + +```kotlin +injectMember { + method { + name = "onCreate" + param(BundleClass) + } + afterHook { + // 定义当前模块中的主题资源 + var appCompatContext: ModuleContextThemeWrapper + // <方案1> 直接得到 Configuration 对象设置 + appCompatContext = instance().applyModuleTheme(R.style.Theme_AppCompat).applyConfiguration { uiMode = Configuration.UI_MODE_NIGHT_YES } + // <方案2> 创建一个新的 Configuration 对象,但会破坏当前宿主中原有的字体缩放大小等设置,你需要手动重新传递 densityDpi 等参数 + appCompatContext = instance().applyModuleTheme(R.style.Theme_AppCompat, Configuration().apply { uiMode = Configuration.UI_MODE_NIGHT_YES }) + // 直接使用这个包装了模块主题后的 Context 创建对话框 + MaterialAlertDialogBuilder(appCompatContext) + .setTitle("AppCompat 主题对话框") + .setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。") + .setPositiveButton("确定", null) + .show() + } +} +``` + +这样,我们就可以在宿主中非常简单地使用 `MaterialAlertDialogBuilder` 创建对话框了。 + +- 可能存在的问题 + +由于一些 APP 自身使用的 `androidx` 依赖库或自定义主题可能会对当前 `MaterialAlertDialog` 实际样式造成干扰,你可以参考 [模块 APP Demo](https://github.com/fankes/YukiHookAPI/tree/master/demo-module) 来修复这个问题。 + +某些 APP 在创建时可能会发生 `ClassCastException` 异常,请手动指定新的 `Configuration` 实例来进行修复。 + +详情请参考 [Context.applyModuleTheme](api/document?id=contextapplymoduletheme-ext-method)。 +

[浏览下一篇  ➡️](guide/move-to-new-api.md) \ No newline at end of file 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 b076edd6..dd0b65cf 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 @@ -116,7 +116,7 @@ val Context.processName * * 注入的资源作用域仅限当前 [Context] - 你需要在每个用到宿主 [Context] 的地方重复调用此方法进行注入才能使用 * - * 详情请参考 [API 文档 - Context+Resources.injectModuleAppResources](https://fankes.github.io/YukiHookAPI/#/api/document?id=contextresourcesinjectmoduleappresources-ext-method) + * 详情请参考 [注入模块资源 (Resources)](https://fankes.github.io/YukiHookAPI/#/guide/special-feature?id=%e6%b3%a8%e5%85%a5%e6%a8%a1%e5%9d%97%e8%b5%84%e6%ba%90-resources) * * - ❗只能在 (Xposed) 宿主环境使用此功能 - 其它环境下使用将不生效且会打印警告信息 */ @@ -129,7 +129,7 @@ fun Context.injectModuleAppResources() = resources?.injectModuleAppResources() * * 注入的资源作用域仅限当前 [Resources] - 你需要在每个用到宿主 [Resources] 的地方重复调用此方法进行注入才能使用 * - * 详情请参考 [API 文档 - Context+Resources.injectModuleAppResources](https://fankes.github.io/YukiHookAPI/#/api/document?id=contextresourcesinjectmoduleappresources-ext-method) + * 详情请参考 [注入模块资源 (Resources)](https://fankes.github.io/YukiHookAPI/#/guide/special-feature?id=%e6%b3%a8%e5%85%a5%e6%a8%a1%e5%9d%97%e8%b5%84%e6%ba%90-resources) * * - ❗只能在 (Xposed) 宿主环境使用此功能 - 其它环境下使用将不生效且会打印警告信息 */ @@ -144,7 +144,7 @@ fun Resources.injectModuleAppResources() = AppParasitics.injectModuleAppResource * * - 你要将需要在宿主启动的 [Activity] 继承于 [ModuleAppActivity] 或 [ModuleAppCompatActivity] * - * 详情请参考 [API 文档 - Context.registerModuleAppActivities](https://fankes.github.io/YukiHookAPI/#/api/document?id=contextregistermoduleappactivities-ext-method) + * 详情请参考 [注册模块 Activity](https://fankes.github.io/YukiHookAPI/#/guide/special-feature?id=%e6%b3%a8%e5%86%8c%e6%a8%a1%e5%9d%97-activity) * * - ❗只能在 (Xposed) 宿主环境使用此功能 - 其它环境下使用将不生效且会打印警告信息 * @param proxy 代理的 [Activity] - 必须存在于宿主的 AndroidMainifest 清单中 - 不填使用默认 [Activity] @@ -152,18 +152,18 @@ fun Resources.injectModuleAppResources() = AppParasitics.injectModuleAppResource fun Context.registerModuleAppActivities(proxy: Any? = null) = AppParasitics.registerModuleAppActivities(context = this, proxy) /** - * 生成一个 [ContextThemeWrapper] 代理以应用主题资源 + * 生成一个 [ContextThemeWrapper] 代理以应用当前 Xposed 模块的主题资源 * * 在 Hook APP (宿主) 中使用此方法会自动调用 [injectModuleAppResources] 注入当前 Xposed 模块的资源 * * - 如果在 Hook APP (宿主) 中使用此方法发生 [ClassCastException] - 请手动设置新的 [configuration] * - * 详情请参考 [API 文档 - Context.applyTheme](https://fankes.github.io/YukiHookAPI/#/api/document?id=contextapplytheme-ext-method) + * 详情请参考 [创建 ContextThemeWrapper 代理](https://fankes.github.io/YukiHookAPI/#/guide/special-feature?id=%e5%88%9b%e5%bb%ba-contextthemewrapper-%e4%bb%a3%e7%90%86) * @param theme 主题资源 ID * @param configuration 使用的 [Configuration] - 默认空 * @return [ModuleContextThemeWrapper] */ -fun Context.applyTheme(@StyleRes theme: Int, configuration: Configuration? = null) = +fun Context.applyModuleTheme(@StyleRes theme: Int, configuration: Configuration? = null) = ModuleContextThemeWrapper.wrapper(baseContext = this, theme, configuration) /**