From 0101e88729c9708051295170d8ed285208f8baad Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 18 Apr 2026 02:41:05 +0530 Subject: [PATCH 1/2] feat: add Circle to Search action with Shizuku --- .../services/handlers/ButtonRemapHandler.kt | 15 +++- .../configs/ButtonRemapSettingsUI.kt | 6 ++ .../essentials/utils/OmniTriggerUtil.kt | 75 +++++++++++++++++++ .../main/res/drawable/frame_inspect_24px.xml | 10 +++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt create mode 100644 app/src/main/res/drawable/frame_inspect_24px.xml diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt index d16b65ef5..b483194b9 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt @@ -197,11 +197,18 @@ class ButtonRemapHandler( "Take screenshot" -> takeScreenshot() "Cycle sound modes" -> cycleSoundModes() "Toggle media volume" -> toggleMediaVolume() - "Like current song" -> service.sendBroadcast( - Intent("com.sameerasw.essentials.ACTION_LIKE_CURRENT_SONG").setPackage( - service.packageName + "Like current song" -> { + service.sendBroadcast( + Intent("com.sameerasw.essentials.ACTION_LIKE_CURRENT_SONG").setPackage( + service.packageName + ) ) - ) + triggerHapticFeedback() + } + "Circle to Search" -> { + com.sameerasw.essentials.utils.OmniTriggerUtil.trigger(service) + triggerHapticFeedback() + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt index 99256f5a2..b811fd1a1 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt @@ -421,6 +421,12 @@ fun ButtonRemapSettingsUI( hasSettings = true, onSettingsClick = { showLikeSongOptions.value = true } ) + RemapActionItem( + title = stringResource(R.string.action_circle_to_search), + isSelected = currentAction == "Circle to Search", + onClick = { onActionSelected("Circle to Search") }, + iconRes = R.drawable.frame_inspect_24px, + ) if (selectedScreenTab == 1) { RemapActionItem( title = stringResource(R.string.action_take_screenshot), diff --git a/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt new file mode 100644 index 000000000..4430affb1 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt @@ -0,0 +1,75 @@ +package com.sameerasw.essentials.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.SystemClock +import android.util.Log +import com.sameerasw.essentials.shizuku.ShizukuPermissionHelper +import org.lsposed.hiddenapibypass.HiddenApiBypass + +object OmniTriggerUtil { + private const val TAG = "OmniTriggerUtil" + + @SuppressLint("PrivateApi") + fun trigger(context: Context): Boolean { + val bundle = Bundle().apply { + putLong("invocation_time_ms", SystemClock.elapsedRealtime()) + putInt("omni.entry_point", 1) // Entry point for home long press + putBoolean("micts_trigger", true) + } + + // 1. Try Shizuku approach first if available and permitted + val shizukuHelper = ShizukuPermissionHelper(context) + if (shizukuHelper.isReady() && shizukuHelper.hasPermission()) { + val result = runCatching { + val vis = ShizukuUtils.getSystemBinder("voiceinteraction") + if (vis != null) { + val iVimsClass = Class.forName("com.android.internal.app.IVoiceInteractionManagerService") + val vims = Class.forName("com.android.internal.app.IVoiceInteractionManagerService\$Stub") + .getMethod("asInterface", IBinder::class.java) + .invoke(null, vis) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7, "hyperOS_home") as Boolean + } else { + HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7) as Boolean + } + } else { + false + } + }.getOrDefault(false) + + if (result) { + Log.d(TAG, "Triggered via Shizuku successfully") + return true + } + } + + // 2. Fallback to Non-Root Reflection + return runCatching { + val vis = Class.forName("android.os.ServiceManager") + .getMethod("getService", String::class.java) + .invoke(null, "voiceinteraction") as IBinder? + + if (vis != null) { + val iVimsClass = Class.forName("com.android.internal.app.IVoiceInteractionManagerService") + val vims = Class.forName("com.android.internal.app.IVoiceInteractionManagerService\$Stub") + .getMethod("asInterface", IBinder::class.java) + .invoke(null, vis) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7, "hyperOS_home") as Boolean + } else { + HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7) as Boolean + } + } else { + false + } + }.onFailure { e -> + Log.e(TAG, "Trigger failed", e) + }.getOrDefault(false) + } +} diff --git a/app/src/main/res/drawable/frame_inspect_24px.xml b/app/src/main/res/drawable/frame_inspect_24px.xml new file mode 100644 index 000000000..eddca4fc2 --- /dev/null +++ b/app/src/main/res/drawable/frame_inspect_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e485cf29c..87eef2bc5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ AI assistant Take screenshot Cycle sound modes + Circle to Search Like current song Like song settings This feature requires notification access to detect the currently playing media and trigger the like action. Please enable it below. From dd11cf58501719be43d882675edeecf9cb199245 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 18 Apr 2026 03:27:13 +0530 Subject: [PATCH 2/2] feat: add circle to search support withotu gesture bar --- .../data/repository/SettingsRepository.kt | 1 + .../handlers/OmniGestureOverlayHandler.kt | 155 ++++++++++++++++++ .../tiles/ScreenOffAccessibilityService.kt | 38 ++++- .../configs/OtherCustomizationsSettingsUI.kt | 12 ++ .../essentials/utils/OmniTriggerUtil.kt | 80 ++++----- .../essentials/viewmodels/MainViewModel.kt | 11 ++ app/src/main/res/values/strings.xml | 4 +- 7 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 76360dfa0..c80247166 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -113,6 +113,7 @@ class SettingsRepository(private val context: Context) { const val KEY_HIDE_SYSTEM_ICONS = "hide_system_icons" const val KEY_HIDE_SYSTEM_ICONS_LOCKED_ONLY = "hide_system_icons_locked_only" const val KEY_HIDE_GESTURE_BAR_ENABLED = "hide_gesture_bar_enabled" + const val KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED = "circle_to_search_gesture_enabled" const val KEY_AUTO_UPDATE_ENABLED = "auto_update_enabled" const val KEY_UPDATE_NOTIFICATION_ENABLED = "update_notification_enabled" const val KEY_LAST_UPDATE_CHECK_TIME = "last_update_check_time" diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt new file mode 100644 index 000000000..888398ac9 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/OmniGestureOverlayHandler.kt @@ -0,0 +1,155 @@ +package com.sameerasw.essentials.services.handlers + +import android.accessibilityservice.AccessibilityService +import android.content.Context +import android.graphics.Color +import android.graphics.PixelFormat +import android.os.* +import android.view.* +import com.sameerasw.essentials.utils.OmniTriggerUtil + +class OmniGestureOverlayHandler(private val service: AccessibilityService) { + private val windowManager = service.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private val vibrator = getVibratorInstance() + + private var overlayView: View? = null + private val handler = Handler(Looper.getMainLooper()) + private val touchSlop = ViewConfiguration.get(service).scaledTouchSlop + + private var startX = 0f + private var startY = 0f + private var isLongPressActive = false + + private val longPressRunnable = Runnable { + isLongPressActive = false + OmniTriggerUtil.trigger(service) + triggerFinalTick() + } + + private val fallbackEffect: VibrationEffect? by lazy { + val segments = 25 + val timings = LongArray(segments) { 20L } + val amplitudes = IntArray(segments) { i -> + val progress = (i + 1).toFloat() / segments + val curve = progress * progress + (3 + (57 * curve)).toInt().coerceAtMost(60) + } + runCatching { VibrationEffect.createWaveform(timings, amplitudes, -1) }.getOrNull() + } + + fun updateOverlay(enabled: Boolean) { + handler.post { + if (enabled) showOverlay() else removeOverlay() + } + } + + private fun showOverlay() { + if (overlayView != null) return + + overlayView = View(service).apply { + setBackgroundColor(Color.TRANSPARENT) + setOnTouchListener { _, event -> + handleTouch(event) + true + } + } + + val params = WindowManager.LayoutParams( + dpToPx(WIDTH_DP), + dpToPx(HEIGHT_DP), + WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + + runCatching { windowManager.addView(overlayView, params) } + } + + private fun handleTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y + isLongPressActive = true + handler.postDelayed(longPressRunnable, LONG_PRESS_TIMEOUT) + startRampingHaptic() + } + MotionEvent.ACTION_MOVE -> { + if (Math.abs(event.x - startX) > touchSlop || Math.abs(event.y - startY) > touchSlop) { + cancelLongPress() + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> cancelLongPress() + } + } + + private fun cancelLongPress() { + if (!isLongPressActive) return + isLongPressActive = false + handler.removeCallbacks(longPressRunnable) + vibrator?.cancel() + } + + fun removeOverlay() { + cancelLongPress() + overlayView?.let { + runCatching { windowManager.removeView(it) } + overlayView = null + } + } + + private fun startRampingHaptic() { + val v = vibrator ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + runCatching { + val effect = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.6f) + .compose() + v.vibrate(effect) + }.onFailure { fallbackRampingWaveform(v) } + } else { + fallbackRampingWaveform(v) + } + } + + private fun fallbackRampingWaveform(v: Vibrator) { + fallbackEffect?.let { + runCatching { v.vibrate(it) } + } ?: v.vibrate(VibrationEffect.createOneShot(LONG_PRESS_TIMEOUT, VibrationEffect.DEFAULT_AMPLITUDE)) + } + + private fun triggerFinalTick() { + val v = vibrator ?: return + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + v.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + } else { + v.vibrate(VibrationEffect.createOneShot(30, 180)) + } + } + } + + private fun getVibratorInstance(): Vibrator? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + service.getSystemService(VibratorManager::class.java)?.defaultVibrator + } else { + @Suppress("DEPRECATION") + service.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator + } + } + + private fun dpToPx(dp: Float) = (dp * service.resources.displayMetrics.density).toInt() + + companion object { + private const val LONG_PRESS_TIMEOUT = 500L + private const val WIDTH_DP = 240f + private const val HEIGHT_DP = 48f + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt index 5fd479c5a..c8f620ea6 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt @@ -18,13 +18,7 @@ import android.view.accessibility.AccessibilityEvent import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.domain.HapticFeedbackType import com.sameerasw.essentials.services.InputEventListenerService -import com.sameerasw.essentials.services.handlers.AmbientGlanceHandler -import com.sameerasw.essentials.services.handlers.AodForceTurnOffHandler -import com.sameerasw.essentials.services.handlers.AppFlowHandler -import com.sameerasw.essentials.services.handlers.ButtonRemapHandler -import com.sameerasw.essentials.services.handlers.FlashlightHandler -import com.sameerasw.essentials.services.handlers.NotificationLightingHandler -import com.sameerasw.essentials.services.handlers.SecurityHandler +import com.sameerasw.essentials.services.handlers.* import com.sameerasw.essentials.services.receivers.FlashlightActionReceiver import com.sameerasw.essentials.utils.FreezeManager import com.sameerasw.essentials.utils.performHapticFeedback @@ -45,6 +39,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private lateinit var securityHandler: SecurityHandler private lateinit var ambientGlanceHandler: AmbientGlanceHandler private lateinit var aodForceTurnOffHandler: AodForceTurnOffHandler + private lateinit var omniGestureOverlayHandler: OmniGestureOverlayHandler private var screenReceiver: BroadcastReceiver? = null @@ -58,6 +53,13 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene FreezeManager.freezeAll(this) } + private val preferenceChangeListener = + android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == "circle_to_search_gesture_enabled" || key == "hide_gesture_bar_enabled") { + updateOmniOverlay() + } + } + override fun onCreate() { super.onCreate() @@ -69,6 +71,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene securityHandler = SecurityHandler(this) ambientGlanceHandler = AmbientGlanceHandler(this) aodForceTurnOffHandler = AodForceTurnOffHandler(this) + omniGestureOverlayHandler = OmniGestureOverlayHandler(this) flashlightHandler.register() @@ -82,6 +85,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene aodForceTurnOffHandler.removeOverlay() freezeHandler.removeCallbacks(freezeRunnable) stopInputEventListener() + updateOmniOverlay() } Intent.ACTION_SCREEN_OFF -> { @@ -89,6 +93,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene scheduleFreeze() startInputEventListenerIfEnabled() ambientGlanceHandler.checkAndShowOnScreenOff() + omniGestureOverlayHandler.updateOverlay(false) // Always hide when screen is off } Intent.ACTION_USER_PRESENT -> { @@ -124,6 +129,9 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene proximitySensor?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL) } + + getSharedPreferences("essentials_prefs", MODE_PRIVATE) + .registerOnSharedPreferenceChangeListener(preferenceChangeListener) } private fun scheduleFreeze() { @@ -152,6 +160,14 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene serviceInfo = serviceInfo.apply { flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS } + updateOmniOverlay() + } + + private fun updateOmniOverlay() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val isHideBarEnabled = prefs.getBoolean("hide_gesture_bar_enabled", false) + val isGestureEnabled = prefs.getBoolean("circle_to_search_gesture_enabled", false) + omniGestureOverlayHandler.updateOverlay(isHideBarEnabled && isGestureEnabled) } override fun onDestroy() { @@ -165,8 +181,11 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene notificationLightingHandler.removeOverlay() ambientGlanceHandler.removeOverlay() aodForceTurnOffHandler.removeOverlay() + omniGestureOverlayHandler.removeOverlay() stopInputEventListener() serviceScope.cancel() + getSharedPreferences("essentials_prefs", MODE_PRIVATE) + .unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) super.onDestroy() } @@ -200,6 +219,11 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { + super.onConfigurationChanged(newConfig) + updateOmniOverlay() // Force refresh overlay on rotation + } + override fun onKeyEvent(event: KeyEvent): Boolean { val keyCode = event.keyCode val isVolumeKey = keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt index 9d6b276a2..fdea93656 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt @@ -105,6 +105,18 @@ fun OtherCustomizationsSettingsUI( iconRes = R.drawable.rounded_home_24, modifier = Modifier.highlight(highlightSetting == "hide_gesture_bar_toggle") ) + + IconToggleItem( + title = stringResource(R.string.feat_circle_to_search_gesture_title), + description = stringResource(R.string.feat_circle_to_search_gesture_desc), + isChecked = viewModel.isCircleToSearchGestureEnabled.value, + onCheckedChange = { enabled -> + viewModel.setCircleToSearchGestureEnabled(enabled, context) + }, + enabled = viewModel.isHideGestureBarEnabled.value, + iconRes = R.drawable.rounded_touch_app_24, + modifier = Modifier.highlight(highlightSetting == "circle_to_search_gesture_toggle") + ) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt index 4430affb1..17359f274 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/OmniTriggerUtil.kt @@ -7,69 +7,75 @@ import android.os.Bundle import android.os.IBinder import android.os.SystemClock import android.util.Log +import androidx.annotation.RequiresApi import com.sameerasw.essentials.shizuku.ShizukuPermissionHelper import org.lsposed.hiddenapibypass.HiddenApiBypass +import java.lang.reflect.Method object OmniTriggerUtil { private const val TAG = "OmniTriggerUtil" + private var iVimsClass: Class<*>? = null + private var vimsInterfaceMethod: Method? = null + private var serviceManagerClass: Class<*>? = null + private var getServiceMethod: Method? = null + @SuppressLint("PrivateApi") + private fun ensureReflection() { + if (iVimsClass != null) return + runCatching { + iVimsClass = Class.forName("com.android.internal.app.IVoiceInteractionManagerService") + vimsInterfaceMethod = Class.forName("com.android.internal.app.IVoiceInteractionManagerService\$Stub") + .getMethod("asInterface", IBinder::class.java) + serviceManagerClass = Class.forName("android.os.ServiceManager") + getServiceMethod = serviceManagerClass?.getMethod("getService", String::class.java) + } + } + fun trigger(context: Context): Boolean { + ensureReflection() + val bundle = Bundle().apply { putLong("invocation_time_ms", SystemClock.elapsedRealtime()) - putInt("omni.entry_point", 1) // Entry point for home long press + putInt("omni.entry_point", 1) putBoolean("micts_trigger", true) } - // 1. Try Shizuku approach first if available and permitted + // 1. Try Shizuku logic val shizukuHelper = ShizukuPermissionHelper(context) if (shizukuHelper.isReady() && shizukuHelper.hasPermission()) { val result = runCatching { val vis = ShizukuUtils.getSystemBinder("voiceinteraction") - if (vis != null) { - val iVimsClass = Class.forName("com.android.internal.app.IVoiceInteractionManagerService") - val vims = Class.forName("com.android.internal.app.IVoiceInteractionManagerService\$Stub") - .getMethod("asInterface", IBinder::class.java) - .invoke(null, vis) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7, "hyperOS_home") as Boolean - } else { - HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7) as Boolean - } - } else { - false - } + val vims = vimsInterfaceMethod?.invoke(null, vis) + val clazz = iVimsClass + if (vims != null && clazz != null) { + invokeShowSession(clazz, vims, bundle) + } else false }.getOrDefault(false) - if (result) { - Log.d(TAG, "Triggered via Shizuku successfully") - return true - } + if (result) return true } // 2. Fallback to Non-Root Reflection return runCatching { - val vis = Class.forName("android.os.ServiceManager") - .getMethod("getService", String::class.java) - .invoke(null, "voiceinteraction") as IBinder? - - if (vis != null) { - val iVimsClass = Class.forName("com.android.internal.app.IVoiceInteractionManagerService") - val vims = Class.forName("com.android.internal.app.IVoiceInteractionManagerService\$Stub") - .getMethod("asInterface", IBinder::class.java) - .invoke(null, vis) + val vis = getServiceMethod?.invoke(null, "voiceinteraction") as IBinder? + val vims = vimsInterfaceMethod?.invoke(null, vis) + val clazz = iVimsClass + if (vims != null && clazz != null) { + invokeShowSession(clazz, vims, bundle) + } else false + }.onFailure { e -> + Log.e(TAG, "Trigger failed", e) + }.getOrDefault(false) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7, "hyperOS_home") as Boolean - } else { - HiddenApiBypass.invoke(iVimsClass, vims, "showSessionFromSession", null, bundle, 7) as Boolean - } + private fun invokeShowSession(clazz: Class<*>, vims: Any, bundle: Bundle): Boolean { + return runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + HiddenApiBypass.invoke(clazz, vims, "showSessionFromSession", null, bundle, 7, "hyperOS_home") as Boolean? ?: false } else { - false + HiddenApiBypass.invoke(clazz, vims, "showSessionFromSession", null, bundle, 7) as Boolean? ?: false } - }.onFailure { e -> - Log.e(TAG, "Trigger failed", e) }.getOrDefault(false) } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 32f06d4bc..dae097b66 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -133,6 +133,7 @@ class MainViewModel : ViewModel() { val dnsPresets = mutableStateListOf() val addedQSTiles = mutableStateOf>(emptySet()) val isHideGestureBarEnabled = mutableStateOf(false) + val isCircleToSearchGestureEnabled = mutableStateOf(false) @@ -486,6 +487,10 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_FLASHLIGHT_PULSE_MAX_INTENSITY -> { flashlightPulseMaxIntensity.floatValue = settingsRepository.getFloat(key, 0.5f) } + + SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED -> { + isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(key) + } } } } @@ -520,6 +525,7 @@ class MainViewModel : ViewModel() { isShizukuPermissionGranted.value = ShizukuUtils.hasPermission() isAutoAccessibilityEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_AUTO_ACCESSIBILITY_ENABLED) isHideGestureBarEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_HIDE_GESTURE_BAR_ENABLED, false) + isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED, false) notificationLightingSystemMode.intValue = settingsRepository.getNotificationLightingSystemMode() if (isHideGestureBarEnabled.value) { applyHideGestureBar(context, true) @@ -1261,6 +1267,11 @@ class MainViewModel : ViewModel() { applyHideGestureBar(context, enabled) } + fun setCircleToSearchGestureEnabled(enabled: Boolean, context: Context) { + isCircleToSearchGestureEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED, enabled) + } + private fun applyHideGestureBar(context: Context, enabled: Boolean) { if (enabled) { com.sameerasw.essentials.utils.StatusBarManager.requestDisable( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87eef2bc5..7eb1837c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -351,9 +351,11 @@ Hide notifications Hide all incoming notification icons Hide gesture bar + Hide the navigation pill at the bottom + Circle to Search gesture + Long-press the bottom area to trigger Circle to Search Other customizations Additional system tweaks and modifications - In gesture navigation Please note that the implementation of these options may depend on the OEM and some may not be functional at all.