Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
3. 设置触发方式
- 打开MiCTS即可触发,因此可以利用其他软件,比如悬浮球、Xposed Edge、ShortX等,将动作设置为打开MiCTS,实现自定义触发方式
- MiCTS提供了一个触发磁贴,可将其添加到快速设置面板里,通过点击磁贴触发
- 对于免root设备,可以使用Shizuku实现长按Home键触发圈定即搜,在MiCTS应用设置中开启即可
- 对于小米设备,MiCTS内置了长按小白条触发和长按Home键触发的功能,可以在MiCTS设置里开启(安装MiCTS后需要激活模块并重启手机才能使用)
- 对于Android版本>=13的三星设备,可以从[三星应用商店](https://galaxystore.samsung.com/detail/com.samsung.android.app.routineplus)或[Good Lock](https://galaxystore.samsung.com/detail/com.samsung.android.goodlock)里下载安装“日常程序+”,然后在“设置-模式和日常程序”里,创建日常程序通过按钮操作实现长按电源按钮等方式来启动MiCTS

Expand All @@ -36,6 +37,10 @@
### 应用设置
- 默认触发延迟:通过打开MiCTS触发的延迟
- 磁贴触发延迟:通过点击快速设置面板的磁贴触发的延迟
- 长按Home键触发(Shizuku):免root通过Shizuku授权`WRITE_SECURE_SETTINGS`并设置助理应用,实现长按Home键触发圈定即搜
- 需要Shizuku正在运行,且已将Google设置为默认助理应用
- 小米与HyperOS设备需要在开发者选项中开启“USB调试(安全设置)”,否则无法修改设置
- 手机重启后Android会重置助理应用设置。如果重启后长按Home键失效,打开MiCTS重新开启该开关即可

### 模块设置
需要在LSPosed里激活模块
Expand Down
5 changes: 5 additions & 0 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Trigger Circle to Search on any Android 9–16 device
3. Set up the trigger method
- Launching MiCTS will trigger, so you can use other apps like Quick Ball, Xposed Edge, ShortX, etc., set launching MiCTS as the action to customize the trigger method
- MiCTS provides a trigger tile, so you can add it to the Quick Settings panel and trigger by clicking it
- You can trigger Circle to Search by long-pressing the Home button without root via Shizuku. Turn on "Trigger by long press home button (Shizuku)" in MiCTS App Settings
- For Xiaomi devices, MiCTS has built-in support for `Trigger by long press gesture handle` and `Trigger by long press home button`, which can be enabled in the MiCTS settings (need to activate the module and restart the phone after installing MiCTS)
- For Samsung devices running Android 13 and above, you can download and install "Routines+" from the [Galaxy Store](https://galaxystore.samsung.com/detail/com.samsung.android.app.routineplus) or [Good Lock](https://galaxystore.samsung.com/detail/com.samsung.android.goodlock). Then, go to Settings > Modes and Routines to create routines that launch MiCTS by Button action such as long-pressing the power button.

Expand All @@ -37,6 +38,10 @@ Trigger Circle to Search on any Android 9–16 device
### App Settings
- Default trigger delay: The delay when triggering by launching MiCTS
- Tile trigger delay: The delay when triggering by the Quick Settings panel tile
- Trigger by long press home button (Shizuku): Triggers Circle to Search on long-press Home without root or Xposed by using Shizuku to grant `WRITE_SECURE_SETTINGS` and configure the assistant app
- Requires Shizuku running and Google set as the default digital assistant
- On Xiaomi and HyperOS devices, enable "USB debugging (Security settings)" in Developer Options so Shizuku can grant permissions and modify secure settings
- Android resets the assistant selection when the phone restarts. If long-pressing Home stops opening Circle to Search after a reboot, open MiCTS and toggle the switch back on

### Module Settings
Need to activate the module in LSPosed
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,6 @@ dependencies {
compileOnly(libs.libxposed.api)
implementation(libs.libxposed.service)
implementation(libs.hiddenapibypass)
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
}
24 changes: 23 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MainApplication"
android:description="@string/xposed_description"
Expand Down Expand Up @@ -39,6 +42,17 @@
android:resource="@xml/shortcuts" />
</activity>

<receiver
android:name=".receiver.BootReceiver"
android:directBootAware="true"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="999">
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

<service
android:name=".LaunchTileService"
android:exported="true"
Expand All @@ -49,5 +63,13 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>

<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
</application>
</manifest>
2 changes: 2 additions & 0 deletions app/src/main/java/com/parallelc/micts/config/AppConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ object AppConfig {
const val KEY_TILE_DELAY = "tile_delay"
const val KEY_VIBRATE = "vibrate"
const val KEY_ASYNC_TRIGGER = "async_trigger"
const val KEY_HOME_TRIGGER = "home_trigger"

val DEFAULT_CONFIG = mapOf<String, Any>(
KEY_LANGUAGE to Language.FollowSystem.ordinal,
KEY_DEFAULT_DELAY to 0L,
KEY_TILE_DELAY to 400L,
KEY_VIBRATE to false,
KEY_ASYNC_TRIGGER to false,
KEY_HOME_TRIGGER to false,
)
}
56 changes: 56 additions & 0 deletions app/src/main/java/com/parallelc/micts/receiver/BootReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.parallelc.micts.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.UserManager
import android.util.Log
import com.parallelc.micts.config.AppConfig
import com.parallelc.micts.util.ShizukuHelper

class BootReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "MiCTS_BootReceiver"
}

override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action != Intent.ACTION_BOOT_COMPLETED && action != Intent.ACTION_LOCKED_BOOT_COMPLETED) {
return
}

val defaultVal = (AppConfig.DEFAULT_CONFIG[AppConfig.KEY_HOME_TRIGGER] as? Boolean) ?: false
var homeTriggerEnabled = false

runCatching {
val deContext = context.createDeviceProtectedStorageContext()
val dePrefs = deContext.getSharedPreferences(AppConfig.CONFIG_NAME, Context.MODE_PRIVATE)

val userManager = context.getSystemService(UserManager::class.java)
if (!dePrefs.contains(AppConfig.KEY_HOME_TRIGGER) && userManager?.isUserUnlocked == true) {
val cePrefs = context.getSharedPreferences(AppConfig.CONFIG_NAME, Context.MODE_PRIVATE)
val migratedVal = cePrefs.getBoolean(AppConfig.KEY_HOME_TRIGGER, defaultVal)
dePrefs.edit().putBoolean(AppConfig.KEY_HOME_TRIGGER, migratedVal).commit()
}

homeTriggerEnabled = dePrefs.getBoolean(AppConfig.KEY_HOME_TRIGGER, defaultVal)
}.onFailure { e ->
Log.e(TAG, "Failed reading home trigger preference on $action", e)
}

if (!homeTriggerEnabled) {
return
}

if (ShizukuHelper.hasWriteSecureSettings(context)) {
val result = ShizukuHelper.applyAssistantSetting(context, enable = true)
if (result.isSuccess) {
Log.i(TAG, "Successfully restored assistant setting on $action")
} else {
Log.e(TAG, "Failed to restore assistant setting on $action", result.exceptionOrNull())
}
} else {
Log.w(TAG, "Cannot restore assistant setting on $action: WRITE_SECURE_SETTINGS not granted")
}
}
}
13 changes: 11 additions & 2 deletions app/src/main/java/com/parallelc/micts/ui/activity/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.parallelc.micts.ui.activity

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.os.Build
import android.os.Bundle
Expand All @@ -23,6 +24,7 @@ import com.parallelc.micts.config.AppConfig.KEY_DEFAULT_DELAY
import com.parallelc.micts.config.AppConfig.KEY_TILE_DELAY
import com.parallelc.micts.config.AppConfig.KEY_VIBRATE
import com.parallelc.micts.module
import com.parallelc.micts.util.ShizukuHelper
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -77,7 +79,13 @@ class MainActivity : ComponentActivity() {
if (delayMs > 0) {
delay(delayMs)
}
if (!triggerCircleToSearch(1, this, vibrate)) {
var success = triggerCircleToSearch(1, this, vibrate)
if (!success && ShizukuHelper.hasWriteSecureSettings(this)) {
ShizukuHelper.restoreVoiceInteractionServiceIfNeeded(this)
delay(250)
success = triggerCircleToSearch(1, this, vibrate)
}
if (!success) {
Toast.makeText(this, getString(R.string.trigger_failed), Toast.LENGTH_SHORT).show()
}
finish()
Expand All @@ -90,8 +98,9 @@ class MainActivity : ComponentActivity() {
window.isNavigationBarContrastEnforced = false
}
val prefs = getSharedPreferences(CONFIG_NAME, MODE_PRIVATE)
val isAssistant = intent.action == Intent.ACTION_ASSIST
val key = if (intent.getBooleanExtra("from_tile", false)) KEY_TILE_DELAY else KEY_DEFAULT_DELAY
val delayMs = prefs.getLong(key, DEFAULT_CONFIG[key] as Long)
val delayMs = if (isAssistant) 0L else prefs.getLong(key, DEFAULT_CONFIG[key] as Long)
val vibrate = prefs.getBoolean(KEY_VIBRATE, DEFAULT_CONFIG[KEY_VIBRATE] as Boolean)

if (prefs.getBoolean(KEY_ASYNC_TRIGGER, DEFAULT_CONFIG[KEY_ASYNC_TRIGGER] as Boolean)) {
Expand Down
139 changes: 139 additions & 0 deletions app/src/main/java/com/parallelc/micts/ui/activity/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
Expand Down Expand Up @@ -68,6 +69,7 @@ import com.parallelc.micts.config.TriggerService
import com.parallelc.micts.config.XposedConfig
import com.parallelc.micts.ui.theme.MiCTSTheme
import com.parallelc.micts.ui.viewmodel.SettingsViewModel
import com.parallelc.micts.util.ShizukuHelper
import kotlin.system.exitProcess

class SettingsActivity : ComponentActivity() {
Expand All @@ -87,12 +89,23 @@ class SettingsActivity : ComponentActivity() {
}
}
}

override fun onResume() {
super.onResume()
if (::viewModel.isInitialized) {
viewModel.refreshHomeTriggerStatus()
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
var showAboutDialog by remember { viewModel.showAboutDialog }
var showHomeTriggerRestartDialog by remember { viewModel.showHomeTriggerRestartDialog }
var showXiaomiSecurityDialog by remember { viewModel.showXiaomiSecurityDialog }
var showShizukuNotRunningDialog by remember { viewModel.showShizukuNotRunningDialog }
var showGoogleNotDefaultDialog by remember { viewModel.showGoogleNotDefaultDialog }

var topAppBarState = remember { viewModel.topAppBarState }
val appIcon = LocalContext.current.packageManager.getApplicationIcon(LocalContext.current.applicationInfo.packageName)
Expand Down Expand Up @@ -198,6 +211,105 @@ fun SettingsScreen(viewModel: SettingsViewModel) {
modifier = Modifier.fillMaxWidth(0.9f)
)
}

if (showHomeTriggerRestartDialog) {
AlertDialog(
onDismissRequest = { showHomeTriggerRestartDialog = false },
title = {
Text(stringResource(R.string.home_trigger_dialog_title))
},
text = {
Text(stringResource(R.string.home_trigger_dialog_message))
},
confirmButton = {
TextButton(onClick = { showHomeTriggerRestartDialog = false }) {
Text(stringResource(R.string.home_trigger_dialog_confirm))
}
},
modifier = Modifier.fillMaxWidth(0.9f)
)
}

if (showXiaomiSecurityDialog) {
AlertDialog(
onDismissRequest = { showXiaomiSecurityDialog = false },
title = {
Text(stringResource(R.string.xiaomi_security_dialog_title))
},
text = {
Text(stringResource(R.string.xiaomi_security_setting_required))
},
confirmButton = {
TextButton(onClick = { showXiaomiSecurityDialog = false }) {
Text(stringResource(android.R.string.ok))
}
},
modifier = Modifier.fillMaxWidth(0.9f)
)
}

if (showShizukuNotRunningDialog) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = { showShizukuNotRunningDialog = false },
title = {
Text(stringResource(R.string.shizuku_dialog_title))
},
text = {
Text(stringResource(R.string.shizuku_not_running))
},
confirmButton = {
TextButton(onClick = {
showShizukuNotRunningDialog = false
val launchIntent = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
if (launchIntent != null) {
context.startActivity(launchIntent)
}
}) {
Text(stringResource(R.string.open_shizuku))
}
},
dismissButton = {
TextButton(onClick = { showShizukuNotRunningDialog = false }) {
Text(stringResource(android.R.string.cancel))
}
},
modifier = Modifier.fillMaxWidth(0.9f)
)
}

if (showGoogleNotDefaultDialog) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = { showGoogleNotDefaultDialog = false },
title = {
Text(stringResource(R.string.google_assistant_needed_title))
},
text = {
Text(stringResource(R.string.google_not_default_assistant))
},
confirmButton = {
TextButton(onClick = {
showGoogleNotDefaultDialog = false
runCatching {
context.startActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS))
}.onFailure {
runCatching {
context.startActivity(Intent(Settings.ACTION_VOICE_INPUT_SETTINGS))
}
}
}) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { showGoogleNotDefaultDialog = false }) {
Text(stringResource(android.R.string.cancel))
}
},
modifier = Modifier.fillMaxWidth(0.9f)
)
}
}

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -262,6 +374,33 @@ fun SettingsPage(
}
)

val homeTriggerStatus by viewModel.homeTriggerStatus.collectAsState()

ListItem(
headlineContent = { Text(stringResource(R.string.home_trigger_shizuku)) },
supportingContent = {
val (textRes, color) = when (homeTriggerStatus) {
is ShizukuHelper.Status.Active -> R.string.home_trigger_active to MaterialTheme.colorScheme.primary
is ShizukuHelper.Status.ResetAfterBoot -> R.string.home_trigger_reset to MaterialTheme.colorScheme.error
is ShizukuHelper.Status.ShizukuNotRunning -> R.string.shizuku_not_running to MaterialTheme.colorScheme.error
is ShizukuHelper.Status.PermissionNeeded -> R.string.shizuku_permission_denied to MaterialTheme.colorScheme.error
is ShizukuHelper.Status.Inactive -> R.string.home_trigger_inactive to MaterialTheme.colorScheme.onSurfaceVariant
}
Text(
text = stringResource(textRes),
color = color
)
},
trailingContent = {
Switch(
checked = homeTriggerStatus is ShizukuHelper.Status.Active,
onCheckedChange = { checked ->
viewModel.toggleHomeTrigger(checked)
}
)
}
)

ListItem(
headlineContent = {
Text(
Expand Down
Loading
Loading