Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ A brand can carry its own `bootloader` block, for the policy that applies across
- key: google
name: Google
bootloader:
notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked.
unlock: conditional
notes: Pixel and Nexus devices unlock with fastboot, unless a carrier has SIM locked them.
source: https://source.android.com/docs/setup/build/running
```

It shows on the brand page and under Bootloader on every device page, and it never changes what a
device's own `unlock` says. Both fields are needed together, and only add one where the policy
It shows on the brand page and under Bootloader on every device page. A device whose own `unlock`
is `unknown` takes the brand's, marked as brand policy, and anything the device file says wins.
`notes` and `source` are needed together, `unlock` is optional, and only add one where the policy
really does hold brand-wide - most brands split it by chipset, region or Android version, so they
stay unknown.

Expand Down
4 changes: 3 additions & 1 deletion data/brands.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- key: fairphone
name: Fairphone
bootloader:
unlock: "yes"
notes: Fairphone publishes an unlock procedure for its phones, the Fairphone 2 aside.
source: https://support.fairphone.com/hc/en-us/articles/10492476238865-How-to-unlock-or-lock-your-Fairphone-s-bootloader
- key: fxtec
Expand All @@ -25,7 +26,8 @@
- key: google
name: Google
bootloader:
notes: Pixel and Nexus phones from 2015 on unlock with fastboot, unless they were sold carrier locked.
unlock: conditional
notes: Pixel and Nexus devices unlock with fastboot, unless a carrier has SIM locked them.
source: https://source.android.com/docs/setup/build/running
- key: htc
name: HTC
Expand Down
4 changes: 3 additions & 1 deletion data/devices/fairphone/fp2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ released: 2015
soc: Qualcomm MSM8974AB-AB Snapdragon 801
hardware: [cellular]
bootloader:
unlock: unknown
unlock: "yes"
notes: LineageOS installs straight over fastboot, with no unlock step.
source: https://wiki.lineageos.org/devices/FP2/install/
1 change: 1 addition & 0 deletions schema/brands.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"required": ["notes", "source"],
"additionalProperties": false,
"properties": {
"unlock": { "enum": ["yes", "no", "conditional"] },
"notes": { "type": "string", "minLength": 1 },
"source": { "$ref": "#/$defs/link" }
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/data/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export function buildModel(data = getData(), now = new Date()) {
const brand = brands.get(brandKey);
const prefixed = device.name.toLowerCase().startsWith(brand.name.toLowerCase());
const slug = device.slug ?? slugify(device.name);
const policy = device.bootloader.unlock === 'unknown' && brand.bootloader?.unlock;
const entry = {
...device,
bootloader: policy
? { ...device.bootloader, unlock: policy, origin: 'brand' }
: device.bootloader,
key,
file,
brand,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const needsCell = ({ firmware, channel }) => {
return chips.length ? { chips } : {};
};

const suffixes = { upstream: 'docs', rom: 'ROM' };
const suffixes = { upstream: 'docs', rom: 'ROM', brand: 'brand' };

export const originSuffix = ({ origin, inferred }) =>
[suffixes[origin], inferred && 'inferred'].filter(Boolean).join(', ') || undefined;
Expand Down Expand Up @@ -181,6 +181,7 @@ export const deviceRows = (devices, roms) =>
unlock: {
tone: lockTone[device.bootloader.unlock],
text: labelOf(device.bootloader.unlock),
suffix: originSuffix(device.bootloader),
},
...Object.fromEntries(roms.map((rom) => [rom.key, romStatus(device, rom)])),
},
Expand Down
3 changes: 2 additions & 1 deletion src/pages/devices/[brand]/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const { device } = Astro.props;
const { features } = getModel();
const { bootloader, stock, support } = device;
const policy = device.brand.bootloader;
const unlock = `${labelOf(bootloader.unlock)}${bootloader.origin ? ', brand policy' : ''}`;
const [current, ended] = splitByActive(support);
const unreported = featuresFrom(features, support, 'none');
const quiet = new Set(
Expand Down Expand Up @@ -138,7 +139,7 @@ const codename = device.codenames[0];

<section aria-labelledby="bootloader">
<h2 id="bootloader">Bootloader</h2>
<Facts items={[{ label: 'Unlock', value: labelOf(bootloader.unlock) }]} />
<Facts items={[{ label: 'Unlock', value: unlock }]} />
{bootloader.notes && <p>{bootloader.notes}</p>}
{bootloader.source && (
<p>Going by <a href={bootloader.source}>{new URL(bootloader.source).hostname}</a>.</p>
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/good/brands.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- key: acme
name: Acme
bootloader:
unlock: "yes"
notes: Acme phones unlock with fastboot.
source: https://acme.test/unlock
16 changes: 15 additions & 1 deletion tests/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { resolveCell } from '../src/lib/data/status.js';
const model = (now) => buildModel(loadData('tests/fixtures/good'), new Date(now));
const rowOf = (now, rom) =>
model(now).support.find((row) => row.device.key === 'rocket' && row.rom.key === rom);
const unlockOf = (deviceUnlock, brandUnlock) => {
const data = loadData('tests/fixtures/good');
data.devices[0].data.bootloader = { unlock: deviceUnlock };
data.brands.data[0].bootloader.unlock = brandUnlock;
return buildModel(data).devices[0].bootloader;
};
const cellsOf = (row) =>
Object.fromEntries(
Object.entries(row.cells).map(([key, cell]) => [key, [cell.value, cell.origin]]),
Expand All @@ -21,7 +27,7 @@ test('cells resolve report, then docs, then hardware, then ROM-wide', () => {
});
});

test('a brand policy reaches the device without touching its own unlock', () => {
test("a device's own unlock beats the brand policy", () => {
const [device] = model('2026-02-01').devices;
assert.equal(device.brand.bootloader.source, 'https://acme.test/unlock');
assert.deepEqual(device.bootloader, {
Expand All @@ -30,6 +36,14 @@ test('a brand policy reaches the device without touching its own unlock', () =>
});
});

test('an unknown unlock takes the brand policy, marked as such', () => {
assert.deepEqual(unlockOf('unknown', 'yes'), { unlock: 'yes', origin: 'brand' });
});

test('a brand without an unlock leaves the device unknown', () => {
assert.deepEqual(unlockOf('unknown', undefined), { unlock: 'unknown' });
});

test('explicit values beat inferred ones, whatever layer they sit in', () => {
const feature = { key: 'wallet' };
const cell = resolveCell(feature, {
Expand Down
Loading