/* ── the phone, turned over: the two settings that do it ─────────────── * `settings get system accelerometer_rotation` and `user_rotation` are the * only two things on the device this walk ever writes, and both are READ * before either is written. The saved pair is latched at module scope on * purpose: the step loop's `still()` and its PROBE call are wrapped in * nothing at all, so a WebView that stops answering rejects after cdp.mjs's * timeout and throws clean out of the walk into main's `finally`. That is * the exact path that would otherwise leave Kyle's phone locked sideways, * which is why the restore is a latch reachable from four places — the * rotation block's own finally, main's finally, process 'exit' and a * signal — rather than a step in a list. */ let rotationSaved = null; /** Both rotation settings as the device has them now, or null when either * will not read as something we recognise. null is the SKIP signal: the * block never writes what it could not read first. `user_rotation` comes * back as the literal "null" on a phone where it has never been set. */ function rotationSettings() { try { const accel = adb("shell", "settings", "get", "system", "accelerometer_rotation").toString().trim(); const user = adb("shell", "settings", "get", "system", "user_rotation").toString().trim(); if (!/^\d+$/.test(accel)) return null; if (!/^\d+$/.test(user) && user !== "null") return null; return { accel, user }; } catch { return null; } } /** Read once and latch. Returns the pair, or null when the device would not * say — in which case nothing has been written and there is nothing to put * back. */ function rotationArm() { const was = rotationSettings(); if (was) rotationSaved = was; return was; } /** Drop the latch WITHOUT writing anything. For the skip paths: the pair has * been read but nothing has been put, and restoring there would write two * settings onto Kyle's phone that this walk never changed. */ function rotationDisarm() { rotationSaved = null; } /** Hold the phone at one rotation. Auto-rotate goes off FIRST: with the * sensor still in charge it takes `user_rotation` straight back and the * turn never happens. Returns null, or the message it failed with. */ function rotationPut(n) { try { adb("shell", "settings", "put", "system", "accelerometer_rotation", "0"); adb("shell", "settings", "put", "system", "user_rotation", String(n)); return null; } catch (e) { return e.message; } } /** Put both back, once. The latch is cleared BEFORE anything is written, so * the first caller is the only one that acts and the three behind it are * no-ops; a restore that fails puts the latch back so the next caller can * try again. The rotation goes first and auto-rotate last — a phone whose * auto-rotate was on must hand control back to the sensor only once the * angle underneath it is the one it started at. Synchronous, because * `adb()` is spawnSync and nothing asynchronous survives an 'exit' * handler. Returns true, null when there was nothing to restore, or the * message it failed with. */ function rotationRestore() { const was = rotationSaved; rotationSaved = null; if (!was) return null; try { if (was.user === "null") { /* never set on this phone: delete it rather than inventing a 0 */ try { adb("shell", "settings", "delete", "system", "user_rotation"); } catch { adb("shell", "settings", "put", "system", "user_rotation", "0"); } } else { adb("shell", "settings", "put", "system", "user_rotation", was.user); } adb("shell", "settings", "put", "system", "accelerometer_rotation", was.accel); return true; } catch (e) { rotationSaved = was; return e.message; } } /** The system's own view of the turn, for the report only. The two settings * read back, plus the display's current size and rotation out of `dumpsys * window displays` — whose spelling moves between Android versions, so a * parse failure comes back empty and is printed, never asserted. */ function rotationSystem() { const out = { setAccel: "", setUser: "", dispCur: "", dispRot: "" }; try { out.setAccel = adb("shell", "settings", "get", "system", "accelerometer_rotation").toString().trim(); out.setUser = adb("shell", "settings", "get", "system", "user_rotation").toString().trim(); } catch { /* empty is the reading */ } try { const d = adb("shell", "dumpsys", "window", "displays").toString(); out.dispCur = (d.match(/\bcur=(\d+x\d+)/) || [])[1] || ""; out.dispRot = (d.match(/mCurrentRotation=(?:ROTATION_)?(\w+)/) || d.match(/\brotation=(\d+)/) || [])[1] || ""; } catch { /* empty is the reading */ } return out; } /* 'exit' does not run on a signal and a signal does not run 'exit', so both. Ctrl-C in the middle of the turn is precisely the case that would otherwise leave the phone sideways until Kyle found the Quick Settings tile. Registered only for the phone walk — the mock never writes a setting, and a SIGINT handler on the mock run would change how Ctrl-C behaves there for nothing. */ if (PHONE) { process.on("exit", () => { rotationRestore(); }); for (const sig of ["SIGINT", "SIGTERM", "SIGBREAK"]) { process.on(sig, () => { rotationRestore(); process.exit(130); }); } }