feat(android): in-app updater (backend/Gitea APK), auto-bump versionCode

The app checks {backend}/api/apk/info on launch and, if a newer versionCode
than BuildConfig.VERSION_CODE is offered, shows an update dialog that
downloads {backend}/api/apk into a single wiped-each-time cache file and
hands it to the system installer (FileProvider + REQUEST_INSTALL_PACKAGES).
No APK hoarding; in-place install preserves data (stable signing key needed).

- backend /api/apk/info now returns versionCode (parsed from the release body).
- publish-apk.mjs auto-increments versionCode, sets versionName, and writes
  versionCode/versionName into the Gitea release body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-09-02 00:46:11 +02:00
1 parent 845f125d76
commit 2fd4d978d0
7 files changed
+175 -14

No files matched your search

+8 -3
View File
@@ -508,10 +508,14 @@ async function _giteaApkAsset() {
const rel = await _giteaApiJson(`/releases/tags/${encodeURIComponent(GITEA_APK_TAG)}`);
const asset = (rel.assets || []).find(a => /\.apk$/i.test(a.name || ''));
if (!asset) return null;
// The publish script writes "versionCode=NN" into the release body so the in-app updater has a
// reliable integer to compare against BuildConfig.VERSION_CODE.
const vcMatch = /versionCode\s*[=:]\s*(\d+)/i.exec(rel.body || '');
return {
size: asset.size,
url: asset.browser_download_url,
version: rel.name || rel.tag_name || null,
versionCode: vcMatch ? parseInt(vcMatch[1], 10) : null,
updatedAt: new Date(asset.created_at || rel.published_at || Date.now()).getTime(),
};
}
@@ -548,13 +552,14 @@ function _streamApkFromUrl(urlStr, res, redirectsLeft = 3) {
app.get('/api/apk/info', async (req, res) => {
try {
const a = await _giteaApkAsset();
if (a) return res.json({ available: true, size: a.size, updatedAt: a.updatedAt, version: a.version, source: 'gitea' });
if (a) return res.json({ available: true, size: a.size, updatedAt: a.updatedAt, version: a.version, versionCode: a.versionCode, source: 'gitea' });
} catch { /* fall through to local */ }
fs.stat(APK_PATH, (err, st) => {
if (err) return res.json({ available: false });
let version = null;
let version = null, versionCode = null;
try { version = fs.readFileSync(APK_VER_PATH, 'utf8').trim() || null; } catch {}
res.json({ available: true, size: st.size, updatedAt: st.mtimeMs, version, source: 'local' });
try { versionCode = parseInt(fs.readFileSync(path.join(CONFIG.dataPath, 'novasonic.versioncode.txt'), 'utf8').trim(), 10) || null; } catch {}
res.json({ available: true, size: st.size, updatedAt: st.mtimeMs, version, versionCode, source: 'local' });
});
});