Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

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

Expand All @@ -69,6 +71,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
securityHandler = SecurityHandler(this)
ambientGlanceHandler = AmbientGlanceHandler(this)
aodForceTurnOffHandler = AodForceTurnOffHandler(this)
omniGestureOverlayHandler = OmniGestureOverlayHandler(this)

flashlightHandler.register()

Expand All @@ -82,13 +85,15 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
aodForceTurnOffHandler.removeOverlay()
freezeHandler.removeCallbacks(freezeRunnable)
stopInputEventListener()
updateOmniOverlay()
}

Intent.ACTION_SCREEN_OFF -> {
appFlowHandler.clearAuthenticated()
scheduleFreeze()
startInputEventListenerIfEnabled()
ambientGlanceHandler.checkAndShowOnScreenOff()
omniGestureOverlayHandler.updateOverlay(false) // Always hide when screen is off
}

Intent.ACTION_USER_PRESENT -> {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}
}
}
Loading