const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const { exec, spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const multer = require('multer'); const AdmZip = require('adm-zip'); const archiver = require('archiver'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const upload = multer({ dest: 'uploads/' }); // Store active builds and terminals const activeBuilds = new Map(); const activeTerminals = new Map(); // Middleware app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Serve all static files from root app.use(express.static(__dirname)); // ==================== API Routes ==================== // 1. Home page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); // 2. Get all projects app.get('/api/projects', async (req, res) => { try { const projects = await fs.readdir('/workspace'); res.json({ projects }); } catch (error) { res.json({ projects: [] }); } }); // 3. Create new project app.post('/api/project/create', async (req, res) => { const { name, template = 'empty' } = req.body; const projectPath = path.join('/workspace', name); try { await fs.mkdir(projectPath, { recursive: true }); await createAndroidProject(projectPath, name); res.json({ success: true, message: 'Project created', path: projectPath }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 4. Get project files app.get('/api/files/:project/*', async (req, res) => { const { project, '0': filePath } = req.params; const fullPath = path.join('/workspace', project, filePath || ''); try { const stat = await fs.stat(fullPath); if (stat.isDirectory()) { const files = await fs.readdir(fullPath); res.json({ type: 'directory', files }); } else { const content = await fs.readFile(fullPath, 'utf-8'); res.json({ type: 'file', content }); } } catch (error) { res.status(404).json({ error: 'Not found' }); } }); // 5. Save file app.post('/api/file/:project/*', async (req, res) => { const { project, '0': filePath } = req.params; const { content } = req.body; const fullPath = path.join('/workspace', project, filePath); try { await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 6. Build project app.post('/api/build/:project', (req, res) => { const { project } = req.params; const projectPath = path.join('/workspace', project); const buildId = Date.now().toString(); const buildProcess = spawn('./gradlew', ['assembleDebug'], { cwd: projectPath, env: { ...process.env, ANDROID_HOME: '/android-sdk', JAVA_HOME: '/usr/lib/jvm/java-17-openjdk-amd64' } }); let output = ''; buildProcess.stdout.on('data', (data) => { output += data.toString(); }); buildProcess.stderr.on('data', (data) => { output += data.toString(); }); buildProcess.on('close', (code) => { const success = code === 0; const apkPath = path.join(projectPath, 'app/build/outputs/apk/debug/app-debug.apk'); activeBuilds.set(buildId, { success, output, apkPath: success ? apkPath : null, project }); res.json({ buildId, success, output, message: success ? 'Build successful!' : 'Build failed' }); }); }); // 7. Get build status app.get('/api/build/:buildId', (req, res) => { const { buildId } = req.params; const build = activeBuilds.get(buildId); if (build) { res.json(build); } else { res.status(404).json({ error: 'Build not found' }); } }); // 8. Download APK app.get('/api/apk/:project', async (req, res) => { const { project } = req.params; const apkPath = path.join('/workspace', project, 'app/build/outputs/apk/debug/app-debug.apk'); try { await fs.access(apkPath); res.download(apkPath, `${project}.apk`); } catch (error) { res.status(404).json({ error: 'APK not found. Build first!' }); } }); // 9. Upload logo app.post('/api/logo/upload/:project', upload.single('logo'), async (req, res) => { const { project } = req.params; const logoPath = req.file.path; try { // Convert to Android icon sizes and copy to project const resDir = path.join('/workspace', project, 'app/src/main/res'); // Create mipmap directories const sizes = { 'mipmap-hdpi': 72, 'mipmap-mdpi': 48, 'mipmap-xhdpi': 96, 'mipmap-xxhdpi': 144, 'mipmap-xxxhdpi': 192 }; for (const [dir, size] of Object.entries(sizes)) { const destDir = path.join(resDir, dir); await fs.mkdir(destDir, { recursive: true }); // Copy and rename to ic_launcher.png const destPath = path.join(destDir, 'ic_launcher.png'); await fs.copyFile(logoPath, destPath); } res.json({ success: true, message: 'Logo updated successfully' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 10. Terminal WebSocket wss.on('connection', (ws) => { console.log('Terminal connected'); const { spawn } = require('node-pty'); const shell = spawn('bash', [], { name: 'xterm-color', cols: 80, rows: 24, cwd: '/workspace', env: process.env }); const terminalId = Date.now().toString(); activeTerminals.set(terminalId, shell); shell.on('data', (data) => { ws.send(JSON.stringify({ type: 'data', data: data.toString() })); }); ws.on('message', (msg) => { try { const { type, data } = JSON.parse(msg); if (type === 'input') { shell.write(data); } else if (type === 'resize') { shell.resize(data.cols, data.rows); } } catch (e) { console.error('Terminal error:', e); } }); ws.on('close', () => { shell.kill(); activeTerminals.delete(terminalId); }); }); // ==================== Helper Functions ==================== async function createAndroidProject(projectPath, name) { const packageName = `com.${name.toLowerCase().replace(/[^a-z]/g, '')}.app`; // Create directory structure const mainPath = path.join(projectPath, 'app/src/main'); const javaPath = path.join(mainPath, 'java', ...packageName.split('.')); const resPath = path.join(mainPath, 'res'); const layoutPath = path.join(resPath, 'layout'); const drawablePath = path.join(resPath, 'drawable'); const valuesPath = path.join(resPath, 'values'); const mipmapPath = path.join(resPath, 'mipmap-hdpi'); await fs.mkdir(javaPath, { recursive: true }); await fs.mkdir(layoutPath, { recursive: true }); await fs.mkdir(drawablePath, { recursive: true }); await fs.mkdir(valuesPath, { recursive: true }); await fs.mkdir(mipmapPath, { recursive: true }); // 1. settings.gradle await fs.writeFile(path.join(projectPath, 'settings.gradle'), `rootProject.name = "${name}"\ninclude ':app'`); // 2. build.gradle (project) await fs.writeFile(path.join(projectPath, 'build.gradle'), ` // Top-level build file buildscript { ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } `); // 3. gradle.properties await fs.writeFile(path.join(projectPath, 'gradle.properties'), ` org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true kotlin.code.style=official `); // 4. gradlew wrapper const gradlew = `#!/usr/bin/env bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" $DIR/gradlew "$@" `; await fs.writeFile(path.join(projectPath, 'gradlew'), gradlew); await fs.chmod(path.join(projectPath, 'gradlew'), 0o755); // 5. app/build.gradle await fs.writeFile(path.join(projectPath, 'app/build.gradle'), ` plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { compileSdk 34 defaultConfig { applicationId "${packageName}" minSdk 29 targetSdk 34 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' } `); // 6. AndroidManifest.xml await fs.writeFile(path.join(mainPath, 'AndroidManifest.xml'), ` `); // 7. MainActivity.kt await fs.writeFile(path.join(javaPath, 'MainActivity.kt'), ` package ${packageName} import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Your app starts here supportActionBar?.title = "${name}" } } `); // 8. activity_main.xml await fs.writeFile(path.join(layoutPath, 'activity_main.xml'), ` `); // 9. strings.xml await fs.writeFile(path.join(valuesPath, 'strings.xml'), ` ${name} `); // 10. colors.xml await fs.writeFile(path.join(valuesPath, 'colors.xml'), ` #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF `); // 11. themes.xml await fs.writeFile(path.join(valuesPath, 'themes.xml'), ` `); } // Start server server.listen(7860, () => { console.log('🚀 Server running on http://localhost:7860'); console.log('📱 Android 10 Preview Ready'); console.log('🔧 Build System Active'); });