Repository navigation
Conversation
… wake-and-retry Remote screen: - Merge D-pad and trackpad into one Siri-Remote-style pad: rim direction buttons, circular touch surface in the middle, and the whole pad surface is live (swipes no longer depend on where the finger lands) - Swipes stream Press/Hold/Release touch samples against a fixed 300 dp reference pad with a pointer-ballistics gain curve, so tvOS momentum scrolling matches the hardware remote - Adaptive layout: the pad takes the leftover height, landscape shows the app drawer full-screen, and the now-playing card no longer jolts the column when playback state arrives Startup: - The last device appears instantly (persisted) and devices stream in as discovery resolves them - If exactly one pairing exists, auto-connect straight into the remote; with several pairings the list stays - Wake & retry: when a connect times out on a sleeping Apple TV, the list offers a wake button (minimal Companion session: connect, authenticate, WAKE, retry) i18n & theme: - zh-CN localization for all screens and notifications - Light theme alongside the dark one, following the system Small fixes: - Fixed an intermittent startup crash (touch channel consumed before init) - Haptic ticks on pad/volume, hold-to-repeat volume - OK folds the keyboard panel; simplified now-playing card
There was a problem hiding this comment.
🟡 Changes recommended
There are a few correctness/accessibility issues in changed code (notably the Now Playing notification channel name, remaining hardcoded strings, and lost button semantics on hold-to-repeat controls) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR significantly reworks the Android remote UI and startup flow to better match the Siri Remote interaction model, improves discovery/connect UX (including wake-and-retry), and adds localization/theme updates across the app.
Changes:
- Replaces the old D-pad/trackpad mode toggle with a combined Siri-remote-style pad that streams touch samples (with velocity-based gain via
TouchAcceleration). - Improves startup/device list UX: instant last-device display, incremental discovery updates, one-shot auto-connect when a single pairing exists, and wake-and-retry for sleeping devices.
- Adds zh localization resources and introduces a system-following light theme; broadens string-resource usage across screens/notifications.
File summaries
| File | Description |
|---|---|
| protocol/src/main/kotlin/dev/atvremote/protocol/companion/TouchAcceleration.kt | Adds the speed→gain curve used to scale touch movement for tvOS-like momentum behavior. |
| protocol/src/main/kotlin/dev/atvremote/protocol/companion/AppleTvRemote.kt | Adds explicit button down/up APIs to support held-select behavior. |
| app/src/main/res/values/strings.xml | Introduces a large set of new string resources for UI, errors, and notifications. |
| app/src/main/res/values-zh/strings.xml | Adds Chinese translations for the newly introduced strings. |
| app/src/main/kotlin/dev/atvremote/app/RemoteViewModel.kt | Implements auto-connect, incremental discovery merge, wake-and-retry, and serialized touch-sample streaming. |
| app/src/main/kotlin/dev/atvremote/app/RemoteScreen.kt | Major UI refactor: adaptive layout, app drawer behavior, combined touch pad, hold-to-repeat volume, and more string-resource usage. |
| app/src/main/kotlin/dev/atvremote/app/PinEntryScreen.kt | Switches pairing UI text to string resources. |
| app/src/main/kotlin/dev/atvremote/app/NsdDiscovery.kt | Adds a scan overload that streams resolved devices via callback while discovery continues. |
| app/src/main/kotlin/dev/atvremote/app/NowPlayingService.kt | Localizes “Nothing playing” and transport action labels; adjusts notification channel creation. |
| app/src/main/kotlin/dev/atvremote/app/MainActivity.kt | Adds light theme and routes hardware volume keys to TV volume when appropriate. |
| app/src/main/kotlin/dev/atvremote/app/KeyboardNotification.kt | Localizes notification text and channel metadata via string resources. |
| app/src/main/kotlin/dev/atvremote/app/DeviceListScreen.kt | Adds wake-and-retry CTA and auto-connect loading state; localizes list UI. |
| app/src/main/kotlin/dev/atvremote/app/CredentialStore.kt | Persists/loads “last device” and exposes paired-key enumeration for auto-connect logic. |
Review details
Suppressed comments (2)
app/src/main/kotlin/dev/atvremote/app/RemoteScreen.kt:604
- RimDirection uses
button.name.lowercase()for the Icon contentDescription, which isn’t localized and can expose internal enum naming to screen readers. Since you already have localized direction strings (cd_up/cd_down/cd_left/cd_right), prefer those for accessibility.
Icon(
icon,
contentDescription = button.name.lowercase(),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.size(iconSize),
)
app/src/main/kotlin/dev/atvremote/app/RemoteScreen.kt:956
- PillButton replaced
.clickablewith a customrepeatOnHoldpointerInput handler. This likely removes button semantics/ripple for accessibility services (TalkBack) and non-touch input, since pointerInput alone does not expose an onClick action in semantics.
Row(
modifier = modifier
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.repeatOnHold(onStep = onClick)
.padding(vertical = 9.dp),
- Files reviewed: 13/13 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| val channel = NotificationChannel( | ||
| CHANNEL_ID, | ||
| "Now playing", | ||
| getString(R.string.nothing_playing), |
| lastPos = change.position | ||
| lastTime = change.uptimeMillis | ||
| advance(change.position, change.uptimeMillis) |
| // Touch samples for the pad. Declared ahead of init: the drain coroutine | ||
| // starts there, and a property read before its declaration line would be | ||
| // null — the intermittent startup crash. | ||
| private val touchInput = Channel<TouchSample>(Channel.UNLIMITED) | ||
|
|
| RoundButton( | ||
| icon = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, | ||
| description = if (isPlaying) "Pause" else "Play", | ||
| ) { vm.press(Button.PLAY_PAUSE) } |
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <!-- 设备列表 --> | ||
| <string name="app_name">Apple TV 遥控器</string> | ||
| <string name="devices_subtitle">网络中的设备</string> |
- The media notification channel was created with the nothing-playing string as its user-visible name; use the proper channel name (and bump the channel id — the system caches channel names at first creation) - Localize the play/pause content description that was still hardcoded - Rim arrow content descriptions: localized strings instead of raw enum names - Volume pills: restore button semantics for TalkBack (pointerInput alone exposes no click action)
Tapping near a rim arrow could fall outside its small hit box and be read by the pad surface as a centre tap, i.e. an accidental Select. Instead, the whole pad classifies gestures positionally: taps inside the centre circle select, taps on the rim steer by the dominant axis of the tap position (quadrant-sized targets), and drags anywhere stream touch samples. The rim chevrons and centre circle become visual affordances.
Positional classification waited for the finger to lift (or the long-press timeout) before sending a direction, which read as lag. Rim touches now hold the HID direction down from the instant the finger lands — tvOS repeats it while held — and release when the finger lifts. Only centre touches keep the timed long-press classification.
The centre-select radius grows past the visual circle (0.38 of the short side vs 0.30), and taps are classified by the midpoint of the gesture, so a tap that drifts near the circle edge still selects instead of flipping to a rim direction.
The tap-versus-swipe threshold was the system touch slop (~8 dp). Quick taps almost always drift past it, so the pad streamed a micro-swipe — and in video players even a tiny horizontal swipe skips ±10 seconds, which read as mysterious rewinds on centre taps and as vertical instability. 20 dp keeps taps taps while deliberate swipes still cross it immediately.
Hi — first of all, thank you for this project. The protocol layer (Companion, MRP, AirPlay 2) and the overall architecture are excellent, and everything in this PR was straightforward to build on top of it.
This reworks the remote screen and adds a few quality-of-life features. The diff is large but splits cleanly into four areas:
Remote screen
TouchAcceleration), so tvOS momentum scrolling matches the hardware remoteStartup
i18n & theme
Small fixes
Testing
Verified end-to-end against an Apple TV 4K (AppleTV14,1): discovery, pairing, pad gestures, keyboard, now playing, and wake. Compose layout changes were checked in both orientations and with the now-playing card open.
No pressure to merge — mainly sharing the work back since the protocol base made it possible. Happy to split, reshape, or drop anything that doesn't fit the project's direction.