Skip to content

Commit

Permalink
feat: support CSService on Android 15
Browse files Browse the repository at this point in the history
  • Loading branch information
parallelcc committed Dec 13, 2024
1 parent 26106bb commit 2e74254
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 34 deletions.
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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. 设置触发方式
Expand All @@ -39,8 +40,9 @@
需要在LSPosed里激活模块

- 系统触发服务:触发所使用的系统服务,只会显示当前支持的选项,依赖作用域选择系统框架
- VIS:支持Android 9–15,需要将默认助理应用设置为Google,触发时屏幕边缘会闪,没有激活模块的情况下只能使用此服务
- VIS:支持Android 9–15,需要将默认助理应用设置为Google,触发时一些设备的屏幕边缘会闪,没有激活模块的情况下只能使用此服务
- CSHelper:支持Android 14 QPR3及以上,不需要设置默认助理应用,触发时屏幕边缘不会闪
- CSService:支持Android 15及以上,圈定即搜专用的服务,效果同CSHelper


- 长按小白条触发:仅支持小米设备,依赖作用域选择系统桌面
Expand All @@ -57,10 +59,6 @@

## 常见问题

### 需要root吗?

正如[使用方法](#使用方法)第2步中所说,没有root的情况下也可能直接成功触发,这取决于设备的配置,像很多小米设备都可以无需root直接触发,所以你可以尝试一下。但如果不行的话,那原因大概就是没有通过Google的设备检查,因此需要进行机型伪装,这就需要使用LSPosed模块里的功能

### 提示“触发失败!”

大概率是没有将Google设为默认助理,检查一下
Expand All @@ -69,7 +67,7 @@

Google不是最新版,更新一下

### 有时屏幕边缘会闪,但无法成功触发,手动打开Google后才会出现刚才圈定即搜的界面
### 有时无法成功触发,手动打开Google后才会出现刚才圈定即搜的界面

原因应该是墓碑机制导致的,看看手机有没有相关的设置可以把Google加到白名单里,比如电池优化选择无限制等,在模块设置里`系统触发服务`使用`CSHelper`应该没有这个问题

Expand Down
18 changes: 8 additions & 10 deletions README_en.md
Original file line number Diff line number Diff line change
@@ -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

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

Expand Down
12 changes: 10 additions & 2 deletions app/src/main/java/com/parallelc/micts/ModuleMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/com/parallelc/micts/config/XposedConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TriggerService> {
Expand Down
86 changes: 86 additions & 0 deletions app/src/main/java/com/parallelc/micts/hooker/CSMSHooker.kt
Original file line number Diff line number Diff line change
@@ -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<MethodUnhooker<Method>>()
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")
}
}
}
}
}
19 changes: 13 additions & 6 deletions app/src/main/java/com/parallelc/micts/hooker/VIMSHooker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,13 +40,20 @@ class VIMSHooker {
@JvmStatic
@BeforeInvocation
fun before(callback: BeforeHookCallback) : MethodUnhooker<Method>? {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down

0 comments on commit 2e74254

Please sign in to comment.