From 4c89a3a0178abe789393ac90a76eaaa7321864dc Mon Sep 17 00:00:00 2001 From: flawedworld Date: Sun, 16 Jul 2023 23:46:13 +0000 Subject: [PATCH] [UNFINISHED] Fingerprint Calibration Tool --- static/js/udfps.js | 242 +++++++++++++++++++++++++++++++++++++++++++++ static/udfps.html | 231 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 static/js/udfps.js create mode 100644 static/udfps.html diff --git a/static/js/udfps.js b/static/js/udfps.js new file mode 100644 index 000000000..b95011a2f --- /dev/null +++ b/static/js/udfps.js @@ -0,0 +1,242 @@ +// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT + +import * as fastboot from "./fastboot/066d736d/fastboot.min.mjs"; + +const CACHE_DB_NAME = "BlobStore"; +const CACHE_DB_VERSION = 1; + +const Buttons = { + OBTAIN_CALIBRATION: "obtain-calibration-file", + FLASH_CALIBRATION: "flash-calibration-file", +}; + +// 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) { + 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(); + } + + 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 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(); + } +} + +const supportedDevices = ["lynx", "cheetah", "panther", "bluejay", "raven", "oriole"]; + +async function reconnectCallback() { + let statusField = document.getElementById("flash-calibration-file-status"); + statusField.textContent = + "To continue flashing, reconnect the device by tapping here:"; + + let reconnectButton = document.getElementById("flash-calibration-file-reconnect-button"); + + reconnectButton.hidden = false; + + reconnectButton.onclick = async () => { + await device.connect(); + reconnectButton.hidden = true; + }; +} + +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"; + // Rethrow the error so it shows up in the console + throw error; + } + }; +} + +async function obtainCalibration() { + await device.runCommand("oem get-cal-url "); + // save output from fastboot command + // fetchBlobWithProgress + console.log("unimplemented"); +} + +async function flashCalibration() { + // fastboot stage + await device.runCommand("oem update-cal "); + await device.reboot("bootloader", 4000, reconnectCallback); + console.log("unimplemented"); +} + +let safeToLeave = true; + +// 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); + +if ("usb" in navigator) { + addButtonHook(Buttons.OBTAIN_CALIBRATION, obtainCalibration); + addButtonHook(Buttons.FLASH_CALIBRATION, flashCalibration); +} 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 diff --git a/static/udfps.html b/static/udfps.html new file mode 100644 index 000000000..2715ce925 --- /dev/null +++ b/static/udfps.html @@ -0,0 +1,231 @@ + + + + + Fingerprint Calibration Tool | GrapheneOS + + + + + + + + + + + + + + + + + + + + + + [[css|/main.css]] + + + + [[js|/js/redirect.js]] + + [[js|/js/udfps.js]] + + + {% include "header.html" %} +
+

Fingerprint calibration tool

+ +

This is a reimplementation of Google's Pixel Fingerprint Calibration Tool, + which is needed when the screen is replaced on a Pixel device which uses a + fingerprint sensor under the device screen.

+ +

If you have trouble with the calibration process, ask for help on the + official GrapheneOS chat channel. There are almost + always people around willing to help with it. Before asking for help, make an attempt + to follow the guide on your own and then ask for help with anything you get stuck + on.

+ + + +
+

Prerequisites

+ +

This tool is only compatible with the following devices:

+ +
    +
  • Pixel 6
  • +
  • Pixel 6 Pro
  • +
  • Pixel 6a
  • +
  • Pixel 7
  • +
  • Pixel 7 Pro
  • +
  • Pixel 7a
  • +
+ +

You need a USB cable for attaching the device to a laptop or desktop. Whenever + possible, use the high quality standards compliant USB-C cable packaged with the + device. If your computer doesn't have any USB-C ports, you'll need a high quality + USB-C to USB-A cable. You should avoid using a USB hub such as the front panel on + a desktop computer case. Connect directly to a rear port on a desktop or the ports + on a laptop. Many widely distributed USB cables and hubs are broken and are the + most common source of issues for installing GrapheneOS.

+ +

Running this tool from an OS in a virtual machine is not recommended. USB + passthrough is often not reliable. To rule out these problems, install from an OS + running on bare metal. Virtual machines are also often configured to have overly + limited memory and storage space.

+ +

Officially supported operating systems for this tool:

+ +
    +
  • Windows 10
  • +
  • Windows 11
  • +
  • macOS Big Sur (11)
  • +
  • macOS Monterey (12)
  • +
  • macOS Ventura (13)
  • +
  • Arch Linux
  • +
  • Debian 10 (buster)
  • +
  • Debian 11 (bullseye)
  • +
  • Debian 12 (bookworm)
  • +
  • Ubuntu 20.04 LTS
  • +
  • Ubuntu 22.04 LTS
  • +
  • Ubuntu 22.10
  • +
  • Ubuntu 23.04
  • +
  • ChromeOS
  • +
  • GrapheneOS
  • +
  • Google Android (stock Pixel OS) and other certified Android variants
  • +
+ +

Make sure your operating system is up-to-date before proceeding.

+ +

Officially supported browsers for this tool:

+ +
    +
  • Chromium (outside Ubuntu, since they ship a broken Snap package without working WebUSB)
  • +
  • Vanadium (GrapheneOS)
  • +
  • Google Chrome
  • +
  • Microsoft Edge
  • +
  • Brave
  • +
+ +

You should avoid Flatpak and Snap versions of browsers, as they're known to cause issues during the usage of this tool.

+ +

Make sure your browser is up-to-date before proceeding.

+ +

It's best practice to update the device before using this tool to have + the latest firmware for connecting the phone to the computer and performing the + calibration. Older firmware versions may no longer have files hosted on Google + servers.

+ +

This tool connects to Google servers as part of it's operation, in order + to obtain the calibration file.

+
+ +
+

Calibrating as non-root

+ +

On traditional Linux distributions, USB devices cannot be used as non-root + without udev rules for each type of device. This is not an issue for other + platforms.

+ +

On Arch Linux, install the android-udev package. On Debian and + Ubuntu, install the android-sdk-platform-tools-common package.

+
+ +
+

Booting into the bootloader interface

+ +

You need to boot your phone into the bootloader interface. To do this, you need + to hold the volume down button while the phone boots.

+ +

The easiest approach is to reboot the phone and begin holding the volume down + button until it boots up into the bootloader interface.

+ +

Alternatively, turn off the phone, then boot it up while holding the volume + down button during the boot process. You can either boot it with the power button + or by plugging it in as required in the next section.

+
+ +
+

Connecting the phone

+ +

Connect the phone to the computer. On Linux, you'll need to do this again if + you didn't have the udev rules set up when you connected it.

+ +

On Linux, GNOME has a bug causing compatibility issues with the installation + process. It wrongly detects the phone in fastboot mode or fastbootd mode as being + an MTP device and claims exclusive control over it. This will block the install + process from proceeding. You can run the following command to work around it:

+ +
echo 0 | sudo tee /sys/bus/usb/drivers_autoprobe
+ +

After installing, you can undo this by rebooting or by running the following + command:

+ +
echo 1 | sudo tee /sys/bus/usb/drivers_autoprobe
+ +

On Windows, you need to install a driver for fastboot if you don't already have + it. No driver is needed on other operating systems. You can obtain the driver from + Windows Update which will detect it as an optional update when the device is + booted into the bootloader interface and connected to the computer. Open Windows + Update, run a check for updates and then open the "View optional updates" + interface. Install the driver for the Android bootloader interface as an optional + update.

+ +

An alternative approach to obtaining the Windows fastboot driver is to obtain + the latest driver for + Pixels from Google and then + manually + install it with the Windows Device Manager.

+
+ +
+

Obtain calibration file

+ +

In order to calibrate the fingerprint sensor, a special calibration file + specific to your model of device must be downloaded from Google's servers. + The URL for this file is hardcoded in the device firmware, and is this URL is + extracted from the device, and then downloaded by this step of the tool. +

+ + + +

+ +
+ +

+
+ +
+

Flash calibration file

+ +

This stage of the tool loads the previously downloaded calibration file + onto the device, and then reboots your device in order for it to take effect. + Once done, you can safely unplug the device, and use the power button to press + "Start" to boot into GrapheneOS.

+ + + +

+ + +

+
+ +
+ {% include "footer.html" %} + +