6.7.0 - Alpha6 - 新增 Android 发行版本 scraper 脚本及信息打印脚本

This commit is contained in:
SuperMonster003
2025-10-09 17:13:48 +08:00
parent 0ff16b8ec0
commit b83ce13d0e
9 changed files with 702 additions and 4 deletions

2
.gitignore vendored
View File

@@ -38,4 +38,4 @@ sync.ffs_db
/*dependecies_tree.txt
**/assets-app/declarations/
**/assets-app/sample/declarations/
**/assets-app/sample/declarations/

View File

@@ -1,3 +1,6 @@
type Cheerio<T> = import('cheerio').Cheerio<T>;
type DomElement = import('domhandler').Element;
type Page = import('puppeteer').Page;
type Frame = import('puppeteer').Frame;
type ElementHandle<T extends Node> = import('puppeteer').ElementHandle<T>;
@@ -13,6 +16,18 @@ type TableDataStructureItemName = string;
type TableDataStructureItem = RegExp | ((s: string) => boolean | string);
type TableDataStructureItemForPageEvaluate = string | RegExp | ((s: string) => boolean | string);
interface AndroidReleaseWiki {
RELEASE_NAME: string;
INTERNAL_CODENAME: string;
PLATFORM_VERSION: string;
API_LEVEL: number;
RELEASE_DATE: string;
}
interface AndroidReleaseCsv extends AndroidReleaseWiki {
VERSION_CODE: string;
}
interface AndroidStudioRelease {
content: {
item: AndroidStudioReleaseItem[];

View File

@@ -0,0 +1,230 @@
// fetch-and-parse-android-releases.mjs
import * as cheerio from 'cheerio';
import fetch from 'node-fetch';
import { fileURLToPath } from 'node:url';
const versionCodeBlacklist = [ 'CUR_DEVELOPMENT' ];
const WIKI_URL = 'https://en.wikipedia.org/wiki/Android_version_history';
const ANDROID_VC_URL = 'https://developer.android.com/reference/android/os/Build.VERSION_CODES';
const norm = (/** @type {string} */ s) => (s ?? '').trim().replace(/\s+/g, ' ');
const trimAndNormDash = (/** @type {string} */ s) => s.replace(/\s*[\u002d\u2013\u2014]\s*/g, '-');
const stripSupportPrefix = (/** @type {string} */ s) => s.replace(/^(un)?supported:\s*/ig, '');
const handleCsvEmptyString = (/** @type {string} */ s) => /^-?$/.test(s) ? '""' : s;
/**
* @param {AndroidReleaseCsv} o
* @returns {AndroidReleaseCsv}
*/
function quoteIfNeeded(o) {
Object.keys(o).forEach((k) => {
if (typeof o[k] === 'string') {
o[k] = handleCsvEmptyString(o[k]);
}
});
return o;
}
/**
* Remove style, script, and sup.reference elements before getting text.<br>
* zh-CN: 获取文本前, 先移除 style, script 及 sup.reference.
*
* @param {Cheerio<DomElement>} cell
* @returns {string}
*/
function cleanCell(cell) {
const cloned = cell.clone();
cloned.find('style,script,sup.reference').remove();
return cloned.text().trim().replace(/\s+/g, ' ');
}
/**
* Fetch official VERSION_CODE and API_LEVEL from Android documentation.<br>
* zh-CN: 抓取官方 VERSION_CODE 与 API_LEVEL.
*
* @example Map<apiLevel, versionCode>
* @returns {Promise<Map<number, string>>}
*/
async function fetchOfficialVC() {
const html = await (await fetch(ANDROID_VC_URL)).text();
const $ = cheerio.load(html);
/**
* @example Map<apiLevel, versionCode>
* @type {Map<number, string>}
*/
const map = new Map();
$('h3.api-name').each((_i, h3) => {
const $h3 = $(h3);
const versionCode = norm($h3.attr('id') || $h3.text());
if (!versionCode || versionCodeBlacklist.includes(versionCode.toUpperCase())) return;
const container = $h3.parent();
const apiLevelText = container.find('.api-level').first().text();
const apiLevel = (/* @IIFE */ () => {
// "Constant Value: X"
const constValMatched = container.text().match(/Constant\s+Value:\s*(\d+)/i);
if (constValMatched) return parseInt(constValMatched[1], 10);
// "Added in API level X"
const addedInMatched = apiLevelText.match(/API level\s+(\d+)/i);
if (addedInMatched) return parseInt(addedInMatched[1], 10);
})();
if (apiLevel && Number.isFinite(apiLevel)) {
if (!map.has(apiLevel)) {
map.set(apiLevel, versionCode);
}
}
});
return map;
}
/**
* Fetch all fields except VERSION_CODE from Wikipedia.<br>
* zh-CN: 抓取维基除 VERSION_CODE 外的字段.
*
* @returns {Promise<AndroidReleaseWiki[]>}
*/
async function fetchWikiRows() {
const $ = cheerio.load(await (await fetch(WIKI_URL)).text());
const re = /^(Name|Internal\s*codename|Version\s*number(\(s\)|s)?|API\s*level|Release\s*date)$/i;
const table = $('table.wikitable').filter((_, table) => {
return Array.from($(table).find('tr th')).filter(th => {
const extractedText = $(th).text().replace(/\[\d+]/g, '').trim();
return re.test(extractedText);
}).length >= 3;
}).first();
/** @type {AndroidReleaseWiki[]} */
const rows = [];
/**
* Maintain column state.<br>
* zh-CN: 维护列状态.
*
* @example Record<columnIndex, { text: string; rowSpansLeft: number }>
* { 2: { text: "Gingerbread"; rowSpansLeft: 3 } }
*
* @type {Record<number, { cell: Cheerio<DomElement>; rowSpansLeft: number }>}
*/
const rowSpansState = {};
table.find('tr').each((_i, tr) => {
const $tr = $(tr);
const tds = $tr.find('td');
if (!tds.length) return;
/** @type {Cheerio<DomElement>[]} */
const cells = [];
let chosenTdsIdx = 0;
for (let i = 0; i < 5; i += 1) {
let cell = null;
if (i in rowSpansState) {
cell = rowSpansState[i].cell;
cells.push(cell);
rowSpansState[i].rowSpansLeft -= 1;
} else {
cell = tds.eq(chosenTdsIdx++);
if (!cell || cell.length === 0) return;
cells.push(cell);
const rowSpansLeft = parseInt(cell.attr('rowspan') || '1', 10) - 1;
if (rowSpansLeft > 0) {
rowSpansState[i] = { cell, rowSpansLeft };
}
}
}
const [ nameCell, codenameCell, versionCell, apiCell, dateCell ] = cells;
const name = cleanCell(nameCell);
const codename = trimAndNormDash(cleanCell(codenameCell));
// Version prefers data-sort-value (can be on current td or its child elements).
// zh-CN: Version 优先使用 data-sort-value (可在当前 td 或其子元素上).
const sortVal = versionCell.attr('data-sort-value') || versionCell.find('[data-sort-value]').attr('data-sort-value');
const versionText = trimAndNormDash(stripSupportPrefix(sortVal || cleanCell(versionCell)));
const api = cleanCell(apiCell);
const date = cleanCell(dateCell);
rows.push({
RELEASE_NAME: name,
INTERNAL_CODENAME: codename,
PLATFORM_VERSION: versionText,
API_LEVEL: parseInt(api, 10),
RELEASE_DATE: date,
});
for (const k of Object.keys(rowSpansState)) {
if (rowSpansState[k].rowSpansLeft === 0) {
delete rowSpansState[k];
}
}
});
return rows;
}
/**
* @returns {Promise<AndroidReleaseCsv[]>}
*/
export async function fetchAndroidReleases() {
const official = await fetchOfficialVC();
const wiki = await fetchWikiRows();
/** @type {Map<number, AndroidReleaseCsv>} */
const mergedByApi = new Map();
// First, populate with wiki data.
// zh-CN: 先放入 wiki 数据.
for (const r of wiki) {
const api = r.API_LEVEL;
if (!Number.isFinite(api)) continue;
const prev = mergedByApi.get(api) || {
VERSION_CODE: '', RELEASE_NAME: '', INTERNAL_CODENAME: '',
PLATFORM_VERSION: '', API_LEVEL: api, RELEASE_DATE: '',
};
mergedByApi.set(api, quoteIfNeeded({
...prev,
RELEASE_NAME: r.RELEASE_NAME || prev.RELEASE_NAME,
INTERNAL_CODENAME: r.INTERNAL_CODENAME || prev.INTERNAL_CODENAME,
PLATFORM_VERSION: r.PLATFORM_VERSION || prev.PLATFORM_VERSION,
RELEASE_DATE: r.RELEASE_DATE || prev.RELEASE_DATE,
}));
}
// Override VERSION_CODE with official data.
// zh-CN: 用 official 覆盖 VERSION_CODE.
for (const [ api, versionCode ] of official.entries()) {
const prev = mergedByApi.get(api) || {
VERSION_CODE: '', RELEASE_NAME: '', INTERNAL_CODENAME: '',
PLATFORM_VERSION: '', API_LEVEL: api, RELEASE_DATE: '',
};
mergedByApi.set(api, { ...prev, VERSION_CODE: versionCode, API_LEVEL: api });
}
// Output: Sort by API_LEVEL in descending order,
// keep only records with VERSION_CODE (officially released/named).
// zh-CN: 输出: 按 API_LEVEL 降序, 仅保留存在 VERSION_CODE 的记录 (即官方已发布/命名的).
return Array.from(mergedByApi.values())
.filter(r => r.VERSION_CODE)
.sort((a, b) => b.API_LEVEL - a.API_LEVEL);
}
async function main() {
console.table(await fetchAndroidReleases());
}
// Determine if this file is being run directly.
// zh-CN: 判断是否为直接执行该文件.
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch(err => {
console.error(err);
process.exitCode = 1;
});
}

View File

@@ -0,0 +1,32 @@
@ECHO OFF
:RUN
node "print-android-version-codes.mjs"
ECHO.
ECHO Press [R] to rerun, [Shift+R] to clear screen then rerun, or [ESC]/[Enter]/[Space] to exit...
powershell -NoLogo -NoProfile -Command ^
"$ErrorActionPreference='Stop';" ^
"while($true){" ^
" $k=$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');" ^
" if($k.VirtualKeyCode -eq 27 -or $k.VirtualKeyCode -eq 13 -or $k.Character -eq ' '){ exit 1 }" ^
" elseif($k.VirtualKeyCode -eq 82){" ^
" $isShift = ($k.ControlKeyState -band 0x0010) -ne 0 -or ($k.ControlKeyState -band 0x0080) -ne 0;" ^
" if($isShift){ exit 2 } else { exit 0 }" ^
" }" ^
"}"
IF ERRORLEVEL 2 GOTO SHIFT_R
IF ERRORLEVEL 1 GOTO EXIT
echo.
GOTO RUN
:SHIFT_R
CLS
echo.
GOTO RUN
:EXIT
echo.
EXIT /B

View File

@@ -0,0 +1,110 @@
// print-android-version-codes.mjs
import * as fsp from 'node:fs/promises';
import * as path from 'path';
const CSV_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.csv');
/**
* Simple CSV parsing: supports double quoted fields, commas and line breaks.<br>
* zh-CN: 简易 CSV 解析: 支持双引号字段, 逗号, 换行.
*
* @param {string} text
* @returns {string[][]}
*/
function parseCsv(text) {
/** @type {string[][]} */
const rows = [];
/** @type {string[]} */
const row = [];
let i = 0;
let field = '';
let inQuotes = false;
const pushField = () => {
row.push(field);
field = '';
};
const pushRow = () => {
if (row.length) rows.push(row.slice());
row.splice(0);
};
while (i < text.length) {
const c = text[i];
if (inQuotes) {
if (c === '"') {
if (i + 1 < text.length && text[i + 1] === '"') {
field += '"';
i += 2;
continue;
}
inQuotes = false;
i++;
continue;
}
field += c;
i++;
continue;
}
if (c === '"') {
inQuotes = true;
i++;
continue;
}
if (c === ',') {
pushField();
i++;
continue;
}
if (c === '\r') {
i++;
continue;
}
if (c === '\n') {
pushField();
pushRow();
i++;
continue;
}
field += c;
i++;
}
// Last cell or last row.
// zh-CN: 最后一格或最后一行.
if (field.length || row.length) {
pushField();
pushRow();
}
return rows;
}
/**
* @param {string[][]} rows
* @returns {Object<string, string|number>[]}
*/
function toObjects(rows) {
if (!rows.length) return [];
/** @type {string[]} */
const header = rows[0].map(h => h.trim());
return rows.slice(1).map(r => {
/** @type {Object<string, string|number>} */
const o = {};
for (let i = 0; i < header.length; i++) {
const key = header[i];
const value = (r[i] ?? '').trim();
o[key] = key === 'API_LEVEL' ? parseInt(value, 10) : value;
}
return o;
});
}
(async function main() {
const text = await fsp.readFile(CSV_PATH, 'utf8');
const objs = toObjects(parseCsv(text));
console.table(objs);
})().catch(err => {
console.error(err);
process.exitCode = 1;
});

View File

@@ -20,6 +20,7 @@ const SCRIPT_LIST = [
'scrape-and-inject-agp-releases-list.mjs',
'scrape-and-inject-ksp-releases-map.mjs',
'scrape-and-update-foojay-resolver-version.mjs',
'scrape-and-update-android-releases.mjs',
'scrape-and-update-readme-template-contributors-table.mjs',
];

View File

@@ -0,0 +1,268 @@
// scrape-and-update-android-releases.mjs
import * as fs from 'fs';
import * as path from 'path';
import { fetchAndroidReleases } from './fetch-and-parse-android-releases.mjs';
import { printLinesDiffs } from './utils/print.mjs';
const CSV_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.csv');
const TXT_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt');
const readFileSafe = (/** @type {string} */ p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '');
const quoteCsv = (/** @type {string | number} */ v) => {
const s = String(v ?? '');
if (s === '""') return s;
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
/**
* @param {AndroidReleaseCsv[]} rows
* @returns {string}
*/
const toCsv = (rows) => {
/** @type {(keyof AndroidReleaseCsv)[]} */
const headers = [
'VERSION_CODE',
'RELEASE_NAME',
'INTERNAL_CODENAME',
'PLATFORM_VERSION',
'API_LEVEL',
'RELEASE_DATE',
];
const lines = rows.map(r => headers.map(k => quoteCsv(r[k])).join());
return [ headers.join(), ...lines ].join('\n');
};
/**
* @param {AndroidReleaseCsv[]} data
* @returns {void}
*/
function updateCsv(data) {
fs.mkdirSync(path.dirname(CSV_PATH), { recursive: true });
fs.writeFileSync(CSV_PATH, toCsv(data), 'utf8');
}
/**
* @param {AndroidReleaseCsv[]} data
* @param {object} options
* @param {boolean} [options.showIndex=false]
* @param {{ mode: 'all' | 'not-all-numbers' | 'none', style: 'single' | 'double' }} [options.quote]
* @param {{ key: string, compare: (a: AndroidReleaseCsv, b: AndroidReleaseCsv) => number } | { key: string, direction: 'asc' | 'desc' }} [options.sortBy]
* @param {{ [key: string]: string }} [options.headerMap]
* @param {'lower' | 'upper' | 'snake' | 'kebab' | 'title' | 'camel' | 'pascal' | null} [options.headerCase]
* @param {string[]} [options.headers]
* @param {{ [key: string]: (v: any, row: AndroidReleaseCsv, header: string) => string }} [options.renderers]
* @returns {void}
*/
function updateTxt(data, options) {
const defaultOptions = {
showIndex: false,
quote: {
mode: 'not-all-numbers',
style: 'single',
},
sortBy: null,
headerMap: null,
headerCase: null,
headers: [
'VERSION_CODE',
'RELEASE_NAME',
'INTERNAL_CODENAME',
'PLATFORM_VERSION',
'API_LEVEL',
'RELEASE_DATE',
],
renderers: {},
};
const opts = Object.assign(defaultOptions, options ?? {});
const headers = opts.headers.slice();
const toWords = (/** @type {any} */ s) => String(s ?? '')
.replace(/[_\-\s]+/g, ' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.trim()
.split(/\s+/);
/**
* @param {any} s
* @returns {string}
*/
const toCase = (s) => {
const words = toWords(s);
switch (opts.headerCase) {
case 'lower':
return words.join(' ').toLowerCase();
case 'upper':
return words.join(' ').toUpperCase();
case 'snake':
return words.map(w => w.toLowerCase()).join('_');
case 'kebab':
return words.map(w => w.toLowerCase()).join('-');
case 'title':
return words.map(w => w[0] ? (w[0].toUpperCase() + w.slice(1).toLowerCase()) : w).join(' ');
case 'camel':
return words.map((w, i) => i === 0 ? w.toLowerCase() : (w[0]?.toUpperCase() + w.slice(1).toLowerCase())).join('');
case 'pascal':
return words.map(w => w[0]?.toUpperCase() + w.slice(1).toLowerCase()).join('');
default:
return s;
}
};
/**
* Copy and sort data.<br>
* zh-CN: 数据拷贝与排序.
* @type {AndroidReleaseCsv[]}
*/
const rows = data.slice();
if (opts.sortBy) {
if (typeof opts.sortBy === 'function') {
rows.sort(opts.sortBy);
} else if (opts.sortBy && typeof opts.sortBy === 'object' && opts.sortBy.key) {
const key = opts.sortBy.key;
const dir = (opts.sortBy.direction || 'asc').toLowerCase() === 'desc' ? -1 : 1;
const cmp = typeof opts.sortBy.compare === 'function'
? opts.sortBy.compare
: (/** @type {AndroidReleaseCsv} */ a, /** @type {AndroidReleaseCsv} */ b) => {
const va = a?.[key];
const vb = b?.[key];
if (va == null && vb == null) return 0;
if (va == null) return -1;
if (vb == null) return 1;
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
};
rows.sort((a, b) => dir * cmp(a, b));
}
}
/**
* Prepare column values (strings) and determine column quotation in "not-all-numbers" mode.<br>
* zh-CN: 先把每列值准备好 (字符串), 并在 "not-all-numbers" 模式下做整列判定.
* @type {string[][]}
*/
const tableValues = rows.map(r => {
return headers.map(h => {
const v = r[h];
const renderer = opts.renderers?.[h];
const rendered = renderer ? renderer(v, r, h) : (v ?? '');
return String(rendered);
});
});
/**
* Column headers (remapping + case transformation + affixes).<br>
* zh-CN: 列标题 (重映射 + 大小写 + 前后缀).
* @type {string[]}
*/
const headerLabels = headers.map(h => {
const mapped = opts.headerMap?.[h] ?? h;
return toCase(mapped);
});
const finalHeader = opts.showIndex ? [ '(index)', ...headerLabels ] : headerLabels;
const qStyle = opts.quote?.style === 'double' ? `"` : `'`;
const isNumericLike = (/** @type {any} */ v) => /^-?\d+(\.\d+)?$/.test(String(v ?? '').trim());
/** @type {boolean[]} */
const quotedColumns = tableValues[0] ? new Array(tableValues[0].length).fill(false) : [];
if (opts.quote?.mode === 'all') {
for (let c = 0; c < quotedColumns.length; c++) {
quotedColumns[c] = true;
}
} else if (opts.quote?.mode === 'not-all-numbers' || !opts.quote?.mode) {
for (let c = 0; c < quotedColumns.length; c++) {
const col = tableValues.map(row => row[c]);
const allNumeric = col.length > 0 && col.every(isNumericLike);
quotedColumns[c] = !allNumeric;
}
}
/**
* Apply quotes to data.<br>
* zh-CN: 应用引号到数据.
* @type {string[][]}
*/
const quotedTable = tableValues.map(row => {
return row.map((cell, c) => quotedColumns[c]
? qStyle + cell + qStyle
: cell === `""` ? qStyle + qStyle : cell);
});
/**
* Calculate the width of each column.<br>
* zh-CN: 计算每列宽度.
* @type {string[][]}
*/
const rowsForWidth = [];
rowsForWidth.push(finalHeader);
if (opts.showIndex) {
for (let i = 0; i < quotedTable.length; i++) {
rowsForWidth.push([ String(i), ...quotedTable[i] ]);
}
} else {
rowsForWidth.push(...quotedTable);
}
const colCount = rowsForWidth[0]?.length ?? 0;
/** @type {number[]} */
const colWidths = new Array(colCount).fill(0);
for (const r of rowsForWidth) {
for (let c = 0; c < colCount; c++) {
colWidths[c] = Math.max(colWidths[c], String(r[c] ?? '').length);
}
}
/**
* @param {any} s
* @param {number} w
* @returns {string}
*/
const pad = (s, w) => {
const str = String(s ?? '');
const len = str.length;
return str + (len < w ? ' '.repeat(w - len) : '');
};
const joinRow = (/** @type {any[]} */ cells) => `| ${cells.map((v, i) => pad(v, colWidths[i])).join(' | ')} |`;
const divider = `+-${colWidths.map(w => '-'.repeat(w)).join('-+-')}-+`;
const lines = [];
lines.push(divider);
lines.push(joinRow(finalHeader));
lines.push(divider);
if (quotedTable.length > 0) {
for (let i = 0; i < quotedTable.length; i++) {
const cells = opts.showIndex ? [ String(i), ...quotedTable[i] ] : quotedTable[i];
lines.push(joinRow(cells));
}
}
lines.push(divider);
fs.mkdirSync(path.dirname(TXT_PATH), { recursive: true });
fs.writeFileSync(TXT_PATH, lines.join('\n'), 'utf8');
}
(async function main() {
const filename = path.basename(CSV_PATH);
const releases = await fetchAndroidReleases();
const prev = readFileSafe(CSV_PATH).replaceAll('\r\n', '\n');
if (prev !== toCsv(releases)) {
updateCsv(releases);
updateTxt(releases, {
showIndex: false,
quote: {
mode: 'none',
style: 'single',
},
});
console.log(`[${filename}] Updated (Android releases)`);
printLinesDiffs(prev, toCsv(releases));
} else {
// console.log('[${filename}] No update needed (Android releases)');
}
})().catch(err => {
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,40 @@
+------------------------+----------------------------+----------------------+------------------+-----------+--------------------+
| VERSION_CODE | RELEASE_NAME | INTERNAL_CODENAME | PLATFORM_VERSION | API_LEVEL | RELEASE_DATE |
+------------------------+----------------------------+----------------------+------------------+-----------+--------------------+
| BAKLAVA | Android 16 | Baklava | 16 | 36 | June 10, 2025 |
| VANILLA_ICE_CREAM | Android 15 | Vanilla Ice Cream | 15 | 35 | September 3, 2024 |
| UPSIDE_DOWN_CAKE | Android 14 | Upside Down Cake | 14 | 34 | October 4, 2023 |
| TIRAMISU | Android 13 | Tiramisu | 13 | 33 | August 15, 2022 |
| S_V2 | Android 12L | Snow Cone v2 | 12.1 | 32 | March 7, 2022 |
| S | Android 12 | Snow Cone | 12 | 31 | October 4, 2021 |
| R | Android 11 | Red Velvet Cake | 11 | 30 | September 8, 2020 |
| Q | Android 10 | Quince Tart | 10 | 29 | September 3, 2019 |
| P | Android Pie | Pistachio Ice Cream | 9 | 28 | August 6, 2018 |
| O_MR1 | Android Oreo | Oatmeal Cookie | 8.1 | 27 | December 5, 2017 |
| O | Android Oreo | Oatmeal Cookie | 8.0 | 26 | August 21, 2017 |
| N_MR1 | Android Nougat | New York Cheesecake | 7.1-7.1.2 | 25 | October 4, 2016 |
| N | Android Nougat | New York Cheesecake | 7.0 | 24 | August 22, 2016 |
| M | Android Marshmallow | Macadamia Nut Cookie | 6.0-6.0.1 | 23 | September 29, 2015 |
| LOLLIPOP_MR1 | Android Lollipop | Lemon Meringue Pie | 5.1-5.1.1 | 22 | March 2, 2015 |
| LOLLIPOP | Android Lollipop | Lemon Meringue Pie | 5.0-5.0.2 | 21 | November 4, 2014 |
| KITKAT_WATCH | Android KitKat | Key Lime Pie | 4.4W-4.4W.2 | 20 | June 25, 2014 |
| KITKAT | Android KitKat | Key Lime Pie | 4.4-4.4.4 | 19 | October 31, 2013 |
| JELLY_BEAN_MR2 | Android Jelly Bean | Jelly Bean | 4.3-4.3.1 | 18 | July 24, 2013 |
| JELLY_BEAN_MR1 | Android Jelly Bean | Jelly Bean | 4.2-4.2.2 | 17 | November 13, 2012 |
| JELLY_BEAN | Android Jelly Bean | Jelly Bean | 4.1-4.1.2 | 16 | July 9, 2012 |
| ICE_CREAM_SANDWICH_MR1 | Android Ice Cream Sandwich | Ice Cream Sandwich | 4.0.3-4.0.4 | 15 | December 16, 2011 |
| ICE_CREAM_SANDWICH | Android Ice Cream Sandwich | Ice Cream Sandwich | 4.0-4.0.2 | 14 | October 18, 2011 |
| HONEYCOMB_MR2 | Android Honeycomb | Honeycomb | 3.2-3.2.6 | 13 | July 15, 2011 |
| HONEYCOMB_MR1 | Android Honeycomb | Honeycomb | 3.1 | 12 | May 10, 2011 |
| HONEYCOMB | Android Honeycomb | Honeycomb | 3.0 | 11 | February 22, 2011 |
| GINGERBREAD_MR1 | Android Gingerbread | Gingerbread | 2.3.3-2.3.7 | 10 | February 9, 2011 |
| GINGERBREAD | Android Gingerbread | Gingerbread | 2.3-2.3.2 | 9 | December 6, 2010 |
| FROYO | Android Froyo | Froyo | 2.2-2.2.3 | 8 | May 20, 2010 |
| ECLAIR_MR1 | Android Eclair | Eclair | 2.1 | 7 | January 11, 2010 |
| ECLAIR_0_1 | Android Eclair | Eclair | 2.0.1 | 6 | December 3, 2009 |
| ECLAIR | Android Eclair | Eclair | 2.0 | 5 | October 27, 2009 |
| DONUT | Android Donut | Donut | 1.6 | 4 | September 15, 2009 |
| CUPCAKE | Android Cupcake | Cupcake | 1.5 | 3 | April 27, 2009 |
| BASE_1_1 | Android 1.1 | Petit Four | 1.1 | 2 | February 9, 2009 |
| BASE | Android 1.0 | '' | 1.0 | 1 | September 23, 2008 |
+------------------------+----------------------------+----------------------+------------------+-----------+--------------------+

View File

@@ -1,6 +1,8 @@
#Wed Oct 01 22:12:30 GMT+8 2025
#Sep 12, 2025
2.2.20=2.0.3
#Thu Oct 09 11:46:44 GMT+8 2025
#Oct 9, 2025
2.2.21-RC=2.0.4
#Oct 8, 2025
2.2.20=2.0.4
#Sep 4, 2025
2.2.20-RC2=2.0.2
#Aug 20, 2025