feat: Android APP V1 - Experimental Alpha

This commit is contained in:
2026-06-05 07:40:25 +02:00
parent bf4e6891d1
commit 9a1305f976
179 changed files with 8031 additions and 120 deletions

25
tools/build-android-apk.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Build a debug Android APK for the Capacitor shell.
#
# Prerequisites (CI installs these automatically):
# - Node.js 22 + root npm dependencies (`npm ci`)
# - JDK 21 (JAVA_HOME)
# - Android SDK with platform 36 + build-tools (ANDROID_SDK_ROOT / ANDROID_HOME)
#
# Output:
# toju-app/android/app/build/outputs/apk/debug/app-debug.apk
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
npm run bundle:rnnoise
npm run build:prod
cd toju-app
npx cap sync android
cd android
chmod +x gradlew
./gradlew assembleDebug --no-daemon --stacktrace

45
tools/cap-open-android.js Normal file
View File

@@ -0,0 +1,45 @@
'use strict';
const { spawn } = require('child_process');
const path = require('path');
const { resolveAndroidStudioPath } = require('./resolve-android-studio-path');
function main() {
const studioPath = resolveAndroidStudioPath();
if (!studioPath) {
console.error(
'[error] Unable to locate Android Studio (studio.sh).\n'
+ ' Install Android Studio or set CAPACITOR_ANDROID_STUDIO_PATH to studio.sh.'
);
process.exit(1);
}
const tojuAppDir = path.resolve(__dirname, '..', 'toju-app');
const child = spawn('npx', ['cap', 'open', 'android'], {
cwd: tojuAppDir,
env: {
...process.env,
CAPACITOR_ANDROID_STUDIO_PATH: studioPath
},
stdio: 'inherit',
shell: process.platform === 'win32'
});
child.on('error', (error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
}
main();

View File

@@ -0,0 +1,99 @@
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
/**
* @param {string} filePath
* @returns {boolean}
*/
function isExecutableStudioSh(filePath) {
try {
const stat = fs.statSync(filePath);
return stat.isFile() && (stat.mode & 0o111) !== 0;
} catch {
return false;
}
}
/**
* @param {string} homeDir
* @returns {string | null}
*/
function findJetBrainsToolboxStudioSh(homeDir) {
const toolboxRoot = path.join(
homeDir,
'.local/share/JetBrains/Toolbox/apps/AndroidStudio'
);
let entries;
try {
entries = fs.readdirSync(toolboxRoot, { withFileTypes: true });
} catch {
return null;
}
const studioPaths = entries
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(toolboxRoot, entry.name, 'bin/studio.sh'))
.filter((candidate) => isExecutableStudioSh(candidate))
.sort();
return studioPaths.at(-1) ?? null;
}
/**
* @param {{ env?: NodeJS.ProcessEnv; homeDir?: string }} [options]
* @returns {string | null}
*/
function resolveAndroidStudioPath(options = {}) {
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? os.homedir();
const fromEnv = String(env.CAPACITOR_ANDROID_STUDIO_PATH ?? '').trim();
if (fromEnv && isExecutableStudioSh(fromEnv)) {
return fromEnv;
}
const candidates = [
'/usr/local/android-studio/bin/studio.sh',
'/opt/android-studio/bin/studio.sh',
path.join(homeDir, 'android-studio/bin/studio.sh'),
'/var/lib/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh',
path.join(
homeDir,
'.local/share/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh'
),
'/snap/android-studio/current/bin/studio.sh'
];
for (const candidate of candidates) {
if (isExecutableStudioSh(candidate)) {
return candidate;
}
}
return findJetBrainsToolboxStudioSh(homeDir);
}
module.exports = {
resolveAndroidStudioPath,
isExecutableStudioSh
};
if (require.main === module) {
const resolved = resolveAndroidStudioPath();
if (!resolved) {
console.error(
'Could not find Android Studio (studio.sh). Install Android Studio or set CAPACITOR_ANDROID_STUDIO_PATH.'
);
process.exit(1);
}
process.stdout.write(`${resolved}\n`);
}

View File

@@ -0,0 +1,41 @@
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { resolveAndroidStudioPath } = require('./resolve-android-studio-path');
test('resolveAndroidStudioPath prefers CAPACITOR_ANDROID_STUDIO_PATH when executable', () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'studio-sh-'));
const studioSh = path.join(tempDir, 'studio.sh');
fs.writeFileSync(studioSh, '#!/bin/sh\n', { mode: 0o755 });
const resolved = resolveAndroidStudioPath({
env: { CAPACITOR_ANDROID_STUDIO_PATH: studioSh },
homeDir: tempDir
});
assert.equal(resolved, studioSh);
fs.rmSync(tempDir, { recursive: true, force: true });
});
test('resolveAndroidStudioPath finds flatpak active studio.sh when present', () => {
const flatpakPath =
'/var/lib/flatpak/app/com.google.AndroidStudio/x86_64/stable/active/files/extra/bin/studio.sh';
if (!fs.existsSync(flatpakPath)) {
return;
}
const resolved = resolveAndroidStudioPath({
env: {},
homeDir: '/nonexistent-home-for-test'
});
assert.equal(resolved, flatpakPath);
});