Skip to content

Add configurable gesture activation zones - #4

Open
Dragonk wants to merge 11 commits into
tanujnotes:mainfrom
Dragonk:feat/configurable-gesture-zones
Open

Add configurable gesture activation zones#4
Dragonk wants to merge 11 commits into
tanujnotes:mainfrom
Dragonk:feat/configurable-gesture-zones

Conversation

@Dragonk

@Dragonk Dragonk commented Aug 25, 2026

Copy link
Copy Markdown

Depends on #3. Until #3 is merged, GitHub shows the prerequisite accessibility-overlay commits in this PR's diff. After #3 lands, the diff should reduce to the configurable gesture-zone changes only.

Summary

Adds user-configurable activation geometry for the Back and Home/Recents gestures:

  • Back activation height — how much of the side edge can start Back (10–100%);
  • Home/Recents activation width — the horizontal width of the bottom gesture region (10–100%);
  • independent Back edge sensitivity — how far inward from the side edge a touch may start (0.5×–4.0×);
  • independent Home/Recents bottom-edge sensitivity — how far above the bottom edge a touch may start (0.5×–4.0×);
  • the visible bottom indicator now uses the same resolved width as the real touch zone, so at 100% it spans the full bottom activation region;
  • a dedicated Gesture Areas settings screen, parallel to the existing App compatibility screen.

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

  • Activation height: 10–100%, 10% increments.
  • The Back activation region is anchored upward from the lower usable part of the screen (not vertically centered), so the percentage describes the span measured from the bottom.
  • Edge sensitivity: 0.5×–4.0× relative to the original 16 dp hit depth (0.5× → 8 dp, 1× → 16 dp, 4× → 64 dp).
  • The same setting is shared by the left and right Back zones.

Home & Recents

  • Activation width: 10–100%, centered.
  • Bottom edge sensitivity: 0.5×–4.0× relative to the original 12 dp hit depth (0.5× → 6 dp, 1× → 12 dp, 4× → 48 dp).
  • Home and Recents continue sharing the same bottom region (Recents is the long action of the bottom gesture).
  • The visible bottom indicator uses the same resolved horizontal width as the bottom touch zone, so the bar matches the actual activation region 1:1 (10% → central 10%, 100% → full width). Only horizontal width follows the percentage; sensitivity does not widen the bar.

Geometry

  • Production and tests share a single pure geometry resolver (computeGestureZoneLayout(settings, screenGeometry)) — no parallel math.
  • The bottom Home/Recents band has deterministic precedence in the lower corners: the side Back zones are offset upward by the effective bottom band height, so a vertical gesture in the bottom band reliably belongs to Home/Recents, and Back works immediately above the band. No ambiguous corner overlap.
  • Sensitivity scales only the activation-region depth (the starting hit region); the navigation-bar inset is added afterward and is never multiplied.
  • Swipe-distance thresholds (Back 24 dp, Bottom 10 dp) are unchanged.

UI

  • The four settings live on a dedicated Gesture areas screen, reached from a compact entry card on the main screen (parallel to the existing App compatibility entry).
  • Settings persist through DataStore; changes apply immediately after the slider commits (onValueChangeFinished) — no Save button, no app restart, no Accessibility toggle.
  • Sliders use local drag state while dragging and persist the snapped value on release, avoiding dozens of DataStore writes / overlay rebuilds per second.

Validation

./gradlew testDebugUnitTest  -> BUILD SUCCESSFUL
./gradlew lintDebug          -> BUILD SUCCESSFUL
./gradlew assembleDebug      -> BUILD SUCCESSFUL
./gradlew assembleRelease    -> BUILD SUCCESSFUL

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:

  • configurable Back activation height works;
  • configurable bottom activation width works;
  • independent Back and bottom sensitivity settings work;
  • Back/Home/Recents gesture behavior is working;
  • dedicated Gesture Areas settings screen works correctly;
  • settings persist and apply correctly;
  • 100% bottom activation width produces the intended full-width indicator;
  • gestures continue to work inside Android system Settings;
  • the previous persistent "displaying over other apps" system notification remains absent.

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_INCOMPATIBLE otherwise). Users already running a Dragonk test build signed with the same debug certificate can update with adb install -r.

Notes / risks

  • This PR is currently stacked on and depends on Use accessibility overlays for gesture zones #3 (the Accessibility Overlay migration). Once Use accessibility overlays for gesture zones #3 merges, this branch can be rebased onto main and the diff reduces to the configurable-zone changes.
  • Real-device validation exists, but more devices/configurations are welcome.
  • Changing the touch-region depth can intentionally capture a larger edge area — that is why the setting is user-controlled.
  • The Back indicator visuals themselves were not changed.

Dragonk added 11 commits August 25, 2026 17:09
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant