Skip to content
Open
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
22 changes: 22 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ The Accessibility service filters hardware keys, draws the overlay and performs

The cursor overlay exists only while mouse mode is visible. Cursor position is local to the actual laid-out overlay viewport; touch gestures use that viewport's screen origin plus the same pointer coordinate.

## Touchpad

```text
touch on pad window
↓
TouchpadView ──► PadGestureEngine (pure Java, no Android types)
↓ listener
MouseAccessibilityService
┌───────────┼─────────────┐
PointerController scrollDirect performGlobalAction / mode changes
```

`TouchpadOverlay` owns an accessibility-overlay window (full screen or a rectangle stored as screen fractions) that, unlike the cursor overlay, receives touches. `TouchpadView` converts MotionEvents to engine calls and supplies a one-shot `Handler` timer; `PadGestureEngine` recognizes one-finger movement, tap, tap-then-hold drag and two- and three-finger swipes and reports them through a listener, so it can be unit-tested on a plain JVM with a fake timer.

A tap does not click on release. It waits `tapDelayMs`; a second touch inside that window parks the engine in an undecided state, and only then is the outcome chosen: movement past the touch slop or a hold longer than the tap timeout starts a drag (the first tap is discarded), a quick release is a double tap (two clicks, spaced so they are two separate touches), and extra fingers make it a swipe (first tap discarded). The drag is an ordinary held virtual finger, so scrolling comes from the app's own touch handling rather than from Accessibility scroll actions. Two- and three-finger swipes share one implementation: one event per touch once the centroid travels past a threshold, mapped to a configurable `PadAction` per finger count and direction.

Because injected gestures are hit-tested like real touches, a touchable pad above the cursor would swallow KeyCursor's own clicks. `ContinuousTouchController` therefore reports stroke begin/end to the service, which switches the pad window to `FLAG_NOT_TOUCHABLE` for the duration of an injected stroke whose target lies on the pad, and delays the first injected touch by a short settle time so the flag change takes effect first. With the pad hidden, or the cursor outside a rectangular pad, nothing changes.

Once more than one finger has touched, a gesture never falls back to cursor movement when fingers lift; it stays inert until all fingers are up. A touch cancel during tap-and-drag releases the held virtual finger.

Hiding the cursor (long-press of the Mouse key) also hides the pad, so a dead pad can never sit over the screen.

## Mouse modes

- **Normal** — movement only; the click binding produces real touch DOWN / MOVE / UP and can be held for drag.
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Unreleased

- Added a touchpad: the whole screen, or a movable/resizable rectangle, becomes a trackpad for the cursor.
- Touchpad gestures: tap to click, tap-then-hold to drag and scroll, double tap for two clicks, and two- and three-finger swipes in four directions, each bound to a configurable action (Back, Home, Recents, Notifications, Quick settings, Power menu, Lock screen, Screenshot, toggle Hold, next cursor, hide touchpad).
- Adjustable tap delay: every tap waits before clicking, so starting a hold-and-drag or a multi-finger swipe never leaves an accidental click.
- Pad opacity can go to 0% on the full-screen pad (the movable pad keeps a 5% minimum).
- Added a Quick Settings tile that shows/hides the touchpad, plus a `PAD_TOGGLE` external broadcast.
- Touchpad settings: sensitivity, pointer acceleration, opacity, natural scrolling, scroll distance, area mode, pad reset.
- Keyboard control is unchanged and keeps working alongside the touchpad.

## 0.6.0 — 2026-08-26

- Added three mouse modes: Normal, Hold and Scroll.
Expand Down
1 change: 1 addition & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ KeyCursor does not collect, transmit or sell user data.
- Bindings, cursor preferences and the external-control opt-in are stored locally in Android `SharedPreferences`.
- The Accessibility service observes hardware key events, draws the cursor overlay and injects user-requested touch gestures.
- While **Scroll mode** is actively used, KeyCursor retrieves the active Accessibility node tree only to find a scrollable element under the cursor and inspect its bounds/supported scroll actions. It does not read, store or transmit node text or other UI content, and it does not subscribe to UI accessibility events.
- The touchpad overlay receives touches only on its own surface and uses them to move the cursor, click, scroll and run the configured swipe actions. Touch positions are not stored or transmitted.
- External broadcast control is disabled by default and must be enabled explicitly in the app.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ KeyCursor turns hardware keys into a real touch cursor with movement, click-and-
- optional blending of simultaneously held directions into intermediate angles
- system light / dark theme
- optional Mouse-key capture while the cursor is visible
- touchpad mode: use the whole screen or a movable pad as a trackpad:
- tap
- tap-then-hold to drag/scroll
- configurable two- and three-finger swipes
- Quick Settings tile to show / hide the touchpad
- optional broadcast control for Key Mapper, Tasker, Automate or ADB
- no network permission, analytics or ads

Expand All @@ -39,6 +44,25 @@ Tested on **Fujitsu F-03L** (Android 8.1).

When the cursor is hidden, keys pass through normally. While it is visible, movement/click/cursor-switch keys are consumed so Android focus does not move at the same time. **Capture Mouse key while cursor is visible** can also consume the Mouse key only while the cursor is active, letting the same physical key keep an app shortcut when KeyCursor is hidden.

## Touchpad

Add the **Touchpad** tile to Quick Settings (or use *Show / hide touchpad now* in the app) to turn the screen, or part of it, into a trackpad. The cursor appears with the pad and both go away when you hide it.

| Gesture | Result |
| --- | --- |
| One finger | Move the cursor (relative, with optional acceleration) |
| Tap | Click at the cursor, after the **tap delay** |
| Tap, then touch again and hold or drag | Hold the button and drag. This is a real touch drag, so it scrolls lists. The first tap does not click |
| Tap, then tap again | Double tap: two clicks |
| Two fingers, swipe up / down / left / right | Run the action you assigned to that direction (none by default) |
| Three fingers, swipe up / down / left / right | Run the action you assigned to that direction (Home / Notifications / Back / Recents by default) |

**Tap delay** (0-500 ms, default 150) is how long every tap waits before it clicks. If you touch again inside that time and hold or drag, the tap is dropped, so starting a scroll never leaves a stray click behind. Set it to 0 to click instantly, which turns tap-then-hold off. A tap that turns out to be the start of a two- or three-finger swipe is also dropped.

**Area** can be a *movable pad* (drag the bar at the top to move it, the corner grip to resize it) or the *full screen*. Opacity goes down to 0% on the full-screen pad; the movable pad keeps at least 5% so it can always be found. Keyboard control keeps working while the touchpad is up.

Known limitation: Android documents that injecting a gesture cancels gestures already in progress, including the user's own touch. Clicks are injected after your finger lifts, so they are unaffected, but tap-then-hold and Hold mode inject a touch while a finger may still be on the pad. If drags drop out on your device, turn off *Tap, then hold to drag / scroll*, or use a pad that does not cover the drag target.

## Direction behavior

Hardware bindings are matched as physical keys first. Android-rotated D-pad key codes are normalized so the default mapping stays stable after rotation.
Expand All @@ -64,7 +88,7 @@ adb shell am broadcast \
-n ru.vasys.keycursor/.input.ExternalTriggerReceiver
```

Other actions include `MODE_TOGGLE`, `CURSOR_NEXT`, `LEFT_DOWN`, `LEFT_UP` and `MOVE_*`. `MODE_TOGGLE` switches Normal / Hold.
Other actions include `PAD_TOGGLE` (show / hide the touchpad), `MODE_TOGGLE`, `CURSOR_NEXT`, `LEFT_DOWN`, `LEFT_UP` and `MOVE_*`. `MODE_TOGGLE` switches Normal / Hold.

## Build

Expand Down
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
android:resource="@xml/accessibility_service" />
</service>

<service
android:name=".service.PadTileService"
android:exported="true"
android:icon="@drawable/ic_tile_touchpad"
android:label="@string/tile_label"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>

<receiver
android:name=".input.ExternalTriggerReceiver"
android:enabled="false"
Expand All @@ -41,6 +52,7 @@
<action android:name="ru.vasys.keycursor.input.PULSE" />
<action android:name="ru.vasys.keycursor.action.MOUSE_TOGGLE" />
<action android:name="ru.vasys.keycursor.action.MODE_TOGGLE" />
<action android:name="ru.vasys.keycursor.action.PAD_TOGGLE" />
<action android:name="ru.vasys.keycursor.action.MOUSE_KEY_SHORT" />
<action android:name="ru.vasys.keycursor.action.MOUSE_KEY_LONG" />
<action android:name="ru.vasys.keycursor.action.MOUSE_KEY_DOWN" />
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/ru/vasys/keycursor/core/PadAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ru.vasys.keycursor.core;

import android.os.Build;

/** Actions that can be bound to a two or three-finger swipe on the touchpad. */
public enum PadAction {
NONE("Do nothing", 1),
BACK("Back", 1),
HOME("Home", 1),
RECENTS("Recent apps", 1),
NOTIFICATIONS("Notifications", 1),
QUICK_SETTINGS("Quick settings", 1),
POWER_DIALOG("Power menu", 1),
LOCK_SCREEN("Lock screen", Build.VERSION_CODES.P),
SCREENSHOT("Screenshot", Build.VERSION_CODES.P),
TOGGLE_HOLD("Toggle Hold mode", 1),
NEXT_CURSOR("Next cursor style", 1),
HIDE_PAD("Hide touchpad", 1);

public final String label;
public final int minSdk;

PadAction(String label, int minSdk) {
this.label = label;
this.minSdk = minSdk;
}

public boolean isAvailable() {
return Build.VERSION.SDK_INT >= minSdk;
}

public static PadAction fromName(String name, PadAction fallback) {
if (name == null) return fallback;
try {
return valueOf(name);
} catch (IllegalArgumentException e) {
return fallback;
}
}
}
Loading