Feature/device profiles - #751
Merged
maxjivi05 merged 13 commits intoSep 12, 2026
Merged
Conversation
…tton presses WinNative shipped one set of bundled touch layouts for every device. Their normalized coordinates are resolution-independent, but element size is derived from view width alone (snappingSize = width/100), so on a 16:10 tablet the same layout stretches vertically relative to its own buttons and the inboard controls drift out of thumb reach. Measured on a RedMagic Astra 2 (2400x1504 at 310.6 x 312.6 dpi = 195.0 x 123.0 mm, 9.08 in): with the grip pivot 5 mm outboard of the panel and 37 mm up from the bottom, the stock layouts put the D-pad at 67 mm and the right stick at 66 mm from the thumb pivot, and the inboard face button at 55 mm. Comfortable thumb travel on a tablet held in two hands is roughly 42 mm neutral, 53 mm comfortable, 64 mm stretch. Three of those controls were past the limit for their usage tier. Device Profile DeviceProfile classifies the host from the marketing-name properties (ro.vendor.product.ztename, ro.vendor.feature.nubia.exif.module) and a Build token haystack, so all regional Astra 2 SKUs match (PQ85P01 board family, NP06J model family) while other RedMagic and nubia devices do not. Anything unmatched is Default. PluviaApp seeds the choice once, after the child-process guard; the user can override it from the new Settings > Device page, and the override is never overwritten by later detection. The profile is a global app setting built as the single seam for per-device defaults. Input control layouts and the adaptive-joystick recommendation are wired up now; library icon sizing, session menu button sizing and per-game recommended settings are listed on the page as Coming Soon and only need their resolver pointed at DeviceProfileSettings. Bundled layouts Device variants live in a sibling asset directory (inputcontrols/profiles-astra2) rather than a subdirectory of profiles/. AssetManager.list returns directories too, and the sync loop opens every entry inside a single try block, so a nested variant directory would throw FileNotFoundException on the directory entry and abandon the rest of the loop - with the gate prefs already committed, that would silently skip the sync until the next app version. The listing now also filters to .icp, and the gate commits after the copy loop rather than before it. Switching device profile re-enters the sync through a third gate key. A working layout is replaced only when it still matches its pristine backup (FileUtils.contentEquals); an edited layout is left alone, while its backup is refreshed so Reset offers the layout bundled for the current device. Profile ids stay 1/2/3/6/7/8 in every variant because shortcuts persist the id and both the drawer and the shortcut dialog index by list position. All six Astra 2 layouts follow a DualSense split: D-pad above the left stick, ABXY above the right stick, triggers in the top outer corners for the index fingers, Start and Select centred out of both thumb arcs. L3 sits on the right half and R3 on the left, since the thumb owning a stick cannot also click it. Every gameplay control is inside its tier limit, no two elements overlap, and nothing sits closer than 4 mm to a neighbour or 6 mm to a screen edge. Press semantics A BUTTON no longer releases when the finger slides outside its bounds; it is held until the pointer actually lifts, which is what the release-on-exit branch in handleTouchMove used to prevent. Bundled layouts never persist the swipeable flag, so its true default made every shipped button a swipe target and the old behaviour dropped a held button on any drift. A pointer now captures a list of elements instead of one, so stacked buttons both fire from a single touch. The first accepting element is captured whatever its type, further BUTTONs at the same point are added, and an expanded radial menu suppresses co-capture because its hit test balloons well past its box. ACTION_DOWN and ACTION_UP run inside the batching window so one pointer pressing several bindings still produces a single gamepad state send. Sliding a held button onto another swipe target now adds it rather than swapping, and the D-pad and radial hand-off path is unchanged. The L3/R3 minimum-hold release carried no pointer identity, so a re-press within 300 ms was cancelled by the previous release's pending callback; it now checks a press generation. releaseActiveTouchElements also syncs the captured-pointer set, which it previously left stale for TouchpadView for one gesture. Adaptive joysticks The four sites that resolved the container default to a hard-coded "0" now read the profile recommendation, so Astra 2 containers that have never set the extra default to adaptive. Containers with an explicit value are untouched. Also back-fills library_games_store_none_owned, which was added to the base strings without translations, and adds the 34 new Device Profile strings to all 22 locales.
The first pass placed the two sticks asymmetrically - left at 25.7 mm and high, right at 23.4 mm and low - because it was reproducing an Xbox face, where the sticks sit at different heights. On a touchscreen there is no handle offset to justify that, and it left the clusters spread over the full height of the panel. Both sticks are now identical and exactly mirrored: 23.4 mm, centred at x = 460 and x = 1940, both at y = 1150, both 43.1 mm from their grip pivot. The D-pad and the ABXY diamond mirror each other too, at x = 301 and x = 2099, sharing y = 710. Everything moved down and closer together. Measured from the pivot 5 mm outboard of the panel and 37 mm up from the bottom: control before now face buttons 55.0 mm 50.5 mm D-pad 55.1 mm 40.6 mm right stick 49.7 mm 43.1 mm L3 / R3 29 / 61 mm 27.8 mm both Every thumb control now lives in the bottom 78 mm of the 123 mm panel instead of being spread over 100 mm, and the ABXY diamond is tighter - a 155 px radius with 11.7 mm buttons, so the thumb covers all four without leaving the cluster. L3 and R3 moved into the bottom outer corners, the easiest reach on the panel. The diamond radius also has to clear twice the button half-size or the square bounding boxes overlap even when the drawn circles do not, and the engine hit tests against the box. GameHub now shares the Virtual Gamepad geometry outright rather than carrying its own scales, since VisualStyle only changes how elements are drawn. RTS keys drop to scale 1.05 on a 190 px row pitch so the grid keeps a 5 mm gap and still clears the bottom edge.
…umb rest The previous two passes were tuned against a guessed grip. Reviewing them turned up three things that invalidated parts of that work. First, the hardware. The Astra 2 body is 207.1 x 134.2 x 6.9 mm at 363 g, the panel is 195.0 x 123.0 mm (310.6 x 312.6 dpi, 9.08 in), there is no display cutout anywhere, and the tablet has NO shoulder triggers of any kind - physical or capacitive - unlike RedMagic's phones. So every bumper and trigger has to be on-screen, and the index fingers are not pinned to the top edge by hardware. The top edge does carry the volume rocker and the red Magic Key, which jumps straight into Game Space; an index finger curling over the top-right corner can hit it, so the triggers moved inboard from the extreme corners (RT now has 21.5 mm of clearance from the right edge instead of 7 mm). Second, the grip. Reach was being computed from a pivot assumed to sit 5 mm outboard of the panel edge, 37 mm up. Published anthropometry puts the thumb MCP on the glass, roughly 20-35 mm inboard, at or just below panel centre - a difference larger than the entire design tolerance. Evaluating the shipped layout both ways gave contradictory verdicts, so the pivot was measured instead: 25 seconds of raw contacts off /dev/input/event6 while the tablet was held normally, 1030 samples splitting 523/507 into two clusters, both medians at 33 mm inboard and 35 mm up from the bottom edge. That is now the anchor, and it is a measurement rather than a model. Third, spacing. Adjacent face buttons were 17.8 mm centre to centre. A thumb contact patch is about 25 mm wide, so independent presses need roughly 20 mm of centre-to-centre separation; below that the thumb straddles two targets. The diamond radius went from 155 to 174 px, which puts adjacent centres at exactly 20.0 mm, and the face buttons grew from 11.7 to 13.3 mm - above the measured 9.2-9.6 mm thumb plateau with margin for the +25% scatter seen away from screen centre. Every element is now inside its limit under all three candidate pivot models, not just one: held controls (both sticks, D-pad) never exceed 45 mm and taps never exceed 60 mm whether the grip is low, middle or high. Measured against the captured thumb rest, nothing sits more than 40.5 mm away and most controls are within 30 mm. The sticks stay uniform and exactly mirrored at 23.4 mm. Worth noting that the hand-edited Virtual Gamepad already on the tablet places its sticks 30-37 mm inboard and 24 mm up, which is within a few millimetres of both the measured thumb rest and where this layout puts them - three independent estimates agreeing. That profile is left untouched by the sync because it differs from its pristine backup.
Sticky press kept a button down when the finger drifted off it, but the move handler still looked for another swipe target under that finger and pressed it as well. Holding RT and drifting onto RB therefore ended up holding both, which is worse than the drop it replaced: the finger never left the screen, so there was no moment at which the player asked for the second press. A pointer that already holds an element no longer acquires anything else. The element stays down until the pointer actually lifts, and a second button needs a genuine release and a fresh press. The branch removed here was the only path that could add a capture mid-gesture for a BUTTON. What this does not change: overlapping elements still both fire when the finger lands on them together, since that press is deliberate; several fingers still hold several buttons independently; and the D-pad and radial-menu hand-off is untouched, because it is gated on the held element being a D_PAD or RADIAL_MENU and is how a directional swipe is meant to work.
The diamond had been widened to 20.1 mm between adjacent buttons on the grounds that a thumb contact patch is about 25 mm across, so independent presses needed 20 mm of separation. That reasoning does not apply to this engine: hit testing compares the touch CENTROID against bounding boxes, so contact width cannot cause a false press. The buttons were spread for no benefit and the cluster was reported as too far apart to use. The constraint that does bind is that bounding boxes are SQUARE. On a diamond, diagonally adjacent boxes only clear when the radius is at least twice the button half-size, so larger buttons force wider spacing - which is why the stock phone layout pairs 11.7 mm buttons with 17.9 mm spacing. Compressing the diamond vertically into a rhombus, 164 px wide by 110 px tall, keeps the boxes clear at full button size. Vertical is the right axis to give up: the thumb moves measurably slower in flexion and extension than in adduction and abduction, so shortening the up-and-down hops costs the least. The cluster centre was then placed to minimise the farthest button from the thumb rest measured off the digitiser at 33 mm inboard and 35 mm up. adjacent travel 20.1 -> 16.1 mm farthest from the rest 40.5 -> 27.8 mm average from the rest 28.3 -> 21.3 mm button size 13.3 mm, unchanged A now sits 11.2 mm from where the thumb rests. The D-pad mirrors the new centre, which brings it from 26.5 to 19.2 mm on the left. The layout check was wrong too. Its 4 mm edge-gap floor was invented rather than derived, and it is the wrong test for a single cluster of related buttons. Face button pairs are now checked on what actually matters: bounding boxes must not overlap, since co-press would otherwise fire two buttons from one touch, and centres must be at least 15 mm apart so a mis-hit needs more than a 2.5 sigma targeting error. The visible gap is 2.8 mm, which is tight to look at and safe by that test.
Changing the contents of a bundled .icp does nothing on its own. The sync gate is oldVersion == newVersion && oldSyncRevision >= REVISION && !deviceChanged and a debug rebuild changes none of those three, so copyAssetProfilesIfNeeded returns before the copy loop. The tightened face-button cluster was committed, built, signed and installed, and the layouts on disk still read 20.1 mm between adjacent buttons - the new assets never left the APK. The revision goes to 10. Verified on device: the pref advanced to 10 and all five bundled layouts carrying a face-button cluster now read 16.1 mm. Any future edit to a file under assets/inputcontrols/profiles* has to bump this constant in the same commit or it ships to nobody.
… rest The previous passes optimised every control to sit as close to the thumb as possible. That is not how a controller feels: on a real pad the thumb rests ON the stick and the face buttons are a deliberate reach up and out from it. The result had looked scattered rather than like a controller, and the sticks sat 13 mm below and 6 mm outboard of where the thumbs were measured to rest. Each half is now built as a DualSense half scaled to the panel: stick on the measured rest 3.6 mm from it, was 14.6 cluster centre from the stick 34.8 mm (DualSense ~39) far button from the stick 47.0 mm (DualSense ~55) face buttons 12.2 mm at 17.5 mm, a perfect diamond D-pad 26 mm, mirroring the cluster centre A DualSense puts the cluster ~22 mm outboard of the stick. With the thumb rest only 33 mm from the panel edge the cluster can move ~7 mm outboard before the far button leaves the screen, so it goes up instead and every other proportion is kept. The diamond is square again rather than the squashed rhombus of the last revision, at r = 152 px with 75 px buttons, which keeps the square hit boxes clear without shrinking the buttons below the thumb plateau. L3 and R3 move to the gap between stick and cluster, up-inboard of the stick. That is inside 25 mm of the measured rest and inside 60 mm from every candidate grip, where the previous stick-height slot failed the low grip at 62 mm. Every pair mirrors exactly: sticks, triggers, bumpers, L3/R3, Select/Start, and the D-pad against the cluster centre, at identical scale on both sides. Asset sync revision goes to 11 so the layouts install.
The triggers had been pulled 12 mm inboard, to x = 380 / 2020, on the theory that an index finger curling over the top-right corner could hit the tablet's Magic Key and drop into Game Space mid-game. In use they were reported as too far in and awkward to reach. An index finger wrapping the top edge lands nearest the corner it wraps, so that is where the buttons belong; the Magic Key concern was hypothetical and the person holding the device is the better judge. LT/LB and RT/RB move to x = 230 / 2170. The pair now spans 9 to 28 mm in from its side edge instead of 21 to 40. Vertical placement is unchanged: the upper button 6.5 to 16 mm from the top, the lower 21 to 30 mm. Every other element is where the last revision put it. Asset sync revision goes to 12 so the change installs.
Two more of the Device Profile's promised surfaces are wired up. Library artwork. The grid capped every card at 1.25 times its width - a portrait card - and drew the primary artwork into it with ContentScale.Crop. Steam's primary slot resolves to the 460x215 header, so a 2.14:1 landscape image was centre-cropped into a 0.8:1 card and lost most of its width; itch covers are 630x500 and Epic's wide banner and GOG's hero art are 16:9, all landscape too. The Astra 2 profile now asks for 3:2 cards (height 0.67 of width; the title strip takes the rest, leaving the image close to 16:9), three columns in landscape and two in portrait, and a wide-artwork preference that picks Steam's header explicitly, Epic's DieselStoreFrontWide banner ahead of the tall box, and GOG's hero art ahead of the square tile. The Default profile keeps the stock 1.25 portrait card, the stock column ladder and the stock slot order, so phones are untouched. The carousel and list paths keep their own slots. Session drawer buttons. ActionCardGrid sized each card by dividing the leftover drawer height by the row count, floored at 72 dp. On a phone that leftover is small; on the Astra 2 the drawer is 668 dp tall, so six cards in two rows became about 220 dp tall by 88 dp wide. The row height now also respects a per-profile maximum aspect: Astra 2 caps cards at 1.0 so they are square (88 x 88 dp on the 300 dp drawer), Default keeps Float.MAX_VALUE and fills as before. The computation is a pure function so the geometry is unit-tested at the Astra 2 numbers rather than eyeballed. The Device page now reports both surfaces as live instead of Coming Soon, with four new strings in the base and all 22 locales.
… Astra 2 The 0.67 card factor was a guess at "3:2 card minus a title strip" and it left the image box at roughly 1.8:1. Steam headers are 460x215, 2.14:1, so with ContentScale.Crop the sides went - "RAGON'S DOGM", "ONSTER HUNTER". The grid now derives the row height from the image itself: column width divided by the profile's image aspect, plus the 24 dp title strip (4 dp padding, one bodySmall line, 4 dp padding). The Astra 2 profile's aspect is the Steam header exactly, so headers fit edge to edge; Epic's wide banner and GOG's hero art lose a thin band top and bottom. The Default profile reports no aspect and keeps the stock 1.25 card factor. Landscape goes from three columns to four. On the 1067 dp panel that is about 245 dp per column and a 138 dp card, which fits three and a half rows in view. Portrait stays at two.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
No description provided.