diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-06-29 03:14:12 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-06-29 03:14:12 +0900 |
| commit | ae9ca7d56840d48a42119e67f8e91e7476f30ae3 (patch) | |
| tree | 4876625fd1a231719fcb639c01737e995202c1dd /static/js/web-install.js | |
| parent | ae2ce608935993583304a0e1e6a301bf69ccc75b (diff) | |
static: replace install
Also get rid of many unused pages and assets.
Diffstat (limited to 'static/js/web-install.js')
| -rw-r--r-- | static/js/web-install.js | 475 |
1 files changed, 0 insertions, 475 deletions
diff --git a/static/js/web-install.js b/static/js/web-install.js deleted file mode 100644 index 94b733d2..00000000 --- a/static/js/web-install.js +++ /dev/null @@ -1,475 +0,0 @@ -// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT - -import * as fastboot from "./fastboot/ffe7e270/fastboot.min.mjs"; - -const RELEASES_URL = "https://releases.grapheneos.org"; - -const CACHE_DB_NAME = "BlobStore"; -const CACHE_DB_VERSION = 1; - -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 -}; - -let wakeLock = null; - -const requestWakeLock = async () => { - try { - wakeLock = await navigator.wakeLock.request("screen"); - console.log("Wake lock has been set"); - wakeLock.addEventListener("release", async () => { - console.log("Wake lock has been released"); - }); - } catch (err) { - // if wake lock request fails - usually system related, such as battery - throw new Error(`${err.name}, ${err.message}`); - } -}; - -const releaseWakeLock = async () => { - if (wakeLock !== null) { - wakeLock.release().then(() => { - wakeLock = null; - }); - } -}; - -// reacquires the wake lock should the visibility of the document change and the wake lock is released -document.addEventListener("visibilitychange", async () => { - if (wakeLock !== null && document.visibilityState === "visible") { - await requestWakeLock(); - } -}); - -// This wraps XHR because getting progress updates with fetch() is overly complicated. -function fetchBlobWithProgress(url, onProgress) { - let xhr = new XMLHttpRequest(); - xhr.open("GET", url); - xhr.responseType = "blob"; - xhr.send(); - - return new Promise((resolve, reject) => { - xhr.onload = () => { - resolve(xhr.response); - }; - xhr.onprogress = (event) => { - onProgress(event.loaded / event.total); - }; - xhr.onerror = () => { - reject(`${xhr.status} ${xhr.statusText}`); - }; - }); -} - -function setButtonState({ id, enabled }) { - const button = document.getElementById(`${id}-button`); - button.disabled = !enabled; - return button; -} - -class BlobStore { - constructor() { - this.db = null; - } - - async _wrapReq(request, onUpgrade = null) { - return new Promise((resolve, reject) => { - request.onsuccess = () => { - resolve(request.result); - }; - request.oncomplete = () => { - resolve(request.result); - }; - request.onerror = (event) => { - reject(event); - }; - - if (onUpgrade !== null) { - request.onupgradeneeded = onUpgrade; - } - }); - } - - async init() { - if (this.db === null) { - this.db = await this._wrapReq( - indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION), - (event) => { - let db = event.target.result; - db.createObjectStore("files", { keyPath: "name" }); - /* no index needed for such a small database */ - } - ); - } - } - - async saveFile(name, blob) { - await this._wrapReq( - this.db.transaction(["files"], "readwrite").objectStore("files").add({ - name: name, - blob: blob, - }) - ); - } - - async loadFile(name) { - try { - let obj = await this._wrapReq( - this.db.transaction("files").objectStore("files").get(name) - ); - return obj.blob; - } catch { - return null; - } - } - - async close() { - this.db.close(); - } - - async download(url, onProgress = () => {}) { - let filename = url.split("/").pop(); - let blob = await this.loadFile(filename); - if (blob === null) { - console.log(`Downloading ${url}`); - let blob = await fetchBlobWithProgress(url, onProgress); - console.log("File downloaded, saving..."); - await this.saveFile(filename, blob); - console.log("File saved"); - } else { - console.log( - `Loaded ${filename} from blob store, skipping download` - ); - } - - return blob; - } -} - -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) { - setProgress("Connecting to device..."); - await device.connect(); - } -} - -async function unlockBootloader(setProgress) { - await ensureConnected(setProgress); - - // Trying to unlock when the bootloader is already unlocked results in a FAIL, - // so don't try to do it. - if (await device.getVariable("unlocked") === "yes") { - return "Bootloader is already unlocked."; - } - - setProgress("Unlocking bootloader..."); - try { - await device.runCommand("flashing unlock"); - } catch (error) { - // FAIL = user rejected unlock - if (error instanceof fastboot.FastbootError && error.status === "FAIL") { - throw new Error("Bootloader was not unlocked, please try again!"); - } else { - throw error; - } - } - - return "Bootloader unlocking triggered successfully."; -} - -const supportedDevices = ["tegu", "comet", "komodo", "caiman", "tokay", "akita", "husky", "shiba", "felix", "tangorpro", "lynx", "cheetah", "panther", "bluejay", "raven", "oriole", "barbet", "redfin", "bramble", "sunfish", "coral", "flame"]; - -const legacyQualcommDevices = ["sunfish", "coral", "flame"]; - -const day1SnapshotCancelDevices = ["tegu", "comet", "komodo", "caiman", "tokay", "akita", "husky", "shiba", "felix", "tangorpro", "lynx", "cheetah", "panther", "bluejay", "raven", "oriole", "barbet", "redfin", "bramble"]; - -function hasOptimizedFactoryImage(product) { - return !legacyQualcommDevices.includes(product); -} - -async function getLatestRelease() { - let product = await device.getVariable("product"); - if (!supportedDevices.includes(product)) { - throw new Error(`device model (${product}) is not supported by the GrapheneOS web installer`); - } - - let metadataResp = await fetch(`${RELEASES_URL}/${product}-stable`); - let metadata = await metadataResp.text(); - let releaseId = metadata.split(" ")[0]; - - return [`${product}-${hasOptimizedFactoryImage(product) ? "install" : "factory"}-${releaseId}.zip`, product]; -} - -async function downloadRelease(setProgress) { - await requestWakeLock(); - await ensureConnected(setProgress); - - setProgress("Finding latest release..."); - let [latestZip,] = await getLatestRelease(); - - // Download and cache the zip as a blob - setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: true }); - setProgress(`Downloading ${latestZip}...`); - await blobStore.init(); - try { - await blobStore.download(`${RELEASES_URL}/${latestZip}`, (progress) => { - setProgress(`Downloading ${latestZip}...`, progress); - }); - } finally { - setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: false }); - await releaseWakeLock(); - } - setProgress(`Downloaded ${latestZip} release.`, 1.0); -} - -async function reconnectCallback() { - let statusField = document.getElementById("flash-release-status"); - statusField.textContent = - "To continue flashing, reconnect the device by tapping here:"; - - let reconnectButton = document.getElementById("flash-reconnect-button"); - let progressBar = document.getElementById("flash-release-progress"); - - // Hide progress bar while waiting for reconnection - progressBar.hidden = true; - reconnectButton.hidden = false; - - reconnectButton.onclick = async () => { - await device.connect(); - reconnectButton.hidden = true; - progressBar.hidden = false; - }; -} - -async function flashRelease(setProgress) { - await requestWakeLock(); - await ensureConnected(setProgress); - - // Need to do this again because the user may not have clicked download if - // it was cached - setProgress("Finding latest release..."); - let [latestZip, product] = await getLatestRelease(); - await blobStore.init(); - let blob = await blobStore.loadFile(latestZip); - if (blob === null) { - throw new Error("You need to download a release first!"); - } - - setProgress("Cancelling any pending OTAs..."); - // Cancel snapshot update if in progress on devices which support it on all bootloader versions - if (day1SnapshotCancelDevices.includes(product)) { - let snapshotStatus = await device.getVariable("snapshot-update-status"); - if (snapshotStatus !== null && snapshotStatus !== "none") { - await device.runCommand("snapshot-update:cancel"); - } - } - - setProgress("Flashing release..."); - setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: true }); - try { - await device.flashFactoryZip(blob, true, reconnectCallback, - (action, item, progress) => { - let userAction = fastboot.USER_ACTION_MAP[action]; - let userItem = item === "avb_custom_key" ? "verified boot key" : item; - setProgress(`${userAction} ${userItem}...`, progress); - } - ); - if (legacyQualcommDevices.includes(product)) { - setProgress("Disabling UART..."); - // See https://android.googlesource.com/platform/system/core/+/eclair-release/fastboot/fastboot.c#532 - // for context as to why the trailing space is needed. - await device.runCommand("oem uart disable "); - setProgress("Erasing apdp..."); - // Both slots are wiped as even apdp on an inactive slot will modify /proc/cmdline - await device.runCommand("erase:apdp_a"); - await device.runCommand("erase:apdp_b"); - setProgress("Erasing msadp..."); - await device.runCommand("erase:msadp_a"); - await device.runCommand("erase:msadp_b"); - } - } finally { - setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: false }); - await releaseWakeLock(); - } - - return `Flashed ${latestZip} to device.`; -} - -async function eraseNonStockKey(setProgress) { - await ensureConnected(setProgress); - - setProgress("Erasing key..."); - try { - await device.runCommand("erase:avb_custom_key"); - } catch (error) { - console.log(error); - throw error; - } - return "Key erased."; -} - -async function lockBootloader(setProgress) { - await ensureConnected(setProgress); - - setProgress("Locking bootloader..."); - try { - await device.runCommand("flashing lock"); - } catch (error) { - // FAIL = user rejected lock - if (error instanceof fastboot.FastbootError && error.status === "FAIL") { - throw new Error("Bootloader was not locked, please try again!"); - } else { - throw error; - } - } - - return "Bootloader locking triggered successfully."; -} - -function addButtonHook(id, callback) { - let statusContainer = document.getElementById(`${id}-status-container`); - let statusField = document.getElementById(`${id}-status`); - let progressBar = document.getElementById(`${id}-progress`); - - let statusCallback = (status, progress) => { - if (statusContainer !== null) { - statusContainer.hidden = false; - } - - statusField.className = ""; - statusField.textContent = status; - - if (progress !== undefined) { - progressBar.hidden = false; - progressBar.value = progress; - } - }; - - let button = setButtonState({ id, enabled: true }); - button.onclick = async () => { - try { - let finalStatus = await callback(statusCallback); - if (finalStatus !== undefined) { - statusCallback(finalStatus); - } - } catch (error) { - statusCallback(`Error: ${error.message}`); - statusField.className = "error-text"; - await releaseWakeLock(); - // Rethrow the error so it shows up in the console - throw error; - } - }; -} - -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); - -fastboot.configureZip({ - workerScripts: { - inflate: ["/js/fastboot/ffe7e270/vendor/z-worker-pako.js", "pako_inflate.min.js"], - }, -}); - -if ("usb" in navigator) { - 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()) { - console.log("User tried to leave the page whilst unsafe to leave!"); - event.returnValue = ""; - } -}); - -// @license-end |
