
On Android, Chromium does not support automatic reconnection. The user must manually pair the device after every reboot in order for fastboot.js to reconnect to it. Unfortunately, this requires the user of the library to get involved because USB device connction requests are only allowed as the result of explicit user action, so we need to add a reconnect request callback for the user to handle.
255 lines
7.6 KiB
JavaScript
255 lines
7.6 KiB
JavaScript
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
|
|
|
import * as fastboot from "./fastboot/dist/fastboot.min.mjs?1";
|
|
|
|
const RELEASES_URL = "https://releases.grapheneos.org";
|
|
|
|
const CACHE_DB_NAME = "BlobStore";
|
|
const CACHE_DB_VERSION = 1;
|
|
|
|
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) {
|
|
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 (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async close() {
|
|
this.db.close();
|
|
}
|
|
|
|
/**
|
|
* Downloads the file from the given URL and saves it to this BlobStore.
|
|
*
|
|
* @param {string} url - URL of the file to download.
|
|
* @returns {blob} Blob containing the downloaded data.
|
|
*/
|
|
async download(url) {
|
|
let filename = url.split("/").pop();
|
|
let blob = await this.loadFile(filename);
|
|
if (blob === null) {
|
|
console.log(`Downloading ${url}`);
|
|
let resp = await fetch(new Request(url));
|
|
blob = await resp.blob();
|
|
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;
|
|
}
|
|
}
|
|
|
|
let device = new fastboot.FastbootDevice();
|
|
let blobStore = new BlobStore();
|
|
|
|
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 unlocked.";
|
|
}
|
|
|
|
async function getLatestRelease() {
|
|
let product = await device.getVariable("product");
|
|
|
|
let metadataResp = await fetch(`${RELEASES_URL}/${product}-stable`);
|
|
let metadata = await metadataResp.text();
|
|
let releaseId = metadata.split(" ")[0];
|
|
|
|
return `${product}-factory-${releaseId}.zip`;
|
|
}
|
|
|
|
async function downloadRelease(setProgress) {
|
|
await ensureConnected(setProgress);
|
|
|
|
setProgress("Finding latest release...");
|
|
let latestZip = await getLatestRelease();
|
|
|
|
// Download and cache the zip as a blob
|
|
setProgress(`Downloading ${latestZip}...`);
|
|
await blobStore.init();
|
|
await blobStore.download(`${RELEASES_URL}/${latestZip}`);
|
|
return `Downloaded ${latestZip} release.`;
|
|
}
|
|
|
|
async function reconnectCallback() {
|
|
let statusField = document.getElementById("flash-release-status");
|
|
statusField.textContent =
|
|
"In order to continue flashing, you need to reconnect the device by tapping here:";
|
|
|
|
let reconnectButton = document.getElementById("flash-reconnect-button");
|
|
reconnectButton.style.display = "inline-block";
|
|
reconnectButton.onclick = async () => {
|
|
await device.connect();
|
|
reconnectButton.style.display = "none";
|
|
};
|
|
}
|
|
|
|
async function flashRelease(setProgress) {
|
|
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 = 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("Flashing release...");
|
|
await fastboot.FactoryImages.flashZip(
|
|
device, blob, true, reconnectCallback,
|
|
(action, item) => {
|
|
let userAction = fastboot.FactoryImages.USER_ACTION_MAP[action];
|
|
let userItem = item === "avb_custom_key" ? "verified boot key" : item;
|
|
setProgress(`${userAction} ${userItem}...`);
|
|
}
|
|
);
|
|
|
|
return `Flashed ${latestZip} to device.`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// We can't explicitly validate the bootloader lock state because it reboots
|
|
// to recovery after locking. Assume that the device would've replied with
|
|
// FAIL if if it wasn't locked.
|
|
return "Bootloader locked.";
|
|
}
|
|
|
|
function addButtonHook(id, callback) {
|
|
let statusField = document.getElementById(`${id}-status`);
|
|
let statusCallback = (status) => {
|
|
statusField.textContent = status;
|
|
statusField.className = "";
|
|
};
|
|
|
|
let button = document.getElementById(`${id}-button`);
|
|
button.disabled = false;
|
|
button.onclick = async () => {
|
|
try {
|
|
let finalStatus = await callback(statusCallback);
|
|
statusCallback(finalStatus);
|
|
} catch (error) {
|
|
statusCallback(`Error: ${error.message}`);
|
|
statusField.className = "error-text";
|
|
// Rethrow the error so it shows up in the console
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
|
|
// 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.setDebugMode(true);
|
|
|
|
fastboot.FactoryImages.configureZip({
|
|
workerScripts: {
|
|
inflate: ["/js/fastboot/dist/vendor/z-worker-pako.js", "pako_inflate.min.js"],
|
|
},
|
|
});
|
|
|
|
if ("usb" in navigator) {
|
|
console.log("WebUSB available");
|
|
|
|
addButtonHook("unlock-bootloader", unlockBootloader);
|
|
addButtonHook("download-release", downloadRelease);
|
|
addButtonHook("flash-release", flashRelease);
|
|
addButtonHook("lock-bootloader", lockBootloader);
|
|
} else {
|
|
console.log("WebUSB unavailable");
|
|
}
|
|
|
|
// @license-end
|