From ef70f3bafc880a579df2325f0198cb403651bb32 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Thu, 27 Feb 2020 00:37:03 +0200 Subject: [PATCH] feat/android: Add PoC android project Former-commit-id: 48e47374f4782976d509cc52a2998786073261ea --- AndroidApp/.gitignore | 14 + AndroidApp/.idea/.name | 1 + AndroidApp/.idea/codeStyles/Project.xml | 116 +++++++ AndroidApp/.idea/gradle.xml | 16 + AndroidApp/.idea/misc.xml | 9 + AndroidApp/.idea/runConfigurations.xml | 12 + AndroidApp/.idea/vcs.xml | 6 + AndroidApp/README.md | 3 + AndroidApp/app/.gitignore | 1 + AndroidApp/app/build.gradle | 55 ++++ AndroidApp/app/proguard-rules.pro | 21 ++ .../ExampleInstrumentedTest.java | 27 ++ AndroidApp/app/src/main/AndroidManifest.xml | 28 ++ .../rustboyadvance/EmulatorBindings.java | 79 +++++ .../mrmichel/rustdroid_emu/core/Emulator.java | 77 +++++ .../mrmichel/rustdroid_emu/core/Keypad.java | 44 +++ .../rustdroid_emu/ui/EmulatorActivity.java | 308 ++++++++++++++++++ .../rustdroid_emu/ui/SplashActivity.java | 120 +++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 ++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++ AndroidApp/app/src/main/res/drawable/icon.png | Bin 0 -> 14563 bytes .../src/main/res/drawable/round_button.xml | 28 ++ .../src/main/res/layout/activity_emulator.xml | 25 ++ .../src/main/res/layout/content_emulator.xml | 161 +++++++++ AndroidApp/app/src/main/res/layout/dpad.xml | 58 ++++ .../app/src/main/res/layout/main_activity.xml | 26 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes AndroidApp/app/src/main/res/values/colors.xml | 6 + AndroidApp/app/src/main/res/values/dimens.xml | 3 + .../app/src/main/res/values/strings.xml | 4 + AndroidApp/app/src/main/res/values/styles.xml | 20 ++ .../rustdroid_emu/ExampleUnitTest.java | 17 + AndroidApp/build.gradle | 31 ++ AndroidApp/gradle.properties | 20 ++ AndroidApp/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + AndroidApp/gradlew | 172 ++++++++++ AndroidApp/gradlew.bat | 84 +++++ AndroidApp/settings.gradle | 2 + 50 files changed, 1814 insertions(+) create mode 100644 AndroidApp/.gitignore create mode 100644 AndroidApp/.idea/.name create mode 100644 AndroidApp/.idea/codeStyles/Project.xml create mode 100644 AndroidApp/.idea/gradle.xml create mode 100644 AndroidApp/.idea/misc.xml create mode 100644 AndroidApp/.idea/runConfigurations.xml create mode 100644 AndroidApp/.idea/vcs.xml create mode 100644 AndroidApp/README.md create mode 100644 AndroidApp/app/.gitignore create mode 100644 AndroidApp/app/build.gradle create mode 100644 AndroidApp/app/proguard-rules.pro create mode 100644 AndroidApp/app/src/androidTest/java/com/mrmichel/rustdroid_emu/ExampleInstrumentedTest.java create mode 100644 AndroidApp/app/src/main/AndroidManifest.xml create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Emulator.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Keypad.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java create mode 100644 AndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 AndroidApp/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 AndroidApp/app/src/main/res/drawable/icon.png create mode 100644 AndroidApp/app/src/main/res/drawable/round_button.xml create mode 100644 AndroidApp/app/src/main/res/layout/activity_emulator.xml create mode 100644 AndroidApp/app/src/main/res/layout/content_emulator.xml create mode 100644 AndroidApp/app/src/main/res/layout/dpad.xml create mode 100644 AndroidApp/app/src/main/res/layout/main_activity.xml create mode 100644 AndroidApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 AndroidApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 AndroidApp/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 AndroidApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 AndroidApp/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 AndroidApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 AndroidApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 AndroidApp/app/src/main/res/values/colors.xml create mode 100644 AndroidApp/app/src/main/res/values/dimens.xml create mode 100644 AndroidApp/app/src/main/res/values/strings.xml create mode 100644 AndroidApp/app/src/main/res/values/styles.xml create mode 100644 AndroidApp/app/src/test/java/com/mrmichel/rustdroid_emu/ExampleUnitTest.java create mode 100644 AndroidApp/build.gradle create mode 100644 AndroidApp/gradle.properties create mode 100644 AndroidApp/gradle/wrapper/gradle-wrapper.jar create mode 100644 AndroidApp/gradle/wrapper/gradle-wrapper.properties create mode 100644 AndroidApp/gradlew create mode 100644 AndroidApp/gradlew.bat create mode 100644 AndroidApp/settings.gradle diff --git a/AndroidApp/.gitignore b/AndroidApp/.gitignore new file mode 100644 index 0000000..ebdd23d --- /dev/null +++ b/AndroidApp/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/AndroidApp/.idea/.name b/AndroidApp/.idea/.name new file mode 100644 index 0000000..d193fd8 --- /dev/null +++ b/AndroidApp/.idea/.name @@ -0,0 +1 @@ +RustdroidAdvance \ No newline at end of file diff --git a/AndroidApp/.idea/codeStyles/Project.xml b/AndroidApp/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3279b6b --- /dev/null +++ b/AndroidApp/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/AndroidApp/.idea/gradle.xml b/AndroidApp/.idea/gradle.xml new file mode 100644 index 0000000..2a80f60 --- /dev/null +++ b/AndroidApp/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/AndroidApp/.idea/misc.xml b/AndroidApp/.idea/misc.xml new file mode 100644 index 0000000..f797995 --- /dev/null +++ b/AndroidApp/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/.idea/runConfigurations.xml b/AndroidApp/.idea/runConfigurations.xml new file mode 100644 index 0000000..9b770a6 --- /dev/null +++ b/AndroidApp/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/AndroidApp/.idea/vcs.xml b/AndroidApp/.idea/vcs.xml new file mode 100644 index 0000000..2e3f692 --- /dev/null +++ b/AndroidApp/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AndroidApp/README.md b/AndroidApp/README.md new file mode 100644 index 0000000..2e3bf8d --- /dev/null +++ b/AndroidApp/README.md @@ -0,0 +1,3 @@ +# RustdroidAdvance + +RustdroidAdvance is an android frontend for my rustboyadvance emulator core diff --git a/AndroidApp/app/.gitignore b/AndroidApp/app/.gitignore new file mode 100644 index 0000000..3543521 --- /dev/null +++ b/AndroidApp/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/AndroidApp/app/build.gradle b/AndroidApp/app/build.gradle new file mode 100644 index 0000000..7965667 --- /dev/null +++ b/AndroidApp/app/build.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + defaultConfig { + applicationId "com.mrmichel.rustdroid_emu" + minSdkVersion 19 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' + +cargo { + prebuiltToolchains = true + verbose = true + profile = 'release' + module = "../../rustboyadvance-jni" + targetDirectory = '../../target' + libname = "rustboyadvance_jni" + targets = ['x86', 'arm64'] + apiLevel = 21 +} + +afterEvaluate { + // The `cargoBuild` task isn't available until after evaluation. + android.applicationVariants.all { variant -> + def productFlavor = "" + variant.productFlavors.each { + productFlavor += "${it.name.capitalize()}" + } + def buildType = "${variant.buildType.name.capitalize()}" + tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) + } +} +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'com.google.android.material:material:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/AndroidApp/app/proguard-rules.pro b/AndroidApp/app/proguard-rules.pro new file mode 100644 index 0000000..6e7ffa9 --- /dev/null +++ b/AndroidApp/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/AndroidApp/app/src/androidTest/java/com/mrmichel/rustdroid_emu/ExampleInstrumentedTest.java b/AndroidApp/app/src/androidTest/java/com/mrmichel/rustdroid_emu/ExampleInstrumentedTest.java new file mode 100644 index 0000000..921f480 --- /dev/null +++ b/AndroidApp/app/src/androidTest/java/com/mrmichel/rustdroid_emu/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.mrmichel.rustdroid_emu; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.mrmichel.rustdroid_emu", appContext.getPackageName()); + } +} diff --git a/AndroidApp/app/src/main/AndroidManifest.xml b/AndroidApp/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..617530f --- /dev/null +++ b/AndroidApp/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java new file mode 100644 index 0000000..e1c3d27 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java @@ -0,0 +1,79 @@ +package com.mrmichel.rustboyadvance; + +/** + * JNI wrapper to the rust core + */ +public class EmulatorBindings { + + public class NativeBindingException extends Exception { + public NativeBindingException(String errorMessage) { + super(errorMessage); + } + } + + /** + * Open a new emulator context + * @param bios bytearray of the GBA bios + * @param rom bytearray of the rom to run + * @param save_name name of the save file TODO remove this + * @return the emulator context to use pass to other methods in this class + * @throws NativeBindingException + */ + public static native long openEmulator(byte[] bios, byte[] rom, String save_name) throws NativeBindingException; + + /** + * Make the emulator boot directly into the cartridge + * @param ctx + * @throws NativeBindingException + */ + public static native void skipBios(long ctx) throws NativeBindingException; + + + /** + * Destroys the emulator instance + * should be put in a finalizer or else the emulator context may leak. + * @param ctx + */ + public static native void closeEmulator(long ctx); + + + /** + * Runs the emulation for a single frame. + * @param ctx + * @param frame_buffer will be filled with the frame buffer to render + * @return + * @throws NativeBindingException + */ + public static native void runFrame(long ctx, int[] frame_buffer) throws NativeBindingException; + + /** + * Sets the keystate + * @param keyState + * @return non-zero value on failure + */ + public static native void setKeyState(long ctx, int keyState); + + /** + * Saves the state + * + * @param ctx + * @return save state buffer + * @throws NativeBindingException + */ + public static native byte[] saveState(long ctx) throws NativeBindingException; + + /** + * Loads a save state + * + * @param ctx + * @param state save state buffer + * @throws NativeBindingException + */ + public static native void loadState(long ctx, byte[] state) throws NativeBindingException; + + /** + * Logs the emulator state + * @return non-zero value on failure + */ + public static native void log(long ctx); +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Emulator.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Emulator.java new file mode 100644 index 0000000..86df815 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Emulator.java @@ -0,0 +1,77 @@ +package com.mrmichel.rustdroid_emu.core; + +import com.mrmichel.rustboyadvance.EmulatorBindings; + +public class Emulator { + + public class EmulatorException extends Exception { + public EmulatorException(String errorMessage) { + super(errorMessage); + } + } + + /// context received by the native binding + private long ctx = -1; + + private int[] frameBuffer; + public Keypad keypad; + + static { + System.loadLibrary("rustboyadvance_jni"); + } + + public Emulator() { + frameBuffer = new int[240 * 160]; + keypad = new Keypad(); + } + + public int[] getFrameBuffer() { + return frameBuffer; + } + + public synchronized void runFrame() throws EmulatorBindings.NativeBindingException { + EmulatorBindings.setKeyState(this.ctx, this.keypad.getKeyState()); + EmulatorBindings.runFrame(this.ctx, this.frameBuffer); + } + + public synchronized void setKeyState(int keyState) { + EmulatorBindings.setKeyState(this.ctx, keyState); + } + + + public byte[] saveState() throws EmulatorBindings.NativeBindingException { + return EmulatorBindings.saveState(this.ctx); + } + + + public void loadState(byte[] state) throws EmulatorBindings.NativeBindingException { + EmulatorBindings.loadState(this.ctx, state); + } + + + public synchronized void open(byte[] bios, byte[] rom, String saveName) throws EmulatorBindings.NativeBindingException { + this.ctx = EmulatorBindings.openEmulator(bios, rom, saveName); + } + + public synchronized void close() { + if (this.ctx != -1) { + EmulatorBindings.closeEmulator(this.ctx); + this.ctx = -1; + + } + } + + public boolean isOpen() { + return this.ctx != -1; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + public synchronized void log() { + EmulatorBindings.log(this.ctx); + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Keypad.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Keypad.java new file mode 100644 index 0000000..6d73f42 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Keypad.java @@ -0,0 +1,44 @@ +package com.mrmichel.rustdroid_emu.core; + +public class Keypad { + private int keyState; + + public Keypad() { + reset(); + } + + public void reset() { + this.keyState = 0xffff; + } + + public enum Key { + ButtonA(0), + ButtonB(1), + Select(2), + Start(3), + Right(4), + Left(5), + Up(6), + Down(7), + ButtonR(8), + ButtonL(9); + + private final int keyBit; + + private Key(int keyBit) { + this.keyBit = keyBit; + } + } + + public void onKeyDown(Key key) { + this.keyState = this.keyState & ~(1 << key.keyBit); + } + + public void onKeyUp(Key key) { + this.keyState = this.keyState | (1 << key.keyBit); + } + + public int getKeyState() { + return keyState; + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java new file mode 100644 index 0000000..71b45d4 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java @@ -0,0 +1,308 @@ +package com.mrmichel.rustdroid_emu.ui; + +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.ToggleButton; + +import com.mrmichel.rustboyadvance.EmulatorBindings; +import com.mrmichel.rustdroid_emu.core.Emulator; +import com.mrmichel.rustdroid_emu.core.Keypad; +import com.mrmichel.rustdroid_emu.R; + +import java.io.File; +import java.io.InputStream; + +public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { + + private static final String TAG = "EmulatorActivty"; + private static final int LOAD_ROM_REQUESTCODE = 123; + + private byte[] bios; + private Emulator emulator = null; + private EmulationRunnable runnable; + private Thread emulationThread; + private byte[] snapshot = null; + private ImageView screen; + private boolean turboMode = false; + + @Override + public void onClick(View v) { + if (v.getId() == R.id.tbTurbo) { + ToggleButton tbTurbo = (ToggleButton) findViewById(R.id.tbTurbo); + this.turboMode = tbTurbo.isChecked(); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + Keypad.Key key = null; + switch (v.getId()) { + case R.id.bDpadUp: + key = Keypad.Key.Up; + break; + case R.id.bDpadDown: + key = Keypad.Key.Down; + break; + case R.id.bDpadLeft: + key = Keypad.Key.Left; + break; + case R.id.bDpadRight: + key = Keypad.Key.Right; + break; + case R.id.buttonA: + key = Keypad.Key.ButtonA; + break; + case R.id.buttonB: + key = Keypad.Key.ButtonB; + break; + case R.id.buttonL: + key = Keypad.Key.ButtonL; + break; + case R.id.buttonR: + key = Keypad.Key.ButtonR; + break; + case R.id.bStart: + key = Keypad.Key.Start; + break; + case R.id.bSelect: + key = Keypad.Key.Select; + break; + } + ; + int action = event.getAction(); + if (key != null) { + if (action == MotionEvent.ACTION_DOWN) { + v.setPressed(true); + this.emulator.keypad.onKeyDown(key); + } else if (action == MotionEvent.ACTION_UP) { + v.setPressed(false); + this.emulator.keypad.onKeyUp(key); + } + } + return action == MotionEvent.ACTION_DOWN; + } + + private void showAlertDiaglogAndExit(Exception e) { + new AlertDialog.Builder(this) + .setTitle("Exception") + .setMessage(e.getMessage()) + // Specifying a listener allows you to take an action before dismissing the dialog. + // The dialog is automatically dismissed when a dialog button is clicked. + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + finishAffinity(); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == LOAD_ROM_REQUESTCODE) { + Uri uri = data.getData(); + try { + InputStream inputStream = getContentResolver().openInputStream(uri); + byte[] rom = new byte[inputStream.available()]; + inputStream.read(rom); + inputStream.close(); + + String filename = new File(uri.getPath()).getName(); + + File saveRoot = getFilesDir(); + String savePath = saveRoot.getAbsolutePath() + "/" + filename + ".sav"; + onRomLoaded(rom, savePath); + } catch (Exception e) { + Log.e(TAG, "got error while reading rom file"); + showAlertDiaglogAndExit(e); + } + } + } else { + Log.e(TAG, "got error for request code " + requestCode); + } + } + + public void onRomLoaded(byte[] rom, String savePath) { + if (emulationThread != null) { + runnable.stop(); + try { + emulationThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "emulation thread join interrupted"); + } + emulationThread = null; + } + if (emulator.isOpen()) { + emulator.close(); + } + + findViewById(R.id.bStart).setOnTouchListener(this); + findViewById(R.id.bSelect).setOnTouchListener(this); + findViewById(R.id.buttonA).setOnTouchListener(this); + findViewById(R.id.buttonB).setOnTouchListener(this); + findViewById(R.id.buttonL).setOnTouchListener(this); + findViewById(R.id.buttonR).setOnTouchListener(this); + findViewById(R.id.bDpadUp).setOnTouchListener(this); + findViewById(R.id.bDpadDown).setOnTouchListener(this); + findViewById(R.id.bDpadLeft).setOnTouchListener(this); + findViewById(R.id.bDpadRight).setOnTouchListener(this); + findViewById(R.id.tbTurbo).setOnClickListener(this); + + try { + emulator.open(this.bios, rom, savePath); + } catch (EmulatorBindings.NativeBindingException e) { + showAlertDiaglogAndExit(e); + } + runnable = new EmulationRunnable(this.emulator, this); + emulationThread = new Thread(runnable); + emulationThread.start(); + } + + public void loadRomButton(View v) { + if (runnable != null) { + runnable.pauseEmulation(); + } + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); + startActivityForResult(intent, LOAD_ROM_REQUESTCODE); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_emulator); + + this.bios = getIntent().getByteArrayExtra("bios"); + this.screen = findViewById(R.id.gbaMockImageView); + this.emulator = new Emulator(); + } + + @Override + protected void onPause() { + super.onPause(); + if (emulator.isOpen()) { + if (runnable != null) { + runnable.pauseEmulation(); + } + Log.d(TAG, "onPause - saving emulator state"); + try { + snapshot = emulator.saveState(); + } catch (EmulatorBindings.NativeBindingException e) { + showAlertDiaglogAndExit(e); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + if (emulator.isOpen() && snapshot != null) { + Log.d(TAG, "onResume - loading emulator state"); + try { + emulator.loadState(snapshot); + } catch (EmulatorBindings.NativeBindingException e) { + showAlertDiaglogAndExit(e); + } + snapshot = null; + if (runnable != null) { + runnable.resumeEmulation(); + } + } + } + + public void updateScreen(Bitmap bmp) { + this.screen.setImageBitmap(bmp); + } + + private class EmulationRunnable implements Runnable { + + public static final long NANOSECONDS_PER_MILLISECOND = 1000000; + public static final long FRAME_TIME = 1000000000 / 60; + + EmulatorActivity emulatorActivity; + Emulator emulator; + boolean running; + boolean stopping; + + public EmulationRunnable(Emulator emulator, EmulatorActivity emulatorActivity) { + this.emulator = emulator; + this.emulatorActivity = emulatorActivity; + resumeEmulation(); + } + + private void emulate() { + long startTimer = System.nanoTime(); + + try { + emulator.runFrame(); + } catch (final EmulatorBindings.NativeBindingException e) { + this.running = false; + emulatorActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + showAlertDiaglogAndExit(e); + } + }); + } + + if (!emulatorActivity.turboMode) { + long currentTime = System.nanoTime(); + long timePassed = currentTime - startTimer; + + long delay = FRAME_TIME - timePassed; + if (delay > 0) { + try { + Thread.sleep(delay / NANOSECONDS_PER_MILLISECOND); + } catch (Exception e) { + + } + } + } + + emulatorActivity.runOnUiThread(new Runnable() { + Bitmap bitmap = Bitmap.createBitmap(emulator.getFrameBuffer(), 240, 160, Bitmap.Config.RGB_565); + + @Override + public void run() { + emulatorActivity.updateScreen(bitmap); + } + }); + } + + public void pauseEmulation() { + running = false; + } + + public void resumeEmulation() { + running = true; + } + + public void stop() { + stopping = true; + } + + @Override + public void run() { + while (!stopping) { + if (running) { + emulate(); + } + } + } + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java new file mode 100644 index 0000000..2cac172 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java @@ -0,0 +1,120 @@ +package com.mrmichel.rustdroid_emu.ui; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import com.mrmichel.rustdroid_emu.R; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class SplashActivity extends AppCompatActivity { + + private static final String TAG = "SplashActivity"; + private static final int REQUEST_PERMISSION_CODE = 55; + private static final int BIOS_REQUEST_CODE = 66; + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_PERMISSION_CODE) { + if (permissions.length == 1 && permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initCacheBios(); + } else { + Toast.makeText(this, "WRITE_EXTERNAL_STORAGE not granted, need to quit", Toast.LENGTH_LONG).show(); + this.finishAffinity(); + } + } + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + + if (ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + + // No explanation needed; request the permission + ActivityCompat.requestPermissions(this + , + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_PERMISSION_CODE); + } else { + // Permission has already been granted + initCacheBios(); + + } + } + + private void cacheBiosInAppFiles(byte[] bios) throws FileNotFoundException, IOException { + FileOutputStream fos = openFileOutput("gba_bios.bin", MODE_PRIVATE); + fos.write(bios); + fos.close(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == BIOS_REQUEST_CODE) { + Uri uri = data.getData(); + try { + InputStream inputStream = getContentResolver().openInputStream(uri); + byte[] bios = new byte[inputStream.available()]; + inputStream.read(bios); + inputStream.close(); + + cacheBiosInAppFiles(bios); + + initEmulator(bios); + } catch (Exception e) { + Log.e(TAG, "can't open bios file"); + this.finishAffinity(); + } + } + } else { + Log.e(TAG, "get error for request code " + requestCode); + } + } + + private void initCacheBios() { + try { + FileInputStream fis = openFileInput("gba_bios.bin"); + byte[] bios = new byte[fis.available()]; + fis.read(bios); + initEmulator(bios); + } catch (FileNotFoundException e) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); + intent.putExtra(Intent.EXTRA_TITLE, "Please load the gba_bios.bin file"); + startActivityForResult(intent, BIOS_REQUEST_CODE); + } catch (IOException e) { + Log.e(TAG, "Got IOException while reading from bios"); + } + } + + private void initEmulator(byte[] bios) { + Intent intent = new Intent(this, EmulatorActivity.class); + intent.putExtra("bios", bios); + startActivity(intent); + } +} diff --git a/AndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/AndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..971add5 --- /dev/null +++ b/AndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/AndroidApp/app/src/main/res/drawable/ic_launcher_background.xml b/AndroidApp/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..eed7a42 --- /dev/null +++ b/AndroidApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidApp/app/src/main/res/drawable/icon.png b/AndroidApp/app/src/main/res/drawable/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..39afbf57d5f661871814ffb420638f52cada260e GIT binary patch literal 14563 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⪼DrEn@1e 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 s8K3iC!tMVdg8zS7_ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/res/layout/activity_emulator.xml b/AndroidApp/app/src/main/res/layout/activity_emulator.xml new file mode 100644 index 0000000..bce08d8 --- /dev/null +++ b/AndroidApp/app/src/main/res/layout/activity_emulator.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/res/layout/content_emulator.xml b/AndroidApp/app/src/main/res/layout/content_emulator.xml new file mode 100644 index 0000000..8aa4153 --- /dev/null +++ b/AndroidApp/app/src/main/res/layout/content_emulator.xml @@ -0,0 +1,161 @@ + + + + + + + + +