From 0f371818769cbe42e629d667058fab7dc93c59b5 Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 May 2023 00:06:06 +0000 Subject: [PATCH] bugfix(web installer): disable buttons depending on the state of the installer Currently clicking some web installer buttons, like 'Lock bootloader', can interrupt flashing and some, like 'Flash release', can cause multiple instances of the "flashing" process to initiate. This commit adds support for enabling/disabling web installer buttons and disables buttons that might interrupt flashing while flashing. Clicking the 'Download release' button while a download is in progress can cause multiple downloads to initiate, so disable the button during downloads and enable it afterwards. --- static/js/web-install.js | 112 ++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 13 deletions(-) diff --git a/static/js/web-install.js b/static/js/web-install.js index 7316e443..049800a6 100644 --- a/static/js/web-install.js +++ b/static/js/web-install.js @@ -7,7 +7,18 @@ const RELEASES_URL = "https://releases.grapheneos.org"; const CACHE_DB_NAME = "BlobStore"; const CACHE_DB_VERSION = 1; -let safeToLeave = true; +const Buttons = { + UNLOCK_BOOTLOADER: "unlock-bootloader", + DOWNLOAD_RELEASE: "download-release", + FLASH_RELEASE: "flash-release", + LOCK_BOOTLOADER: "lock-bootloader", + REMOVE_CUSTOM_KEY: "remove-custom-key" +}; + +const InstallerState = { + DOWNLOADING_RELEASE: 0x1, + INSTALLING_RELEASE: 0x2 +}; // This wraps XHR because getting progress updates with fetch() is overly complicated. function fetchBlobWithProgress(url, onProgress) { @@ -29,6 +40,12 @@ function fetchBlobWithProgress(url, onProgress) { }); } +function setButtonState({ id, enabled }) { + const button = document.getElementById(`${id}-button`); + button.disabled = !enabled; + return button; +} + class BlobStore { constructor() { this.db = null; @@ -106,8 +123,39 @@ class BlobStore { } } +class ButtonController { + #map; + + constructor() { + this.#map = new Map(); + } + + setEnabled(...ids) { + ids.forEach((id) => { + // Only enable button if it won't be disabled. + if (!this.#map.has(id)) { + this.#map.set(id, /* enabled = */ true); + } + }); + } + + setDisabled(...ids) { + ids.forEach((id) => this.#map.set(id, /* enabled = */ false)); + } + + applyState() { + this.#map.forEach((enabled, id) => { + setButtonState({ id, enabled }); + }); + this.#map.clear(); + } +} + +let installerState = 0; + let device = new fastboot.FastbootDevice(); let blobStore = new BlobStore(); +let buttonController = new ButtonController(); async function ensureConnected(setProgress) { if (!device.isConnected) { @@ -170,7 +218,7 @@ async function downloadRelease(setProgress) { let [latestZip,] = await getLatestRelease(); // Download and cache the zip as a blob - safeToLeave = false; + setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: true }); setProgress(`Downloading ${latestZip}...`); await blobStore.init(); try { @@ -178,7 +226,7 @@ async function downloadRelease(setProgress) { setProgress(`Downloading ${latestZip}...`, progress); }); } finally { - safeToLeave = true; + setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: false }); } setProgress(`Downloaded ${latestZip} release.`, 1.0); } @@ -225,7 +273,7 @@ async function flashRelease(setProgress) { } setProgress("Flashing release..."); - safeToLeave = false; + setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: true }); try { await device.flashFactoryZip(blob, true, reconnectCallback, (action, item, progress) => { @@ -257,7 +305,7 @@ async function flashRelease(setProgress) { await device.runCommand("erase:dpm_b"); } } finally { - safeToLeave = true; + setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: false }); } return `Flashed ${latestZip} to device.`; @@ -316,8 +364,7 @@ function addButtonHook(id, callback) { } }; - let button = document.getElementById(`${id}-button`); - button.disabled = false; + let button = setButtonState({ id, enabled: true }); button.onclick = async () => { try { let finalStatus = await callback(statusCallback); @@ -333,6 +380,45 @@ function addButtonHook(id, callback) { }; } +function setInstallerState({ state, active }) { + if (active) { + installerState |= state; + } else { + installerState &= ~state; + } + invalidateInstallerState(); +} + +function isInstallerStateActive(state) { + return (installerState & state) === state; +} + +function invalidateInstallerState() { + if (isInstallerStateActive(InstallerState.DOWNLOADING_RELEASE)) { + buttonController.setDisabled(Buttons.DOWNLOAD_RELEASE); + } else { + buttonController.setEnabled(Buttons.DOWNLOAD_RELEASE); + } + + let disableWhileInstalling = [ + Buttons.DOWNLOAD_RELEASE, + Buttons.FLASH_RELEASE, + Buttons.LOCK_BOOTLOADER, + Buttons.REMOVE_CUSTOM_KEY, + ]; + if (isInstallerStateActive(InstallerState.INSTALLING_RELEASE)) { + buttonController.setDisabled(...disableWhileInstalling); + } else { + buttonController.setEnabled(...disableWhileInstalling); + } + + buttonController.applyState(); +} + +function safeToLeave() { + return installerState === 0; +} + // This doesn't really hurt, and because this page is exclusively for web install, // we can tolerate extra logging in the console in case something goes wrong. fastboot.setDebugLevel(2); @@ -344,18 +430,18 @@ fastboot.configureZip({ }); if ("usb" in navigator) { - addButtonHook("unlock-bootloader", unlockBootloader); - addButtonHook("download-release", downloadRelease); - addButtonHook("flash-release", flashRelease); - addButtonHook("lock-bootloader", lockBootloader); - addButtonHook("remove-custom-key", eraseNonStockKey); + addButtonHook(Buttons.UNLOCK_BOOTLOADER, unlockBootloader); + addButtonHook(Buttons.DOWNLOAD_RELEASE, downloadRelease); + addButtonHook(Buttons.FLASH_RELEASE, flashRelease); + addButtonHook(Buttons.LOCK_BOOTLOADER, lockBootloader); + addButtonHook(Buttons.REMOVE_CUSTOM_KEY, eraseNonStockKey); } else { console.log("WebUSB unavailable"); } // This will create an alert box to stop the user from leaving the page during actions window.addEventListener("beforeunload", event => { - if (!safeToLeave) { + if (!safeToLeave()) { console.log("User tried to leave the page whilst unsafe to leave!"); event.returnValue = ""; }