From 2e74254e9766e9574a2efcaa3af3b969deba1750 Mon Sep 17 00:00:00 2001 From: Parallelc Date: Sat, 14 Dec 2024 01:24:42 +0800 Subject: [PATCH] feat: support CSService on Android 15 --- README.md | 18 ++-- README_en.md | 18 ++-- .../java/com/parallelc/micts/ModuleMain.kt | 12 ++- .../parallelc/micts/config/XposedConfig.kt | 10 ++- .../com/parallelc/micts/hooker/CSMSHooker.kt | 86 +++++++++++++++++++ .../com/parallelc/micts/hooker/VIMSHooker.kt | 19 ++-- .../micts/ui/activity/MainActivity.kt | 6 +- 7 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/parallelc/micts/hooker/CSMSHooker.kt diff --git a/README.md b/README.md index c3c0a5f..978801a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MiCTS -[![Downloads](https://img.shields.io/github/downloads/parallelcc/MiCTS/total)](https://github.com/parallelcc/MiCTS/releases) [![Release](https://img.shields.io/github/v/release/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS/releases/latest) +[![Stars](https://img.shields.io/github/stars/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS) [![Downloads](https://img.shields.io/github/downloads/parallelcc/MiCTS/total)](https://github.com/parallelcc/MiCTS/releases) [![Release](https://img.shields.io/github/v/release/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS/releases/latest) 简体中文  |  [English](https://github.com/parallelcc/MiCTS/blob/main/README_en.md) @@ -14,9 +14,10 @@ 2. 安装并打开MiCTS - - 如果幸运的话,在不用LSPosed的情况下,打开MiCTS就会直接触发圈定即搜 - - 如果没有反应,则需要在LSPosed里激活模块,在[MiCTS设置](#进入设置的方式)里开启`Google机型伪装`后,强制重启Google - - 如果还是没有反应,尝试清除Google的数据,然后打开Google,再强制重启Google + - 如果幸运的话,在不需要root的情况下,打开MiCTS就会直接触发圈定即搜 + - 如果没有反应,大概率是因为Google对你的设备禁用了圈定即搜功能(可以通过在Logcat日志中查找`Omni invocation failed: not enabled`确认),在有root的情况下,可以尝试以下方法: + - 在LSPosed里激活模块,在[MiCTS设置](#进入设置的方式)里开启`Google机型伪装`后,强制重启Google + - 如果还是不行,使用[GMS-Flags](https://github.com/polodarb/GMS-Flags),将`com.google.android.apps.search.omnient.device`的flag`45631784`设为true 3. 设置触发方式 @@ -39,8 +40,9 @@ 需要在LSPosed里激活模块 - 系统触发服务:触发所使用的系统服务,只会显示当前支持的选项,依赖作用域选择系统框架 - - VIS:支持Android 9–15,需要将默认助理应用设置为Google,触发时屏幕边缘会闪,没有激活模块的情况下只能使用此服务 + - VIS:支持Android 9–15,需要将默认助理应用设置为Google,触发时一些设备的屏幕边缘会闪,没有激活模块的情况下只能使用此服务 - CSHelper:支持Android 14 QPR3及以上,不需要设置默认助理应用,触发时屏幕边缘不会闪 + - CSService:支持Android 15及以上,圈定即搜专用的服务,效果同CSHelper - 长按小白条触发:仅支持小米设备,依赖作用域选择系统桌面 @@ -57,10 +59,6 @@ ## 常见问题 -### 需要root吗? - -正如[使用方法](#使用方法)第2步中所说,没有root的情况下也可能直接成功触发,这取决于设备的配置,像很多小米设备都可以无需root直接触发,所以你可以尝试一下。但如果不行的话,那原因大概就是没有通过Google的设备检查,因此需要进行机型伪装,这就需要使用LSPosed模块里的功能 - ### 提示“触发失败!” 大概率是没有将Google设为默认助理,检查一下 @@ -69,7 +67,7 @@ Google不是最新版,更新一下 -### 有时屏幕边缘会闪,但无法成功触发,手动打开Google后才会出现刚才圈定即搜的界面 +### 有时无法成功触发,手动打开Google后才会出现刚才圈定即搜的界面 原因应该是墓碑机制导致的,看看手机有没有相关的设置可以把Google加到白名单里,比如电池优化选择无限制等,在模块设置里`系统触发服务`使用`CSHelper`应该没有这个问题 diff --git a/README_en.md b/README_en.md index 4044168..2de8607 100644 --- a/README_en.md +++ b/README_en.md @@ -1,6 +1,6 @@ # MiCTS -[![Downloads](https://img.shields.io/github/downloads/parallelcc/MiCTS/total)](https://github.com/parallelcc/MiCTS/releases) [![Release](https://img.shields.io/github/v/release/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS/releases/latest) +[![Stars](https://img.shields.io/github/stars/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS) [![Downloads](https://img.shields.io/github/downloads/parallelcc/MiCTS/total)](https://github.com/parallelcc/MiCTS/releases) [![Release](https://img.shields.io/github/v/release/parallelcc/MiCTS)](https://github.com/parallelcc/MiCTS/releases/latest) [简体中文](/README.md)  |  English @@ -14,9 +14,10 @@ Trigger Circle to Search on any Android 9–15 device 2. Install and launch MiCTS - - If you're lucky, Circle to Search will be triggered directly without LSPosed when launching MiCTS - - If nothing happened, then activate the module in LSPosed, enable `Device spoof for Google` in the [MiCTS settings](#how-to-enter-settings), and force restart Google - - If it still doesn't work, try clearing Google’s data, then launch Google and force restart it + - If you're lucky, Circle to Search will be triggered directly without root when launching MiCTS + - If nothing happened, most likely it's because Google disabled Circle to Search for your device (you can confirm by checking the message `Omni invocation failed: not enabled` in Logcat). Try the following **with root**: + - Activate the module in LSPosed, enable `Device spoof for Google` in the [MiCTS settings](#how-to-enter-settings), and force restart Google + - If it still doesn't work, then change `com.google.android.apps.search.omnient.device` flag `45631784` to true using [GMS-Flags](https://github.com/polodarb/GMS-Flags) 3. Set up the trigger method @@ -38,8 +39,9 @@ Trigger Circle to Search on any Android 9–15 device ### Module Settings Need to activate the module in LSPosed - System trigger service: The system service used by triggering. Only the services supported will be shown. Need to add System Framework to the scope in LSPosed - - VIS: Supports on Android 9-15. Need to set Google as the default assistant app and the screen edge will flash when triggering. If the module is not activated, only this service will be used + - VIS: Supports on Android 9-15. Need to set Google as the default assistant app and the screen edge will flash when triggering for some devices. If the module is not activated, only this service will be used - CSHelper: Supports on Android 14 QPR3 and above. Don’t need to set Google as the default assistant app and the screen edge will not flash when triggering + - CSService: Supports on Android 15 and above. A dedicated service for Circle to Search, same effect as CSHelper - Trigger by long press gesture handle: Only supports on Xiaomi devices. Need to add System Launcher/POCO Launcher to the scope in LSPosed @@ -56,10 +58,6 @@ Need to activate the module in LSPosed ## FAQ -### Does it require root? - -As mentioned in step 2 of [How to Use](#How-to-Use), it's possible to trigger without root, depending on your device's configuration, for example, many Xiaomi devices can trigger directly without root, so you can give it a try. However, if it doesn't work, it's likely because your device didn't pass Google's device check. In this case, you'll need device spoof, which is what the LSPosed module provides - ### Prompt "Trigger failed!" Most likely because Google is not set as the default assistant, check it @@ -68,7 +66,7 @@ Most likely because Google is not set as the default assistant, check it Ensure that Google is the latest version -### Sometimes, the screen edge flashed but did not trigger successfully, the interface appeared after opening Google +### Sometimes it doesn't trigger successfully, and the interface appears only after opening Google This is likely due to the tombstone mechanism. Check if your device has related settings and add Google to the whitelist, such as selecting "No restrictions" in battery saver diff --git a/app/src/main/java/com/parallelc/micts/ModuleMain.kt b/app/src/main/java/com/parallelc/micts/ModuleMain.kt index 215f3aa..a25a8b6 100644 --- a/app/src/main/java/com/parallelc/micts/ModuleMain.kt +++ b/app/src/main/java/com/parallelc/micts/ModuleMain.kt @@ -6,11 +6,11 @@ import com.parallelc.micts.config.TriggerService import com.parallelc.micts.config.XposedConfig.CONFIG_NAME import com.parallelc.micts.config.XposedConfig.DEFAULT_CONFIG import com.parallelc.micts.config.XposedConfig.KEY_DEVICE_SPOOF -import com.parallelc.micts.config.XposedConfig.KEY_GESTURE_TRIGGER import com.parallelc.micts.config.XposedConfig.KEY_SPOOF_BRAND import com.parallelc.micts.config.XposedConfig.KEY_SPOOF_DEVICE import com.parallelc.micts.config.XposedConfig.KEY_SPOOF_MANUFACTURER import com.parallelc.micts.config.XposedConfig.KEY_SPOOF_MODEL +import com.parallelc.micts.hooker.CSMSHooker import com.parallelc.micts.hooker.InvokeOmniHooker import com.parallelc.micts.hooker.LongPressHomeHooker import com.parallelc.micts.hooker.NavStubViewHooker @@ -39,6 +39,15 @@ class ModuleMain(base: XposedInterface, param: ModuleLoadedParam) : XposedModule log("hook VIMS fail", e) } } + + if (TriggerService.getSupportedServices().contains(TriggerService.CSService)) { + runCatching { + CSMSHooker.hook(param) + }.onFailure { e -> + log("hook CSMS fail", e) + } + } + if (Build.MANUFACTURER == "Xiaomi") { runCatching { LongPressHomeHooker.hook(param) @@ -56,7 +65,6 @@ class ModuleMain(base: XposedInterface, param: ModuleLoadedParam) : XposedModule when (param.packageName) { "com.miui.home", "com.mi.android.globallauncher" -> { - if (!prefs.getBoolean(KEY_GESTURE_TRIGGER, DEFAULT_CONFIG[KEY_GESTURE_TRIGGER] as Boolean)) return runCatching { val circleToSearchHelper = param.classLoader.loadClass("com.miui.home.recents.cts.CircleToSearchHelper") hook(circleToSearchHelper.getDeclaredMethod("invokeOmni", Context::class.java, Int::class.java, Int::class.java), InvokeOmniHooker::class.java) diff --git a/app/src/main/java/com/parallelc/micts/config/XposedConfig.kt b/app/src/main/java/com/parallelc/micts/config/XposedConfig.kt index de30004..0355eb0 100644 --- a/app/src/main/java/com/parallelc/micts/config/XposedConfig.kt +++ b/app/src/main/java/com/parallelc/micts/config/XposedConfig.kt @@ -8,7 +8,15 @@ enum class TriggerService(val isSupported: Boolean) { VIS(true), @SuppressLint("DiscouragedApi") CSHelper(Resources.getSystem().getIdentifier("config_defaultContextualSearchKey", "string", "android") != 0), - ContextualSearchService(false); + @SuppressLint("PrivateApi") + CSService( + try { + Class.forName("android.app.contextualsearch.ContextualSearchManager") + true + } catch (_: Exception) { + false + } + ); companion object { fun getSupportedServices(): List { diff --git a/app/src/main/java/com/parallelc/micts/hooker/CSMSHooker.kt b/app/src/main/java/com/parallelc/micts/hooker/CSMSHooker.kt new file mode 100644 index 0000000..70714a4 --- /dev/null +++ b/app/src/main/java/com/parallelc/micts/hooker/CSMSHooker.kt @@ -0,0 +1,86 @@ +package com.parallelc.micts.hooker + +import android.annotation.SuppressLint +import android.content.Context +import android.os.IBinder +import com.parallelc.micts.module +import io.github.libxposed.api.XposedInterface.BeforeHookCallback +import io.github.libxposed.api.XposedInterface.Hooker +import io.github.libxposed.api.XposedInterface.MethodUnhooker +import io.github.libxposed.api.XposedModuleInterface.SystemServerLoadedParam +import io.github.libxposed.api.annotations.BeforeInvocation +import io.github.libxposed.api.annotations.XposedHooker +import java.lang.reflect.Method + +class CSMSHooker { + companion object { + private var enforcePermission: Method? = null + private var getContextualSearchPackageName: Method? = null + private var contextualSearchPackageName: Int = 0 + + @SuppressLint("PrivateApi") + fun hook(param: SystemServerLoadedParam) { + val rString = param.classLoader.loadClass("com.android.internal.R\$string") + contextualSearchPackageName = rString.getField("config_defaultContextualSearchPackageName").getInt(null) + val systemServer = param.classLoader.loadClass("com.android.server.SystemServer") + module!!.hook(systemServer.getDeclaredMethod("deviceHasConfigString", Context::class.java, Int::class.java), DeviceHasConfigStringHooker::class.java) + + val csms = param.classLoader.loadClass("com.android.server.contextualsearch.ContextualSearchManagerService") + enforcePermission = csms.getDeclaredMethod("enforcePermission", String::class.java) + getContextualSearchPackageName = csms.getDeclaredMethod("getContextualSearchPackageName") + } + + @SuppressLint("PrivateApi") + fun startContextualSearch(entryPoint: Int): Boolean { + var unhookers = mutableListOf>() + return runCatching { + unhookers += module!!.hook(enforcePermission!!, EnforcePermissionHooker::class.java) + unhookers += module!!.hook(getContextualSearchPackageName!!, GetCSPackageNameHooker::class.java) + + val icsmClass = Class.forName("android.app.contextualsearch.IContextualSearchManager") + val cs = Class.forName("android.os.ServiceManager").getMethod("getService", String::class.java).invoke(null, "contextual_search") + val icsm = Class.forName("android.app.contextualsearch.IContextualSearchManager\$Stub").getMethod("asInterface", IBinder::class.java).invoke(null, cs) + icsmClass.getDeclaredMethod("startContextualSearch", Int::class.java).invoke(icsm, entryPoint) + }.onFailure { e -> + module!!.log("invoke startContextualSearch fail", e) + }.also { + unhookers.forEach { unhooker -> unhooker.unhook() } + }.isSuccess + } + + @XposedHooker + class DeviceHasConfigStringHooker : Hooker { + companion object { + @JvmStatic + @BeforeInvocation + fun before(callback: BeforeHookCallback) { + if (callback.args[1] == contextualSearchPackageName) { + callback.returnAndSkip(true) + } + } + } + } + + @XposedHooker + class EnforcePermissionHooker : Hooker { + companion object { + @JvmStatic + @BeforeInvocation + fun before(callback: BeforeHookCallback) { + callback.returnAndSkip(null) + } + } + } + + @XposedHooker + class GetCSPackageNameHooker : Hooker { + companion object { + @JvmStatic + @BeforeInvocation + fun before(callback: BeforeHookCallback) { + callback.returnAndSkip("com.google.android.googlequicksearchbox") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/parallelc/micts/hooker/VIMSHooker.kt b/app/src/main/java/com/parallelc/micts/hooker/VIMSHooker.kt index 9b63080..8c435d3 100644 --- a/app/src/main/java/com/parallelc/micts/hooker/VIMSHooker.kt +++ b/app/src/main/java/com/parallelc/micts/hooker/VIMSHooker.kt @@ -27,11 +27,11 @@ class VIMSHooker { @SuppressLint("PrivateApi") fun hook(param: SystemServerLoadedParam) { - val vims = param.classLoader.loadClass("com.android.server.voiceinteraction.VoiceInteractionManagerService\$VoiceInteractionManagerServiceStub") + val vimsStub = param.classLoader.loadClass("com.android.server.voiceinteraction.VoiceInteractionManagerService\$VoiceInteractionManagerServiceStub") val rString = param.classLoader.loadClass("com.android.internal.R\$string") contextualSearchKey = rString.getField("config_defaultContextualSearchKey").getInt(null) contextualSearchPackageName = rString.getField("config_defaultContextualSearchPackageName").getInt(null) - module!!.hook(vims.getDeclaredMethod("showSessionFromSession", IBinder::class.java, Bundle::class.java, Integer.TYPE, String::class.java), ShowSessionHooker::class.java) + module!!.hook(vimsStub.getDeclaredMethod("showSessionFromSession", IBinder::class.java, Bundle::class.java, Int::class.java, String::class.java), ShowSessionHooker::class.java) } @XposedHooker @@ -40,13 +40,20 @@ class VIMSHooker { @JvmStatic @BeforeInvocation fun before(callback: BeforeHookCallback) : MethodUnhooker? { - return runCatching { - if (!(callback.args[1] as Bundle).getBoolean("micts_trigger", false)) return@runCatching null + runCatching { + val bundle = callback.args[1] as Bundle + if (!bundle.getBoolean("micts_trigger", false)) return@runCatching null Binder.clearCallingIdentity() - module!!.hook(Resources::class.java.getDeclaredMethod("getString", Int::class.java), GetStringHooker::class.java) + val triggerService = module!!.getRemotePreferences(CONFIG_NAME).getInt(KEY_TRIGGER_SERVICE, DEFAULT_CONFIG[KEY_TRIGGER_SERVICE] as Int) + if (triggerService == TriggerService.CSService.ordinal) { + callback.returnAndSkip(CSMSHooker.startContextualSearch(bundle.getInt("omni.entry_point"))) + } else { + return module!!.hook(Resources::class.java.getDeclaredMethod("getString", Int::class.java), GetStringHooker::class.java) + } }.onFailure { e -> module!!.log("hook resources fail", e) - }.getOrNull() + } + return null } @JvmStatic diff --git a/app/src/main/java/com/parallelc/micts/ui/activity/MainActivity.kt b/app/src/main/java/com/parallelc/micts/ui/activity/MainActivity.kt index 9e2891a..3bbf825 100644 --- a/app/src/main/java/com/parallelc/micts/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/parallelc/micts/ui/activity/MainActivity.kt @@ -33,11 +33,7 @@ fun triggerCircleToSearch(entryPoint: Int): Boolean { } }.onFailure { e -> val errMsg = "triggerCircleToSearch failed: " + e.stackTraceToString() - if (module != null) { - module!!.log(errMsg) - } else { - Log.e("MiCTS", errMsg) - } + module?.log(errMsg) ?: Log.e("MiCTS", errMsg) }.getOrDefault(false) }