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 0000000..39afbf5 Binary files /dev/null and b/AndroidApp/app/src/main/res/drawable/icon.png differ diff --git a/AndroidApp/app/src/main/res/drawable/round_button.xml b/AndroidApp/app/src/main/res/drawable/round_button.xml new file mode 100644 index 0000000..0e00740 --- /dev/null +++ b/AndroidApp/app/src/main/res/drawable/round_button.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + +