summaryrefslogtreecommitdiff
path: root/static/js/web-install.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/js/web-install.js')
-rw-r--r--static/js/web-install.js475
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