From 4e3e7c13f1f290ae99d1e71b2a6fc8e4363fac9e Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Thu, 1 Oct 2026 18:32:37 +0200 Subject: [PATCH 1/6] feat(shizuku): add Shizuku dependencies and manifest declarations --- app/build.gradle.kts | 2 ++ app/src/main/AndroidManifest.xml | 12 +++++++++++- gradle/libs.versions.toml | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1739f1e..da78041 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -104,4 +104,6 @@ dependencies { compileOnly(libs.libxposed.api) implementation(libs.libxposed.service) implementation(libs.hiddenapibypass) + implementation(libs.shizuku.api) + implementation(libs.shizuku.provider) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 44de1fa..1d7ec7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 032d313..5c715b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,11 @@ compose-bom = "2026.03.01" animation-core-android = "1.10.6" animation-android = "1.10.6" +shizuku = "13.1.5" + [libraries] +shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } +shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed-api" } libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed-service" } From 3dfe49d85de9c244ebe3972cf4d9836d150632ac Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Thu, 1 Oct 2026 18:38:51 +0200 Subject: [PATCH 2/6] feat(shizuku): implement ShizukuHelper and string resources --- .../com/parallelc/micts/util/ShizukuHelper.kt | 200 ++++++++++++++++++ app/src/main/res/values/strings.xml | 17 +- 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/parallelc/micts/util/ShizukuHelper.kt diff --git a/app/src/main/java/com/parallelc/micts/util/ShizukuHelper.kt b/app/src/main/java/com/parallelc/micts/util/ShizukuHelper.kt new file mode 100644 index 0000000..0a13a18 --- /dev/null +++ b/app/src/main/java/com/parallelc/micts/util/ShizukuHelper.kt @@ -0,0 +1,200 @@ +package com.parallelc.micts.util + +import android.Manifest +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.provider.Settings +import android.util.Log +import com.parallelc.micts.ui.activity.MainActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import rikka.shizuku.Shizuku + +object ShizukuHelper { + private const val TAG = "ShizukuHelper" + const val GOOGLE_PACKAGE = "com.google.android.googlequicksearchbox" + const val GOOGLE_ASSISTANT_SERVICE = + "$GOOGLE_PACKAGE/com.google.android.voiceinteraction.GsaVoiceInteractionService" + + sealed class Status { + data object Active : Status() + data object ResetAfterBoot : Status() + data object Inactive : Status() + data object ShizukuNotRunning : Status() + data object PermissionNeeded : Status() + } + + fun getAssistantComponent(context: Context): String = + ComponentName(context, MainActivity::class.java).flattenToString() + + fun isShizukuRunning(): Boolean { + return runCatching { + Shizuku.pingBinder() + }.getOrDefault(false) + } + + fun hasShizukuPermission(): Boolean { + return if (!isShizukuRunning()) { + false + } else { + runCatching { + Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + }.getOrDefault(false) + } + } + + fun hasWriteSecureSettings(context: Context): Boolean { + return context.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED + } + + fun isHomeTriggerActive(context: Context): Boolean { + val currentAssistant = runCatching { + Settings.Secure.getString(context.contentResolver, "assistant") + }.getOrNull() ?: return false + val target = ComponentName(context, MainActivity::class.java) + return ComponentName.unflattenFromString(currentAssistant) == target + } + + fun isGoogleDefaultAssistant(context: Context): Boolean { + val resolver = context.contentResolver + val voiceService = runCatching { + Settings.Secure.getString(resolver, "voice_interaction_service") + }.getOrNull() + if (voiceService?.contains(GOOGLE_PACKAGE) == true) return true + + val assistant = runCatching { + Settings.Secure.getString(resolver, "assistant") + }.getOrNull() + if (assistant?.contains(GOOGLE_PACKAGE) == true) return true + + if (isHomeTriggerActive(context)) return true + + return false + } + + fun restoreVoiceInteractionServiceIfNeeded(context: Context) { + val currentVoiceService = runCatching { + Settings.Secure.getString(context.contentResolver, "voice_interaction_service") + }.getOrNull() + if (currentVoiceService.isNullOrEmpty() || !currentVoiceService.contains(GOOGLE_PACKAGE)) { + runCatching { + Settings.Secure.putString( + context.contentResolver, + "voice_interaction_service", + GOOGLE_ASSISTANT_SERVICE + ) + } + } + } + + fun getStatus(context: Context, isEnabledPref: Boolean): Status { + if (isHomeTriggerActive(context)) { + return Status.Active + } + if (isEnabledPref) { + return if (hasWriteSecureSettings(context)) { + Status.ResetAfterBoot + } else if (!isShizukuRunning()) { + Status.ShizukuNotRunning + } else if (!hasShizukuPermission()) { + Status.PermissionNeeded + } else { + Status.ResetAfterBoot + } + } + return Status.Inactive + } + + fun requestShizukuPermission(requestCode: Int = 1001, onResult: (Boolean) -> Unit) { + if (!isShizukuRunning()) { + onResult(false) + return + } + if (hasShizukuPermission()) { + onResult(true) + return + } + + val listener = object : Shizuku.OnRequestPermissionResultListener { + override fun onRequestPermissionResult(reqCode: Int, grantResult: Int) { + if (reqCode == requestCode) { + Shizuku.removeRequestPermissionResultListener(this) + onResult(grantResult == PackageManager.PERMISSION_GRANTED) + } + } + } + Shizuku.addRequestPermissionResultListener(listener) + try { + Shizuku.requestPermission(requestCode) + } catch (e: Exception) { + Shizuku.removeRequestPermissionResultListener(listener) + Log.e(TAG, "Failed to request Shizuku permission", e) + onResult(false) + } + } + + private val newProcessMethod by lazy { + Shizuku::class.java.getDeclaredMethod( + "newProcess", + Array::class.java, + Array::class.java, + String::class.java + ).apply { isAccessible = true } + } + + private fun execRemoteProcess(cmd: Array): Process { + return newProcessMethod.invoke(null, cmd, null, null) as Process + } + + suspend fun grantWriteSecureSettings(context: Context): Result = withContext(Dispatchers.IO) { + if (hasWriteSecureSettings(context)) { + return@withContext Result.success(Unit) + } + if (!isShizukuRunning() || !hasShizukuPermission()) { + return@withContext Result.failure(IllegalStateException("Shizuku is not available or permission denied")) + } + + runCatching { + val cmd = "pm grant ${context.packageName} android.permission.WRITE_SECURE_SETTINGS" + val process = execRemoteProcess(arrayOf("sh", "-c", cmd)) + val errorMsg: String + val exitCode: Int + try { + errorMsg = process.errorStream.bufferedReader().use { it.readText() } + exitCode = process.waitFor() + } finally { + process.destroy() + } + if (exitCode != 0) { + if (errorMsg.contains("SecurityException") || errorMsg.contains("Permission Denial")) { + throw SecurityException("XIAOMI_SECURITY_SETTING_REQUIRED: $errorMsg") + } + throw RuntimeException("Grant failed (code $exitCode): $errorMsg") + } + } + } + + fun applyAssistantSetting(context: Context, enable: Boolean): Result { + return runCatching { + val resolver = context.contentResolver + if (enable) { + if (!isGoogleDefaultAssistant(context)) { + throw IllegalStateException("GOOGLE_NOT_DEFAULT_ASSISTANT") + } + val component = getAssistantComponent(context) + val ok1 = Settings.Secure.putString(resolver, "assistant", component) + val ok2 = Settings.Secure.putString(resolver, "voice_interaction_service", GOOGLE_ASSISTANT_SERVICE) + if (!ok1 || !ok2 || !isHomeTriggerActive(context)) { + throw SecurityException("XIAOMI_SECURITY_SETTING_REQUIRED") + } + } else { + val ok1 = Settings.Secure.putString(resolver, "assistant", GOOGLE_ASSISTANT_SERVICE) + val ok2 = Settings.Secure.putString(resolver, "voice_interaction_service", GOOGLE_ASSISTANT_SERVICE) + if (!ok1 || !ok2) { + throw SecurityException("XIAOMI_SECURITY_SETTING_REQUIRED") + } + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c32a77..89a7890 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + MiCTS Circle to Search Trigger Circle to Search on any Android 9–16 device @@ -35,4 +35,19 @@ Español Türkçe Việt Nam + Trigger by long press home button (Shizuku) + Active: long press Home to search + Reset after reboot. Tap to re-enable. + Uses Shizuku to trigger without root + Note about phone restarts + Android resets the default assistant whenever the phone restarts.\n\nIf long pressing Home stops opening Circle to Search after a reboot, open MiCTS and flip this switch back on. + Got it + Shizuku is not running. Start it in the Shizuku app first. + Shizuku permission was not granted. + Open Shizuku + Google needs to be set as your default digital assistant first. Please select Google in Android settings. + On Xiaomi and HyperOS devices, turn on \"USB debugging (Security settings)\" in Developer Options so MiCTS can change the assistant. + Permission required on Xiaomi + Default assistant required + Shizuku From c2c0c63681d83976531a589b3eca7ee9f52e2474 Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Thu, 1 Oct 2026 18:40:41 +0200 Subject: [PATCH 3/6] feat(shizuku): add boot receiver and instant assistant trigger --- app/src/main/AndroidManifest.xml | 12 ++++ .../com/parallelc/micts/config/AppConfig.kt | 2 + .../parallelc/micts/receiver/BootReceiver.kt | 56 +++++++++++++++++++ .../micts/ui/activity/MainActivity.kt | 13 ++++- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/parallelc/micts/receiver/BootReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d7ec7a..65b9ed3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + + + + + + ( KEY_LANGUAGE to Language.FollowSystem.ordinal, @@ -32,5 +33,6 @@ object AppConfig { KEY_TILE_DELAY to 400L, KEY_VIBRATE to false, KEY_ASYNC_TRIGGER to false, + KEY_HOME_TRIGGER to false, ) } diff --git a/app/src/main/java/com/parallelc/micts/receiver/BootReceiver.kt b/app/src/main/java/com/parallelc/micts/receiver/BootReceiver.kt new file mode 100644 index 0000000..31f8ff9 --- /dev/null +++ b/app/src/main/java/com/parallelc/micts/receiver/BootReceiver.kt @@ -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") + } + } +} 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 e5e46d5..479eadf 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 @@ -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 @@ -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 @@ -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() @@ -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)) { From 40681942aad78cc170493cdfe64fa11eeb4febf0 Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Thu, 1 Oct 2026 18:46:43 +0200 Subject: [PATCH 4/6] feat(shizuku): manage home trigger state in SettingsViewModel --- .../micts/ui/viewmodel/SettingsViewModel.kt | 117 +++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/parallelc/micts/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/parallelc/micts/ui/viewmodel/SettingsViewModel.kt index bf1993c..c3c0435 100644 --- a/app/src/main/java/com/parallelc/micts/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/parallelc/micts/ui/viewmodel/SettingsViewModel.kt @@ -15,6 +15,8 @@ import com.parallelc.micts.config.AppConfig import com.parallelc.micts.config.Language import com.parallelc.micts.config.TriggerService import com.parallelc.micts.config.XposedConfig +import com.parallelc.micts.util.ShizukuHelper +import rikka.shizuku.Shizuku import io.github.libxposed.service.XposedService import io.github.libxposed.service.XposedService.OnScopeEventListener import io.github.libxposed.service.XposedService.ServiceException @@ -25,6 +27,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Locale @SuppressLint("PrivateApi") @@ -56,10 +59,29 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application var scrollState = ScrollState(0) var triggerServiceExpanded = mutableStateOf(false) + private val _homeTriggerStatus = MutableStateFlow(ShizukuHelper.Status.Inactive) + val homeTriggerStatus: StateFlow = _homeTriggerStatus + + var showHomeTriggerRestartDialog = mutableStateOf(false) + var showXiaomiSecurityDialog = mutableStateOf(false) + var showShizukuNotRunningDialog = mutableStateOf(false) + var showGoogleNotDefaultDialog = mutableStateOf(false) + + private val binderReceivedListener = Shizuku.OnBinderReceivedListener { refreshHomeTriggerStatus() } + private val binderDeadListener = Shizuku.OnBinderDeadListener { refreshHomeTriggerStatus() } + private var homeTriggerJob: Job? = null + init { appConfigPref = application.getSharedPreferences(AppConfig.CONFIG_NAME, MODE_PRIVATE) _appConfig.value = AppConfig.DEFAULT_CONFIG + appConfigPref.all.filterValues { it != null }.mapValues { it.value as Any } _locale.value = Language.entries[_appConfig.value[AppConfig.KEY_LANGUAGE] as Int].toLocale() + val isHomeTrigger = _appConfig.value[AppConfig.KEY_HOME_TRIGGER] as? Boolean ?: false + syncHomeTriggerToDeviceProtectedStorage(isHomeTrigger) + refreshHomeTriggerStatus() + runCatching { + Shizuku.addBinderReceivedListenerSticky(binderReceivedListener) + Shizuku.addBinderDeadListener(binderDeadListener) + } XposedServiceHelper.registerListener(object : XposedServiceHelper.OnServiceListener { override fun onServiceBind(service: XposedService) { viewModelScope.launch(Dispatchers.IO) { @@ -88,6 +110,14 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application }) } + private fun syncHomeTriggerToDeviceProtectedStorage(enabled: Boolean) { + runCatching { + getApplication().createDeviceProtectedStorageContext() + .getSharedPreferences(AppConfig.CONFIG_NAME, MODE_PRIVATE) + .edit().putBoolean(AppConfig.KEY_HOME_TRIGGER, enabled).apply() + } + } + fun updateAppConfig(key: String, value: Any) { if (key == AppConfig.KEY_LANGUAGE) { _locale.value = Language.entries[value as Int].toLocale() @@ -100,7 +130,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application when (value) { is Long -> appConfigPref.edit().putLong(key, value).apply() is Int -> appConfigPref.edit().putInt(key, value).apply() - is Boolean -> appConfigPref.edit().putBoolean(key, value).apply() + is Boolean -> { + appConfigPref.edit().putBoolean(key, value).apply() + if (key == AppConfig.KEY_HOME_TRIGGER) { + syncHomeTriggerToDeviceProtectedStorage(value) + } + } } } } @@ -165,4 +200,84 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application } } } + + fun refreshHomeTriggerStatus() { + val isEnabled = _appConfig.value[AppConfig.KEY_HOME_TRIGGER] as? Boolean ?: false + _homeTriggerStatus.value = ShizukuHelper.getStatus(getApplication(), isEnabled) + } + + fun toggleHomeTrigger(enable: Boolean) { + val context = getApplication() + homeTriggerJob?.cancel() + homeTriggerJob = viewModelScope.launch(Dispatchers.IO) { + if (enable) { + if (!ShizukuHelper.isGoogleDefaultAssistant(context)) { + withContext(Dispatchers.Main) { + showGoogleNotDefaultDialog.value = true + } + return@launch + } + + if (!ShizukuHelper.hasWriteSecureSettings(context)) { + if (!ShizukuHelper.isShizukuRunning()) { + withContext(Dispatchers.Main) { + showShizukuNotRunningDialog.value = true + } + return@launch + } + + if (!ShizukuHelper.hasShizukuPermission()) { + withContext(Dispatchers.Main) { + ShizukuHelper.requestShizukuPermission { granted -> + if (granted) { + toggleHomeTrigger(true) + } + } + } + return@launch + } + + val grantResult = ShizukuHelper.grantWriteSecureSettings(context) + if (grantResult.isFailure) { + val err = grantResult.exceptionOrNull()?.message.orEmpty() + withContext(Dispatchers.Main) { + if (err.contains("XIAOMI_SECURITY_SETTING_REQUIRED")) { + showXiaomiSecurityDialog.value = true + } + refreshHomeTriggerStatus() + } + return@launch + } + } + + val applyResult = ShizukuHelper.applyAssistantSetting(context, true) + withContext(Dispatchers.Main) { + if (applyResult.isSuccess) { + updateAppConfig(AppConfig.KEY_HOME_TRIGGER, true) + showHomeTriggerRestartDialog.value = true + } else { + val err = applyResult.exceptionOrNull()?.message.orEmpty() + if (err.contains("XIAOMI_SECURITY_SETTING_REQUIRED")) { + showXiaomiSecurityDialog.value = true + } + } + refreshHomeTriggerStatus() + } + } else { + ShizukuHelper.applyAssistantSetting(context, false) + withContext(Dispatchers.Main) { + updateAppConfig(AppConfig.KEY_HOME_TRIGGER, false) + refreshHomeTriggerStatus() + } + } + } + } + + override fun onCleared() { + super.onCleared() + runCatching { + Shizuku.removeBinderReceivedListener(binderReceivedListener) + Shizuku.removeBinderDeadListener(binderDeadListener) + } + } } \ No newline at end of file From a850b2804c3c9d43cbe157ee6e7aee071861d6fa Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Thu, 1 Oct 2026 19:14:57 +0200 Subject: [PATCH 5/6] feat(shizuku): add home trigger toggle and alert dialogs to SettingsActivity --- .../micts/ui/activity/SettingsActivity.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/app/src/main/java/com/parallelc/micts/ui/activity/SettingsActivity.kt b/app/src/main/java/com/parallelc/micts/ui/activity/SettingsActivity.kt index 1a943f8..3182031 100644 --- a/app/src/main/java/com/parallelc/micts/ui/activity/SettingsActivity.kt +++ b/app/src/main/java/com/parallelc/micts/ui/activity/SettingsActivity.kt @@ -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 @@ -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() { @@ -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) @@ -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) @@ -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( From 35dd039b30072c33a51e87cfa3c83f66071db5c9 Mon Sep 17 00:00:00 2001 From: T1NG3R Date: Fri, 9 Oct 2026 12:19:12 +0200 Subject: [PATCH 6/6] docs: document Shizuku Home button trigger and Xiaomi requirements --- README.md | 5 +++++ README_en.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index f3c051b..189efa5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -36,6 +37,10 @@ ### 应用设置 - 默认触发延迟:通过打开MiCTS触发的延迟 - 磁贴触发延迟:通过点击快速设置面板的磁贴触发的延迟 +- 长按Home键触发(Shizuku):免root通过Shizuku授权`WRITE_SECURE_SETTINGS`并设置助理应用,实现长按Home键触发圈定即搜 + - 需要Shizuku正在运行,且已将Google设置为默认助理应用 + - 小米与HyperOS设备需要在开发者选项中开启“USB调试(安全设置)”,否则无法修改设置 + - 手机重启后Android会重置助理应用设置。如果重启后长按Home键失效,打开MiCTS重新开启该开关即可 ### 模块设置 需要在LSPosed里激活模块 diff --git a/README_en.md b/README_en.md index c1713b5..bea42cd 100644 --- a/README_en.md +++ b/README_en.md @@ -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. @@ -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