Files
openless/openless-all/app/scripts/copy-android-scaffolding.mjs
T

188 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const kotlinRoot = join(appRoot, 'android/kotlin');
const manifestsRoot = join(appRoot, 'android/manifests');
const androidIconRoot = join(appRoot, 'src-tauri/icons/android');
const genRoot = join(appRoot, 'src-tauri/gen/android/app/src/main');
const kotlinDest = join(genRoot, 'java/com/openless/app');
const resDest = join(genRoot, 'res');
const resXmlDest = join(genRoot, 'res/xml');
const KOTLIN_FILES = [
'OpenLessAppContext.kt',
'OpenLessNative.kt',
'OpenLessPermissionBridge.kt',
'MicrophonePermissionActivity.kt',
'OpenLessAndroidPreferences.kt',
'OpenLessApplication.kt',
'OpenLessOverlayService.kt',
'OpenLessOverlayBridge.kt',
'OpenLessAccessibilityService.kt',
'OpenLessAccessibilityCommandReceiver.kt',
'OverlayPermissionActivity.kt',
];
const XML_FILES = [
['res/xml/openless_accessibility_config.xml', 'openless_accessibility_config.xml'],
];
const GENERATED_ACCESSIBILITY_CONFIG = `<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowsChanged|typeViewFocused"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
android:description="@string/openless_accessibility_description"
android:notificationTimeout="100"
android:settingsActivity="com.openless.app.MainActivity" />
`;
const GENERATED_STRINGS_SNIPPET = `
<string name="openless_accessibility_description">OpenLess uses accessibility to detect the keyboard and paste dictation results without switching your current keyboard.</string>
`;
function printHelp() {
console.log(`Usage: node scripts/copy-android-scaffolding.mjs [options]
Copy Kotlin scaffolding and XML resources into gen/android after \`tauri android init\`.
Options:
--dry-run Print planned copies without writing
--help Show this help text
`);
}
function parseArgs(argv) {
let dryRun = false;
for (const arg of argv) {
if (arg === '--help' || arg === '-h') {
printHelp();
process.exit(0);
}
if (arg === '--dry-run') {
dryRun = true;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
return { dryRun };
}
function ensureDir(path, dryRun) {
if (dryRun || existsSync(path)) {
return;
}
mkdirSync(path, { recursive: true });
}
function mergeStringsXml(dryRun) {
const stringsPath = join(genRoot, 'res/values/strings.xml');
if (!existsSync(stringsPath)) {
const content = `<?xml version="1.0" encoding="utf-8"?>
<resources>${GENERATED_STRINGS_SNIPPET}
</resources>
`;
if (dryRun) {
console.log(`[dry-run] Would create ${stringsPath}`);
return;
}
ensureDir(dirname(stringsPath), dryRun);
writeFileSync(stringsPath, content, 'utf8');
console.log(`Created ${stringsPath}`);
return;
}
const existing = readFileSync(stringsPath, 'utf8');
if (existing.includes('openless_accessibility_description')) {
console.log(`OpenLess strings already present in ${stringsPath}; skipping.`);
return;
}
const updated = existing.replace('</resources>', `${GENERATED_STRINGS_SNIPPET}\n</resources>`);
if (dryRun) {
console.log(`[dry-run] Would merge OpenLess strings into ${stringsPath}`);
return;
}
writeFileSync(stringsPath, updated, 'utf8');
console.log(`Merged OpenLess strings into ${stringsPath}`);
}
function copyDirectoryContents(srcRoot, destRoot, dryRun) {
if (!existsSync(srcRoot)) {
throw new Error(`Missing Android icon resources: ${srcRoot}`);
}
ensureDir(destRoot, dryRun);
for (const entry of readdirSync(srcRoot)) {
const src = join(srcRoot, entry);
const dest = join(destRoot, entry);
if (statSync(src).isDirectory()) {
copyDirectoryContents(src, dest, dryRun);
continue;
}
if (dryRun) {
console.log(`[dry-run] Would copy ${src} -> ${dest}`);
continue;
}
ensureDir(dirname(dest), dryRun);
copyFileSync(src, dest);
console.log(`Copied ${dest}`);
}
}
function main() {
const { dryRun } = parseArgs(process.argv.slice(2));
if (!existsSync(join(appRoot, 'src-tauri/gen/android'))) {
throw new Error(
`Generated Android project not found under src-tauri/gen/android.\nRun "npm run tauri -- android init --ci" first.`,
);
}
ensureDir(kotlinDest, dryRun);
ensureDir(resXmlDest, dryRun);
copyDirectoryContents(androidIconRoot, resDest, dryRun);
for (const file of KOTLIN_FILES) {
const src = join(kotlinRoot, file);
const dest = join(kotlinDest, file);
if (!existsSync(src)) {
throw new Error(`Missing scaffolding file: ${src}`);
}
if (dryRun) {
console.log(`[dry-run] Would copy ${src} -> ${dest}`);
continue;
}
copyFileSync(src, dest);
console.log(`Copied ${file}`);
}
for (const [relSrc, destName] of XML_FILES) {
const src = join(manifestsRoot, relSrc);
const dest = join(resXmlDest, destName);
const content = existsSync(src)
? readFileSync(src, 'utf8')
: GENERATED_ACCESSIBILITY_CONFIG;
if (dryRun) {
console.log(`[dry-run] Would write ${dest}`);
continue;
}
writeFileSync(dest, content, 'utf8');
console.log(`Wrote ${destName}`);
}
mergeStringsXml(dryRun);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}