diff --git a/.github/workflows/Android.yml b/.github/workflows/Android.yml new file mode 100644 index 0000000..5961466 --- /dev/null +++ b/.github/workflows/Android.yml @@ -0,0 +1,56 @@ +name: Android APK Build + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +jobs: + build-android: + name: Build Android APK (ARM64) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Fetch binaries from OpenROM-bins + run: | + git clone https://github.com/M5Devs/OpenROM-bins.git bins + (cd bins && tr -d '\r' < checksums.sha256 | sha256sum -c -) + + - name: Prepare jniLibs + run: | + mkdir -p gui/android/app/src/main/jniLibs/arm64-v8a + for f in bins/android/arm64-v8a/*; do + if [ -f "$f" ]; then + b=$(basename "$f") + cp "$f" "gui/android/app/src/main/jniLibs/arm64-v8a/lib${b}.so" + fi + done + chmod +x gui/android/app/src/main/jniLibs/arm64-v8a/* + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Build APK + run: | + cd gui + flutter pub get + flutter build apk --release --target-platform android-arm64 + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: OpenROM-Android-arm64 + path: gui/build/app/outputs/flutter-apk/app-release.apk diff --git a/core/lib/src/config.dart b/core/lib/src/config.dart index 80aa930..2d16b7a 100644 --- a/core/lib/src/config.dart +++ b/core/lib/src/config.dart @@ -8,6 +8,12 @@ import 'package:path/path.dart' as p; const String appName = 'OpenROM'; +String? _androidNativeLibDir; + +Future initAndroid(String libDir) async { + _androidNativeLibDir = libDir; +} + String getDirectoryConfig() { String path; if (Platform.isWindows) { @@ -34,7 +40,8 @@ String get configFile => p.join(getDirectoryConfig(), 'config.json'); Map? _configCache; void ensureExecutable(String filePath) { - if ((Platform.isLinux || Platform.isMacOS) && File(filePath).existsSync()) { + if ((Platform.isLinux || Platform.isMacOS || Platform.isAndroid) && + File(filePath).existsSync()) { try { Process.runSync('chmod', ['+x', filePath]); } catch (_) {} @@ -189,6 +196,15 @@ bool saveConfig(Map config) { } String getToolPath(String tool) { + if (Platform.isAndroid) { + if (_androidNativeLibDir != null && _androidNativeLibDir!.isNotEmpty) { + final libPath = p.join(_androidNativeLibDir!, 'lib$tool.so'); + ensureExecutable(libPath); + return libPath; + } + return 'lib$tool.so'; + } + final config = loadConfig(); final val = config[tool] as String? ?? ''; if (val.isNotEmpty && (File(val).existsSync() || p.isAbsolute(val))) { diff --git a/gui/.metadata b/gui/.metadata index 4328837..05a325e 100644 --- a/gui/.metadata +++ b/gui/.metadata @@ -15,13 +15,7 @@ migration: - platform: root create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: linux - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: macos - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: windows + - platform: android create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a diff --git a/gui/android/.gitignore b/gui/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/gui/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/gui/android/app/build.gradle.kts b/gui/android/app/build.gradle.kts new file mode 100644 index 0000000..6dd4023 --- /dev/null +++ b/gui/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.m5dev.openrom" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + applicationId = "com.m5dev.openrom" + minSdk = 26 + targetSdk = 34 + versionCode = flutter.versionCode + versionName = flutter.versionName + ndk { + abiFilters.add("arm64-v8a") + } + } + + sourceSets { + getByName("main") { + jniLibs.srcDirs("src/main/jniLibs") + } + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/gui/android/app/src/debug/AndroidManifest.xml b/gui/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/gui/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/gui/android/app/src/main/AndroidManifest.xml b/gui/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3b1ee37 --- /dev/null +++ b/gui/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/android/app/src/main/kotlin/com/m5dev/openrom/MainActivity.kt b/gui/android/app/src/main/kotlin/com/m5dev/openrom/MainActivity.kt new file mode 100644 index 0000000..e75b412 --- /dev/null +++ b/gui/android/app/src/main/kotlin/com/m5dev/openrom/MainActivity.kt @@ -0,0 +1,20 @@ +package com.m5dev.openrom + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class MainActivity: FlutterActivity() { + private val CHANNEL = "com.m5dev.openrom/native" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + if (call.method == "getNativeLibraryDir") { + result.success(applicationInfo.nativeLibraryDir) + } else { + result.notImplemented() + } + } + } +} diff --git a/gui/android/app/src/main/res/drawable-v21/launch_background.xml b/gui/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/gui/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/gui/android/app/src/main/res/drawable/launch_background.xml b/gui/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/gui/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/gui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/gui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/gui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/gui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/gui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/gui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/gui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/gui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/gui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/gui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/gui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/gui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/gui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/gui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/gui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/gui/android/app/src/main/res/values-night/styles.xml b/gui/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/gui/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/gui/android/app/src/main/res/values/styles.xml b/gui/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/gui/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/gui/android/app/src/profile/AndroidManifest.xml b/gui/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/gui/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/gui/android/build.gradle.kts b/gui/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/gui/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/gui/android/gradle.properties b/gui/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/gui/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/gui/android/gradle/wrapper/gradle-wrapper.properties b/gui/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/gui/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/gui/android/settings.gradle.kts b/gui/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/gui/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/gui/lib/main.dart b/gui/lib/main.dart index 1b1a01e..45e4219 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -1,7 +1,11 @@ // OpenROM — Universal ROM Compression Suite // M5 Dev | GPL v3 +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:openrom_core/openrom_core.dart' as core; import 'package:window_manager/window_manager.dart'; import 'app.dart'; @@ -11,22 +15,35 @@ import 'services/theme_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - try { - await windowManager.ensureInitialized(); - const windowOptions = WindowOptions( - size: Size(1100, 720), - minimumSize: Size(800, 500), - center: true, - backgroundColor: Colors.transparent, - skipTaskbar: false, - title: 'OpenROM — Universal ROM Compression Suite', - ); - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); - } catch (_) { - // Window manager setup fallback if headlessly executing or uninitialized + if (Platform.isAndroid) { + try { + const channel = MethodChannel('com.m5dev.openrom/native'); + final String? libDir = + await channel.invokeMethod('getNativeLibraryDir'); + if (libDir != null && libDir.isNotEmpty) { + await core.initAndroid(libDir); + } + } catch (e) { + debugPrint('Failed to initialize Android native library dir: $e'); + } + } else { + try { + await windowManager.ensureInitialized(); + const windowOptions = WindowOptions( + size: Size(1100, 720), + minimumSize: Size(800, 500), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + title: 'OpenROM — Universal ROM Compression Suite', + ); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + } catch (_) { + // Window manager setup fallback if headlessly executing or uninitialized + } } final themeService = ThemeService(); diff --git a/gui/lib/utils/config.dart b/gui/lib/utils/config.dart index 033f51b..9741ee6 100644 --- a/gui/lib/utils/config.dart +++ b/gui/lib/utils/config.dart @@ -3,10 +3,15 @@ import 'dart:io'; +import 'package:openrom_core/openrom_core.dart' as core; + class AppConfig { /// Resolves tool binary path relative to executable. /// Windows: `.exe`, Linux/macOS: `` static String getToolPath(String toolName) { + if (Platform.isAndroid) { + return core.getToolPath(toolName); + } final exeDir = File(Platform.resolvedExecutable).parent.path; final binaryName = Platform.isWindows ? '$toolName.exe' : toolName; return '$exeDir${Platform.pathSeparator}$binaryName';