Add configurable gesture activation zones - #4
Open
Dragonk wants to merge 11 commits into
Open
Conversation
Built test APKs are written to dist/ (gitignored, untracked). No APKs, signing keys, or build artifacts are committed.
The gesture touch zones and visual indicators were TYPE_APPLICATION_OVERLAY windows owned by a standalone foreground LifecycleService (EdgeOverlayService). Those application overlays are hidden by HIDE_NON_SYSTEM_OVERLAY_WINDOWS on secure system screens (Settings, SubSettings), so gestures stopped working there, and Android showed the persistent "displaying over other apps" notification because of SYSTEM_ALERT_WINDOW. Extract the window logic from EdgeOverlayService into a reusable EdgeOverlayController that is created, owned, and destroyed by the already- enabled EdgeGestureAccessibilityService. Every window the controller and the indicators add now uses WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, bound to the active AccessibilityService lifecycle: trusted windows that are not hidden on secure screens and do not require SYSTEM_ALERT_WINDOW. The controller is decoupled from the concrete service through a GestureDispatcher interface (trigger + replay), keeping it testable. The existing replay/interactivity workarounds (held-zone untouchable+alpha 0, indicator-hide-before-inject grace) are preserved verbatim; behaviour change is limited to the window type and ownership. Remove the now-obsolete foreground service and boot receiver: - delete EdgeOverlayService (foreground service, notification channel, canDrawOverlays watchdog, start/stop helpers) - delete BootReceiver (only revived EdgeOverlayService; the AccessibilityService is re-bound by Android on process death/reboot without it) - drop SYSTEM_ALERT_WINDOW, FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED from the manifest - drop the specialUse service metadata and the EdgeOverlayService declaration - drop the androidx.lifecycle.service / lifecycle-service catalog dependency (no LifecycleService remains) The accessibility service watchdog now only re-checks unrestricted battery and re-asserts the controller; the canDrawOverlays failure path is gone. Accessibility privacy is unchanged (canRetrieveWindowContent stays false; only the foreground package name is read for per-app pass-through). Lifecycle on service bind/unbind/rebind, configuration and display-geometry changes, and rotation are handled by the controller via the owner service.
With gesture windows owned by the AccessibilityService as TYPE_ACCESSIBILITY_OVERLAY, SYSTEM_ALERT_WINDOW / canDrawOverlays / ACTION_MANAGE_OVERLAY_PERMISSION are no longer needed. Remove that requirement from the setup card and the master-switch gating, the periodic overlay-permission re-check, and the gestures-off overlay toast path. Enabling gestures now depends only on the AccessibilityService being bound and unrestricted battery usage. - MainActivity: remove overlayGranted state, its ON_RESUME/1s poll, the ACTION_MANAGE_OVERLAY_PERMISSION intent, and the offReason overlay branch; canEnable no longer requires overlay. Replace the "restricted screens" remember bullet with an on-device/local-only one (the Settings limitation this change fixes is no longer true). - PermissionsCard: drop the overlay RequirementRow and the onRequestOverlay callback; the card now lists accessibility + battery only. - MainViewModel.setMasterEnabled: just write the datastore flag — the AccessibilityService observes the flow and attaches/detaches zones itself, so there is no foreground service to start/stop. - CompatibilityScreen: drop the "System screens" note claiming gestures won't work on Settings (no longer accurate). - strings: remove notification channel/running, permission_overlay, permission_grant, toast_gestures_off_overlay, compat_settings_*; reword the remember bullet to remember_on_device. - ExampleInstrumentedTest: accept the .debug applicationId suffix produced by the debug build type alongside com.ogesture.
- README "How it works": gesture overlays are accessibility-overlay windows owned by Ogesture's accessibility service, so they work on secure system screens and do not require "Display over other apps". Permissions list reduced from three to two (accessibility + unrestricted battery). Remove the known limitation that gestures cannot work on Settings pages — this change addresses it. - PRIVACY: permission table no longer lists SYSTEM_ALERT_WINDOW, FOREGROUND_SERVICE/FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS, or RECEIVE_BOOT_COMPLETED; accessibility now owns the edge overlays and performs the navigation actions.
Pure-data regression guards for the zone set (bottom/left/right), the swipe-direction mapping per zone, the Back/Home/Recents action wiring (incl. the bottom zone's swipe-and-hold Recents long action), and that every zone has positive thickness/length. The accessibility-overlay migration must preserve all of this; a silent change to the layout or action wiring now fails here.
Four user-tunable geometry settings, persisted via DataStore Preferences and clamped to valid ranges so a corrupted preference can never produce a zero-sized or absurd overlay: - back_activation_height_percent (10..100%, step 10, default 80) - bottom_activation_width_percent (10..100%, step 10, default 80) - back_edge_sensitivity (0.5x..4.0x, step 0.25, default 1.0x) - bottom_edge_sensitivity (0.5x..4.0x, step 0.25, default 1.0x) Sensitivity multiplies the BASE gesture-zone THICKNESS only (the starting hit-region depth), never the swipe-distance threshold. Central constants live on GestureZoneSettings so the UI, repository and controller share one source of truth; clampPercent/clampSensitivity normalise raw persisted values. Models also gains buildGestureZones(settings) -> List<ZoneConfig> (geometry from settings, action/direction mapping static) and homeHandleWidthDp(percent) for the Home handle width mapping. GESTURE_ZONES is kept as buildGestureZones(DEFAULT) so the default layout and existing action-mapping tests are unchanged.
…e width EdgeOverlayController observes a single combined runtime configuration (masterEnabled + the four geometry settings) and rebuilds the zones exactly once per committed change (distinctUntilChanged on the whole settings record), so a geometry change updates the active overlay windows immediately — no app restart, accessibility toggle, or reboot required. Pass-through is observed separately since it changes interactivity, not geometry. Side Back zones are now anchored toward the BOTTOM of the screen (measured upward from just above the reserved bottom gesture band) instead of vertically centered, matching the new activation-height semantics. Corner precedence is deterministic: the bottom Home/Recents band has priority, and the side zones are offset upward by the bottom band's height so the two never ambiguously overlap in the corners — a vertical gesture in the bottom band reliably belongs to Home/Recents, and Back works immediately above the band. Sensitivity multiplies only the base zone thickness; the nav-bar inset is added afterward and is never multiplied, preserving the slippery-nav-bar handoff. The last applied settings are cached so a rotation (display-geometry change) re-lays out with the same settings rather than racing the combined flow; no duplicate windows are created (rebuild detaches all first). HomeIndicator takes a barWidthDp parameter; its visual width follows the configured bottom activation width (homeHandleWidthDp), staying a compact handle rather than scaling with the invisible touch depth (sensitivity does not widen it). Default 80% remains 108dp. The Back indicator is unchanged.
A new "Gesture areas" card (separate from the gesture-action rows) exposes the four geometry settings with Material 3 sliders: - Back: Activation height (10..100%), Edge sensitivity (0.5x..4.0x, shows dp) - Home & Recents: Activation width (10..100%), Edge sensitivity (0.5x..4.0x, shows dp) Home and Recents are noted as sharing the same bottom region. Sliders use local Compose state while dragging and persist on onValueChangeFinished (snapped to the allowed step), so dragging a thumb doesn't spam DataStore writes or trigger dozens of overlay rebuilds per second; the committed value updates the active gesture windows immediately. No Save button. MainViewModel exposes gestureZoneSettings as a StateFlow and four write-through setters; the repository remains the source of truth for clamped values. All user-facing strings are resources.
Expand the gesture-zone regression tests for the configurable geometry: - defaults (80/80/1x/1x -> 16dp/12dp) and defaultZones_matchLegacyFixedLayout - percentage geometry (Back height 10/50/100, bottom width 10/50/100; one setting drives both sides) - sensitivity (Back 0.5x->8dp/1x->16dp/4x->64dp; bottom 0.5x->6dp/1x->12dp/4x->48dp) - sensitivity does not change swipe-distance threshold (model exposes no such field) - clampPercent / clampSensitivity normalise out-of-range values; corrupted settings never produce zero/negative thickness - geometry changes never alter action/direction mapping (Back/Home/Recents, directions) - side vs bottom corner-overlap precedence: modelled purely, side zone ends at/above the bottom band top for every combination of height/width/sensitivity - Home handle width mapping: 80%->108dp, 100%->135dp, 10%->24dp (min), 50%->~67dp, never below 24 or above 135
The visible Home/Recents bottom bar now spans the actual configured bottom activation region 1:1 instead of a compact 108dp×pct/80 handle capped at 135dp. At 100% the bar spans the full bottom activation width; at 10% it spans the central 10%; 50% → central half; etc. — exactly matching the touch zone. The width is no longer a separate dp-based formula: HomeIndicator takes a resolved pixel width (barWidthPx) and the controller passes the SAME resolvedBottomZone.widthPx from computeGestureZoneLayout (the single production geometry helper that also sizes the bottom touch window), so the bar can never drift from the actual activation region. The old homeHandleWidthDp() helper and its DEFAULT/MIN/MAX_HANDLE_WIDTH_DP constants are removed. Only horizontal width follows the activation percentage. Bottom edge sensitivity still affects only the invisible vertical touch depth — it does not change the bar's width, height, opacity, shape, or animation. Rotation / display-geometry changes rebuild via the existing controller path, so the bar re-sizes correctly across portrait/landscape with no stale cached width. Builds on the shared computeGestureZoneLayout geometry (ScreenGeometry / ZoneLayout) introduced for the corner-precedence fix, extending that single source of truth to the indicator rather than adding a parallel formula. Tests: the old compact-handle dp tests are replaced by regression tests using the production helper — homeIndicatorWidth_equalsBottomTouchZoneWidth for every 10..100% across representative widths/densities/sensitivities/nav insets, plus explicit 1200px and 1080px percentage checks (10/50/80/100%).
The four Gesture Areas sliders no longer expand on the main dashboard; the main
screen now shows a compact entry card (parallel to the App compatibility entry)
that opens a dedicated Gesture areas screen.
Main screen:
- SectionHeader("Gesture areas") is kept; under it a GestureAreasEntryCard with
the same Surface/extraLarge/surfaceContainerHigh visual language as
CompatEntryCard — full-width click target, bodyMedium description, trailing ›.
Dedicated GestureAreasScreen (new ui/GestureAreasScreen.kt):
- Mirrors CompatibilityScreen: Scaffold + TopAppBar (back arrow, "Gesture areas"
title, surface-colored), scrollable column, short info card, then the detailed
settings card with the existing Back / Home & Recents grouping, labels, hints,
and immediate-persistence slider behavior unchanged.
- GestureAreasCard, AreaSectionHeader, PercentSlider, SensitivitySlider are moved
here (private) from MainActivity.kt, so MainActivity returns to being the
dashboard + lightweight navigation.
Navigation: a mutually-exclusive AppScreen enum (MAIN / GESTURE_AREAS /
COMPATIBILITY) drives the existing local Compose navigation — no Navigation
Compose dependency. BackHandler returns to MAIN; system Back does the same;
CompatibilityScreen's internal App Picker navigation is unchanged.
No runtime gesture geometry, settings, DataStore keys, defaults, clamping,
computeGestureZoneLayout, EdgeOverlayController, indicator sizing, or overlay
architecture is modified — UI/navigation restructuring only. New strings:
gesture_areas_screen_title, gesture_areas_entry_desc.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds user-configurable activation geometry for the Back and Home/Recents gestures:
Why
The existing fixed gesture zones can be too small for some devices/users, especially the bottom Home/Recents start region. Different screen sizes, cases, grip styles and navigation setups benefit from different activation depths and coverage. This lets the user tune the hit regions without changing the actual swipe-distance thresholds or navigation actions — the gesture recognizer, hold timing, Recents semantics, replay and tap handling are unchanged.
What changed
Back
Home & Recents
Geometry
computeGestureZoneLayout(settings, screenGeometry)) — no parallel math.UI
Gesture areasscreen, reached from a compact entry card on the main screen (parallel to the existing App compatibility entry).onValueChangeFinished) — no Save button, no app restart, no Accessibility toggle.Validation
26 unit tests pass (0 failures, 0 errors), including regression tests for the configurable geometry, the corner-overlap precedence (using the same production geometry helper), and the indicator-width invariant. Lint: 0 errors, only pre-existing warnings. No instrumentation test was run by the build environment.
Real-device validation
Tested using the release APK on a real Xiaomi/HyperOS Android device. Confirmed:
Test APK
I tested this build on a real device, but additional device coverage would be useful. A prebuilt test APK matching this PR is available from my fork:
https://github.com/Dragonk/Ogesture/releases/tag/gesture-zones-test-2026-08-25
APK asset: https://github.com/Dragonk/Ogesture/releases/download/gesture-zones-test-2026-08-25/ogesture-configurable-gesture-zones-release.apk
This is a test build from my fork, not an official upstream release. Because the test APK is not signed with the upstream production key, users with the official Ogesture build installed may need to uninstall it before installing this APK (
INSTALL_FAILED_UPDATE_INCOMPATIBLEotherwise). Users already running a Dragonk test build signed with the same debug certificate can update withadb install -r.Notes / risks
mainand the diff reduces to the configurable-zone changes.