Skip to content

Remote: Siri-style combined pad, adaptive layout, zh-CN, light theme, wake-and-retry - #1

Closed
nbzz wants to merge 6 commits into
msthind04:mainfrom
nbzz:upstream-pr
Closed

nbzz wants to merge 6 commits into
msthind04:mainfrom
nbzz:upstream-pr

Conversation

@nbzz

@nbzz nbzz commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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

  • Merged D-pad and trackpad into one Siri-Remote-style pad: directional buttons on the rim, 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 (new TouchAcceleration), so tvOS momentum scrolling matches the hardware remote
  • Adaptive layout: the pad takes whatever height is left; landscape shows the app drawer full-screen; 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, the app auto-connects 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 — a minimal Companion session (connect, authenticate, WAKE) followed by a 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 initialization)
  • Haptic ticks on pad/volume, hold-to-repeat volume
  • OK folds the keyboard panel; simplified now-playing card

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.

… 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
Copilot AI lite review requested due to automatic review settings September 5, 2026 07:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 .clickable with a custom repeatOnHold pointerInput 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),
Comment on lines +465 to +467
lastPos = change.position
lastTime = change.uptimeMillis
advance(change.position, change.uptimeMillis)
Comment on lines +138 to +142
// 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)

Comment on lines +347 to +350
RoundButton(
icon = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
description = if (isPlaying) "Pause" else "Play",
) { vm.press(Button.PLAY_PAUSE) }
Comment on lines +1 to +5
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 设备列表 -->
<string name="app_name">Apple TV 遥控器</string>
<string name="devices_subtitle">网络中的设备</string>
nbzz added 5 commits September 5, 2026 15:49
- 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.
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.

2 participants