From 2aa0eec95e569cf540c287c05b978bc51b8fb6b5 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Sat, 29 Feb 2020 23:40:05 +0200 Subject: [PATCH] android: Putting in work - Landscape layout. - Snapshot viewer is now a fragment for future re-use. - Bugfixes - Etc' Former-commit-id: 34365fd143cefdc2b1bb3b68a62f62849f442fa1 --- AndroidApp/app/build.gradle | 4 +- AndroidApp/app/src/main/AndroidManifest.xml | 18 +- .../rustboyadvance/EmulatorBindings.java | 16 +- .../mrmichel/rustboyadvance/RomHelper.java | 12 + .../java/com/mrmichel/rustdroid_emu/Util.java | 126 ++++ .../mrmichel/rustdroid_emu/core/Emulator.java | 46 +- .../rustdroid_emu/core/RomManager.java | 321 +++++++++++ .../mrmichel/rustdroid_emu/core/Snapshot.java | 8 +- .../rustdroid_emu/core/SnapshotManager.java | 187 +++--- .../rustdroid_emu/ui/EmulationThread.java | 72 +++ .../rustdroid_emu/ui/EmulatorActivity.java | 542 ++++++++++++------ .../rustdroid_emu/ui/RomListActivity.java | 32 -- ...GbaScreenView.java => ScreenRenderer.java} | 49 +- .../mrmichel/rustdroid_emu/ui/ScreenView.java | 37 ++ .../rustdroid_emu/ui/SettingsActivity.java | 19 + .../rustdroid_emu/ui/SettingsFragment.java | 17 + .../rustdroid_emu/ui/SplashActivity.java | 30 +- .../ui/library/RomListActivity.java | 223 +++++++ .../ui/library/RomListItemAdapter.java | 61 ++ .../ui/snapshots/ChosenSnapshot.java | 21 - .../ui/snapshots/ISnapshotListener.java | 8 + .../ui/snapshots/SnapshotActivity.java | 60 -- .../ui/snapshots/SnapshotListFragment.java | 116 ++++ .../ui/snapshots/SnapshotPickerActivity.java | 39 ++ .../src/main/res/drawable/round_button.xml | 33 +- .../res/layout-land/activity_emulator.xml | 18 + .../main/res/layout-land/content_emulator.xml | 122 ++++ .../src/main/res/layout/activity_emulator.xml | 32 +- .../src/main/res/layout/activity_rom_list.xml | 10 +- .../src/main/res/layout/activity_settings.xml | 13 + .../src/main/res/layout/content_emulator.xml | 52 +- .../src/main/res/layout/content_rom_list.xml | 18 +- .../app/src/main/res/layout/rom_item.xml | 38 ++ .../app/src/main/res/layout/snapshot_item.xml | 3 +- ...napshot.xml => snapshot_list_fragment.xml} | 27 +- .../res/layout/snapshot_picker_activity.xml | 7 + .../src/main/res/layout/splash_activity.xml | 1 - .../src/main/res/menu/menu_context_rom.xml | 31 + .../main/res/menu/menu_context_snapshot.xml | 12 + .../app/src/main/res/menu/menu_emulator.xml | 42 +- .../app/src/main/res/menu/menu_rom_list.xml | 9 +- .../app/src/main/res/menu/menu_snapshot.xml | 2 +- AndroidApp/app/src/main/res/values/colors.xml | 7 +- .../app/src/main/res/values/strings.xml | 98 +--- .../app/src/main/res/xml/app_preferences.xml | 8 + rustboyadvance-jni/src/lib.rs | 66 ++- rustboyadvance-jni/src/rom_helper.rs | 30 + src/core/cartridge/mod.rs | 2 +- src/core/gba.rs | 20 + 49 files changed, 2030 insertions(+), 735 deletions(-) create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/RomHelper.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/Util.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/RomManager.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulationThread.java delete mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java rename AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/{GbaScreenView.java => ScreenRenderer.java} (85%) create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenView.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsActivity.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsFragment.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListActivity.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListItemAdapter.java delete mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ChosenSnapshot.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ISnapshotListener.java delete mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotListFragment.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotPickerActivity.java create mode 100644 AndroidApp/app/src/main/res/layout-land/activity_emulator.xml create mode 100644 AndroidApp/app/src/main/res/layout-land/content_emulator.xml create mode 100644 AndroidApp/app/src/main/res/layout/activity_settings.xml create mode 100644 AndroidApp/app/src/main/res/layout/rom_item.xml rename AndroidApp/app/src/main/res/layout/{activity_snapshot.xml => snapshot_list_fragment.xml} (61%) create mode 100644 AndroidApp/app/src/main/res/layout/snapshot_picker_activity.xml create mode 100644 AndroidApp/app/src/main/res/menu/menu_context_rom.xml create mode 100644 AndroidApp/app/src/main/res/menu/menu_context_snapshot.xml create mode 100644 AndroidApp/app/src/main/res/xml/app_preferences.xml create mode 100644 rustboyadvance-jni/src/rom_helper.rs diff --git a/AndroidApp/app/build.gradle b/AndroidApp/app/build.gradle index 7965667..390b7f1 100644 --- a/AndroidApp/app/build.gradle +++ b/AndroidApp/app/build.gradle @@ -5,7 +5,7 @@ android { buildToolsVersion "29.0.3" defaultConfig { applicationId "com.mrmichel.rustdroid_emu" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 23 versionCode 1 versionName "1.0" @@ -49,6 +49,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'com.google.android.material:material:1.1.0' + implementation "androidx.documentfile:documentfile:1.0.1" + implementation 'androidx.preference:preference: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/src/main/AndroidManifest.xml b/AndroidApp/app/src/main/AndroidManifest.xml index 866732c..8268750 100644 --- a/AndroidApp/app/src/main/AndroidManifest.xml +++ b/AndroidApp/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ - + + @@ -13,17 +16,18 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> + + android:name=".ui.snapshots.SnapshotListFragment" + android:label="@string/title_activity_snapshot" /> + android:label="@string/title_activity_emulator" /> + diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java index 870ab57..96c3789 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java @@ -5,6 +5,10 @@ package com.mrmichel.rustboyadvance; */ public class EmulatorBindings { + static { + System.loadLibrary("rustboyadvance_jni"); + } + public class NativeBindingException extends Exception { public NativeBindingException(String errorMessage) { super(errorMessage); @@ -17,10 +21,20 @@ public class EmulatorBindings { * @param rom bytearray of the rom to run * @param frameBuffer frameBuffer render target * @param save_name name of the save file TODO remove this + * @param skipBios skip bios * @return the emulator context to use pass to other methods in this class * @throws NativeBindingException */ - public static native long openEmulator(byte[] bios, byte[] rom, int[] frameBuffer, String save_name) throws NativeBindingException; + public static native long openEmulator(byte[] bios, byte[] rom, int[] frameBuffer, String save_name, boolean skipBios) throws NativeBindingException; + + /** + * Open a new emulator context from a saved state buffer + * @param savedState + * @param frameBuffer + * @return + * @throws NativeBindingException + */ + public static native long openSavedState(byte[] savedState, int[] frameBuffer) throws NativeBindingException; /** * Make the emulator boot directly into the cartridge diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/RomHelper.java b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/RomHelper.java new file mode 100644 index 0000000..33ffe66 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/RomHelper.java @@ -0,0 +1,12 @@ +package com.mrmichel.rustboyadvance; + +public class RomHelper { + + static { + System.loadLibrary("rustboyadvance_jni"); + } + + public static native String getGameCode(byte[] romData); + + public static native String getGameTitle(byte[] romData); +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/Util.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/Util.java new file mode 100644 index 0000000..cd295f8 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/Util.java @@ -0,0 +1,126 @@ +package com.mrmichel.rustdroid_emu; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; + +import com.mrmichel.rustdroid_emu.ui.EmulatorActivity; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public class Util { + + private static final String TAG = "Util"; + + + public static void startEmulator(Context context, byte[] bios, int romId) { + Intent intent = new Intent(context, EmulatorActivity.class); + intent.putExtra("bios", bios); + intent.putExtra("romId", romId); + context.startActivity(intent); + } + + + public static void showAlertDiaglogAndExit(final Activity activity, Exception e) { + new AlertDialog.Builder(activity) + .setTitle(e.toString()) + .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) { + activity.finishAffinity(); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + + public static byte[] compressBitmapToByteArray(Bitmap bitmap) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + + public static void writeCompressedFile(File file, byte[] bytes) { + try { + FileOutputStream fos = new FileOutputStream(file); + GZIPOutputStream gos = new GZIPOutputStream(fos); + + gos.write(bytes); + gos.close(); + fos.close(); + } catch (Exception e) { + Log.e(TAG, "failed to write compressed file " + file.toString() + " error: " + e.getMessage()); + } + } + + public static byte[] readCompressedFile(File file) { + try { + byte[] buffer = new byte[8192]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPInputStream gis = new GZIPInputStream(new FileInputStream(file)); + + int len; + + while ( (len = gis.read(buffer, 0, 8192)) != -1) { + outputStream.write(buffer, 0, len); + } + gis.close(); + return outputStream.toByteArray(); + } catch (Exception e) { + Log.e(TAG, "failed to read compressed file " + file.toString() + " error: " + e.getMessage()); + return null; + } + } + + public static byte[] readFile(File file) throws FileNotFoundException, IOException { + byte[] buffer = new byte[8192]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + FileInputStream fis = new FileInputStream(file); + + int len; + + while ( (len = fis.read(buffer, 0, 8192)) != -1) { + outputStream.write(buffer, 0, len); + } + fis.close(); + return outputStream.toByteArray(); + } + + public static String byteArrayToHexString(final byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } + + public static String getHash(final byte[] bytes) { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + // impossible + Log.e("SnapshotManager", "SHA-256 algo not found"); + return null; + } + + md.update(bytes); + return byteArrayToHexString(md.digest()); + } +} 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 index 21fc868..9d22740 100644 --- 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 @@ -16,13 +16,23 @@ public class Emulator { private int[] frameBuffer; public Keypad keypad; - static { - System.loadLibrary("rustboyadvance_jni"); + public Emulator() { + this.frameBuffer = new int[240 * 160]; + this.keypad = new Keypad(); } - public Emulator() { - frameBuffer = new int[240 * 160]; - keypad = new Keypad(); + public Emulator(long ctx) { + this.ctx = ctx; + this.frameBuffer = new int[240 * 160]; + this.keypad = new Keypad(); + + } + + /** + * Get the native emulator handle for caching + */ + public long getCtx() { + return ctx; } public int[] getFrameBuffer() { @@ -49,12 +59,20 @@ public class Emulator { public synchronized void loadState(byte[] state) throws EmulatorBindings.NativeBindingException { - EmulatorBindings.loadState(this.ctx, state); + if (ctx != -1) { + EmulatorBindings.loadState(this.ctx, state); + } else { + openSavedState(state); + } } - public synchronized void open(byte[] bios, byte[] rom, String saveName) throws EmulatorBindings.NativeBindingException { - this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameBuffer, saveName); + public synchronized void open(byte[] bios, byte[] rom, String saveName, boolean skipBios) throws EmulatorBindings.NativeBindingException { + this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameBuffer, saveName, skipBios); + } + + public synchronized void openSavedState(byte[] savedState) throws EmulatorBindings.NativeBindingException { + this.ctx = EmulatorBindings.openSavedState(savedState, this.frameBuffer); } public synchronized void close() { @@ -66,11 +84,19 @@ public class Emulator { } public String getGameCode() { - return EmulatorBindings.getGameCode(ctx); + if (ctx != -1) { + return EmulatorBindings.getGameCode(ctx); + } else { + return null; + } } public String getGameTitle() { - return EmulatorBindings.getGameTitle(ctx); + if (ctx != -1) { + return EmulatorBindings.getGameTitle(ctx); + } else { + return null; + } } public boolean isOpen() { diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/RomManager.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/RomManager.java new file mode 100644 index 0000000..a1b9cb2 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/RomManager.java @@ -0,0 +1,321 @@ +package com.mrmichel.rustdroid_emu.core; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; + +import com.mrmichel.rustboyadvance.RomHelper; +import com.mrmichel.rustdroid_emu.Util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Timestamp; +import java.util.ArrayList; + +public class RomManager { + private static final String TAG = "RomManager"; + private static RomManager instance; + private RomDatabaseHelper dbHelper; + private Context context; + + public RomManager(Context context) { + this.context = context; + this.dbHelper = new RomDatabaseHelper(this.context, 1); + } + + public static RomManager getInstance(Context context) { + if (instance == null) { + instance = new RomManager(context); + } + return instance; + } + + private static String byteArrayToHexString(final byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } + + private static String getHash(final byte[] bytes) { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + // impossible + Log.e(TAG, "SHA-256 algo not found"); + return null; + } + + md.update(bytes); + return byteArrayToHexString(md.digest()); + } + + public ArrayList getAllRomMetaData() { + return this.dbHelper.queryRomMetadata("SELECT * FROM " + RomDatabaseHelper.TABLE_METADATA + " ORDER BY lastPlayed DESC"); + } + + public RomMetadataEntry getRomMetadata(byte[] romData) { + String romHash = getHash(romData); + + ArrayList metadataEntries = dbHelper.queryRomMetadata( + "SELECT * FROM " + RomDatabaseHelper.TABLE_METADATA + " where hash == '" + romHash + "'"); + + if (metadataEntries.size() > 0) { + return metadataEntries.get(0); + } else { + return null; + } + } + + public RomMetadataEntry getRomMetadata(int romId) { + ArrayList metadataEntries = dbHelper.queryRomMetadata( + "SELECT * FROM " + RomDatabaseHelper.TABLE_METADATA + " where id = '" + romId + "'"); + + if (metadataEntries.size() > 0) { + return metadataEntries.get(0); + } else { + return null; + } + } + + + private byte[] readFromUri(Uri uri) throws IOException { + ContentResolver cr = context.getContentResolver(); + + InputStream is = cr.openInputStream(uri); + byte[] data = new byte[is.available()]; + is.read(data); + return data; + } + + public void importRom(DocumentFile documentFile) { + + Uri uri = documentFile.getUri(); + + byte[] romData; + try { + romData = readFromUri(uri); + } catch (Exception e) { + Log.e(TAG, "could not read rom file"); + return; + } + + if (null != getRomMetadata(romData)) { + Toast.makeText(context, "This rom is already imported!", Toast.LENGTH_LONG).show(); + return; + } + + String hash = getHash(romData); + String gameCode = RomHelper.getGameCode(romData); + String gameTitle = RomHelper.getGameTitle(romData); + + String romFileName = documentFile.getName(); + + // Multiple roms can have the same title+code combo, so we rely on a hash to be a unique identifier. + File baseDir = new File(context.getFilesDir(), hash); + baseDir.mkdirs(); + + File romFile = new File(baseDir, romFileName); + + // cache the rom + try { + FileOutputStream fileOutputStream = new FileOutputStream(romFile); + fileOutputStream.write(romData); + } catch (Exception e) { + Log.e(TAG, "cannot cache rom file"); + } + + File backupFile = new File(baseDir, romFileName + ".sav"); + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + ContentValues cv = new ContentValues(); + + cv.put("name", romFileName); + cv.put("gameTitle", gameTitle); + cv.put("gameCode", gameCode); + cv.put("hash", hash); + cv.put("path", romFile.getPath()); + cv.put("backupPath", backupFile.getPath()); + + db.insertOrThrow(RomDatabaseHelper.TABLE_METADATA, null, cv); + db.close(); + } + + public void deleteRomMetadata(RomMetadataEntry romMetadataEntry) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + db.delete(RomDatabaseHelper.TABLE_METADATA, "id=" + romMetadataEntry.getId(), null); + } + + public void updateLastPlayed(int romId) { + Timestamp now = new Timestamp(System.currentTimeMillis()); + + ContentValues cv = new ContentValues(); + cv.put("lastPlayed", now.toString()); + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + db.update(RomDatabaseHelper.TABLE_METADATA, cv, "id=" + romId, null); + } + + public void updateScreenshot(int romId, Bitmap bitmap) { + + ContentValues cv = new ContentValues(); + cv.put("screenshot", Util.compressBitmapToByteArray(bitmap)); + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + db.update(RomDatabaseHelper.TABLE_METADATA, cv, "id=" + romId, null); + } + + + public class RomMetadataEntry { + int id; + String name; + String gameTitle; + String gameCode; + File romFile; + File backupFile; + Bitmap screenshot; + Timestamp lastPlayed; + + private RomMetadataEntry(int id, String name, String gameTitle, String gameCode, File romFile, File backupFile, Bitmap screenshot, Timestamp lastPlayed) { + this.id = id; + this.name = name; + this.gameTitle = gameTitle; + this.gameCode = gameCode; + this.romFile = romFile; + this.backupFile = backupFile; + this.screenshot = screenshot; + this.lastPlayed = lastPlayed; + } + + + public String getName() { + return name; + } + + public int getId() { + return id; + } + + public Bitmap getScreenshot() { + return screenshot; + } + + public File getBackupFile() { + return backupFile; + } + + public File getRomFile() { + return romFile; + } + + public String getGameTitle() { + return gameTitle; + } + + public String getGameCode() { + return gameCode; + } + + public Timestamp getLastPlayed() { + return lastPlayed; + } + } + + private class RomDatabaseHelper extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "rom_db"; + + private static final String TABLE_METADATA = "rom_metadata"; + + + public RomDatabaseHelper(@Nullable Context context, int version) { + super(context, DATABASE_NAME, null, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("create table " + TABLE_METADATA + + " (id INTEGER PRIMARY KEY," + + "name TEXT UNIQUE," + + "hash TEXT UNIQUE," + + "gameTitle TEXT," + + "gameCode TEXT," + + "screenshot BLOB," + + "lastPlayed TIMESTAMP," + + "path TEXT UNIQUE," + + "backupPath TEXT UNIQUE" + + ")"); + } + + public ArrayList queryRomMetadata(String query) { + ArrayList arrayList = new ArrayList<>(); + + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery(query, null); + + if (cursor.moveToFirst()) { + do { + + String name = cursor.getString(cursor.getColumnIndex("name")); + + File romFile = new File(cursor.getString(cursor.getColumnIndex("path"))); + File backupFile = new File(cursor.getString(cursor.getColumnIndex("backupPath"))); + + byte[] screenshotBlob = cursor.getBlob(cursor.getColumnIndex("screenshot")); + Bitmap screenshot; + if (null != screenshotBlob) { + screenshot = BitmapFactory.decodeByteArray(screenshotBlob, 0, screenshotBlob.length); + } else { + screenshot = null; + } + + String gameTitle = cursor.getString(cursor.getColumnIndex("gameTitle")); + String gameCode = cursor.getString(cursor.getColumnIndex("gameCode")); + + int id = cursor.getInt(cursor.getColumnIndex("id")); + + String lastPlayedString = cursor.getString(cursor.getColumnIndex("lastPlayed")); + Timestamp lastPlayed; + if (lastPlayedString != null) { + lastPlayed = Timestamp.valueOf(lastPlayedString); + } else { + lastPlayed = null; + } + + arrayList.add(new RomMetadataEntry(id, name, gameTitle, gameCode, romFile, backupFile, screenshot, lastPlayed)); + + } while (cursor.moveToNext()); + } + + + cursor.close(); + db.close(); + + return arrayList; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_METADATA); + onCreate(db); + } + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java index 3b6de09..eb13220 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java @@ -2,6 +2,8 @@ package com.mrmichel.rustdroid_emu.core; import android.graphics.Bitmap; +import com.mrmichel.rustdroid_emu.Util; + import java.io.File; public class Snapshot { @@ -27,6 +29,10 @@ public class Snapshot { this.timestamp = timestamp; } + public File getFile() { + return file; + } + public String getGameCode() { return gameCode; } @@ -44,6 +50,6 @@ public class Snapshot { } public byte[] load() { - return SnapshotManager.readCompressedFile(this.file); + return Util.readCompressedFile(this.file); } } diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java index a133a9f..d98a0d1 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java @@ -7,18 +7,13 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.util.Log; + +import com.mrmichel.rustdroid_emu.Util; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.sql.Timestamp; import java.util.ArrayList; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; public class SnapshotManager { private static final String TAG = "SnapshotManager"; @@ -32,6 +27,65 @@ public class SnapshotManager { private SnapshotDatabaseHelper dbHelper; + private SnapshotManager(Context context) { + this.context = context; + this.dbHelper = new SnapshotDatabaseHelper(this.context); +// this.snapshotDB = context.openOrCreateDatabase("snapshots", Context.MODE_PRIVATE, null); + } + + public static SnapshotManager getInstance(Context context) { + if (instance == null) { + instance = new SnapshotManager(context); + } + return instance; + } + + private File getPreviewsDir(String gameCode) { + File d = new File(context.getFilesDir(), SNAPSHOT_ROOT + "/previews"); + d.mkdirs(); + return d; + } + + private File getSnapshotDir(String gameCode) { + File d = new File(context.getFilesDir(), SNAPSHOT_ROOT + "/data"); + d.mkdirs(); + return d; + } + + public void saveSnapshot(String gameCode, String gameTitle, Bitmap previewImage, byte[] data) { + byte[] previewImageBytes = Util.compressBitmapToByteArray(previewImage); + + String hash = Util.getHash(data); + + File previewsDir = getPreviewsDir(gameCode); + File snapshotsDir = getSnapshotDir(gameCode); + + File previewFile = new File(previewsDir, hash); + Util.writeCompressedFile(previewFile, previewImageBytes); + + File snapshotFile = new File(snapshotsDir, hash); + Util.writeCompressedFile(snapshotFile, data); + + this.dbHelper.insertSnapshot(gameCode, gameTitle, previewFile, snapshotFile); + } + + public void deleteSnapshot(Snapshot snapshot) { + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + File file = snapshot.getFile(); + db.delete(dbHelper.TABLE_NAME, "dataFile = '" + file.toString() + "'", null); + file.delete(); + } + + public ArrayList getAllSnapshots() { + return this.dbHelper.getEntries(); + } + + public ArrayList getByGameCode(String gameCode) { + return this.dbHelper.getEntries(gameCode); + } + public class SnapshotDBEntry { String gameCode; String gameTitle; @@ -88,7 +142,7 @@ public class SnapshotManager { File previewImageFile = new File(cursor.getString(4)); File dataFile = new File(cursor.getString(5)); - byte[] previewData = readCompressedFile(previewImageFile); + byte[] previewData = Util.readCompressedFile(previewImageFile); Bitmap previewBitmap = BitmapFactory.decodeByteArray(previewData, 0, previewData.length); arrayList.add(new Snapshot(dataFile, gameCode, gameTitle, previewBitmap, timestamp.getTime())); @@ -100,12 +154,12 @@ public class SnapshotManager { return arrayList; } - public ArrayList getAllEntries() { + public ArrayList getEntries() { return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + " ORDER BY timestamp DESC "); } - public ArrayList getAllEntries(String gameCode) { - return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + "where gameCode = " + gameCode + " ORDER BY timestamp DESC "); + public ArrayList getEntries(String gameCode) { + return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + " where gameCode = '" + gameCode + "' ORDER BY timestamp DESC "); } @Override @@ -113,115 +167,4 @@ public class SnapshotManager { } } - - private SnapshotManager(Context context) { - this.context = context; - this.dbHelper = new SnapshotDatabaseHelper(this.context); -// this.snapshotDB = context.openOrCreateDatabase("snapshots", Context.MODE_PRIVATE, null); - } - - public static SnapshotManager getInstance(Context context) { - if (instance == null) { - instance = new SnapshotManager(context); - } - return instance; - } - - private static String byteArrayToHexString(final byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02x", b & 0xff)); - } - return sb.toString(); - } - - private static String getHash(final byte[] bytes) { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - // impossible - Log.e("SnapshotManager", "SHA-256 algo not found"); - return null; - } - - md.update(bytes); - return byteArrayToHexString(md.digest()); - } - - private File getPreviewsDir(String gameCode) { - File d = new File(context.getFilesDir(), SNAPSHOT_ROOT + "/previews"); - d.mkdirs(); - return d; - } - - - private File getSnapshotDir(String gameCode) { - File d = new File(context.getFilesDir(), SNAPSHOT_ROOT + "/data"); - d.mkdirs(); - return d; - } - - public static void writeCompressedFile(File file, byte[] bytes) { - try { - FileOutputStream fos = new FileOutputStream(file); - GZIPOutputStream gos = new GZIPOutputStream(fos); - - gos.write(bytes); - gos.close(); - fos.close(); - } catch (Exception e) { - Log.e(TAG, "failed to write compressed file " + file.toString() + " error: " + e.getMessage()); - } - } - - public static byte[] readCompressedFile(File file) { - try { - byte[] buffer = new byte[8192]; - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - GZIPInputStream gis = new GZIPInputStream(new FileInputStream(file)); - - int len; - - while ( (len = gis.read(buffer, 0, 8192)) != -1) { - outputStream.write(buffer, 0, len); - } - gis.close(); - return outputStream.toByteArray(); - } catch (Exception e) { - Log.e(TAG, "failed to read compressed file " + file.toString() + " error: " + e.getMessage()); - return null; - } - } - - public static byte[] compressBitmapToByteArray(Bitmap bitmap) { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream); - return byteArrayOutputStream.toByteArray(); - } - - public void saveSnapshot(String gameCode, String gameTitle, Bitmap previewImage, byte[] data) { - byte[] previewImageBytes = compressBitmapToByteArray(previewImage); - - String hash = getHash(data); - - File previewsDir = getPreviewsDir(gameCode); - File snapshotsDir = getSnapshotDir(gameCode); - - File previewFile = new File(previewsDir, hash); - writeCompressedFile(previewFile, previewImageBytes); - - File snapshotFile = new File(snapshotsDir, hash); - writeCompressedFile(snapshotFile, data); - - this.dbHelper.insertSnapshot(gameCode, gameTitle, previewFile, snapshotFile); - } - - public ArrayList getAllSnapshots() { - return this.dbHelper.getAllEntries(); - } - - public ArrayList getByGameCode(String gameCode) { - return this.dbHelper.getAllEntries(gameCode); - } } diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulationThread.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulationThread.java new file mode 100644 index 0000000..3434be5 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulationThread.java @@ -0,0 +1,72 @@ +package com.mrmichel.rustdroid_emu.ui; + +import com.mrmichel.rustdroid_emu.core.Emulator; + +public class EmulationThread extends Thread { + + public static final long NANOSECONDS_PER_MILLISECOND = 1000000; + public static final long FRAME_TIME = 1000000000 / 60; + + private Emulator emulator; + private ScreenView screenView; + + private boolean turbo; + private boolean running; + private boolean stopping; + + public EmulationThread(Emulator emulator, ScreenView screenView) { + this.emulator = emulator; + this.screenView = screenView; + this.running = true; + } + + public void setStopping(boolean stopping) { + this.stopping = stopping; + } + + public void pauseEmulation() { + running = false; + } + + public void resumeEmulation() { + running = true; + } + + public void setTurbo(boolean turbo) { + this.turbo = turbo; + } + + public boolean isTurbo() { return turbo; } + + @Override + public void run() { + super.run(); + + // wait until renderer is ready + while (!screenView.getRenderer().isReady()); + + while (!stopping) { + if (running) { + long startTimer = System.nanoTime(); + emulator.runFrame(); + if (!turbo) { + 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) { + + } + } + } + + screenView.updateFrame(emulator.getFrameBuffer()); + } + } + } + + +} 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 index d802ece..97b6700 100644 --- 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 @@ -1,7 +1,9 @@ package com.mrmichel.rustdroid_emu.ui; +import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.media.AudioFormat; import android.media.AudioManager; @@ -9,57 +11,74 @@ import android.media.AudioTrack; import android.net.Uri; import android.os.Build; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - import android.util.Log; +import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; -import android.widget.Switch; +import android.widget.CompoundButton; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; + import com.mrmichel.rustboyadvance.EmulatorBindings; +import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.Util; import com.mrmichel.rustdroid_emu.core.AudioThread; import com.mrmichel.rustdroid_emu.core.Emulator; import com.mrmichel.rustdroid_emu.core.Keypad; -import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.core.RomManager; +import com.mrmichel.rustdroid_emu.core.Snapshot; import com.mrmichel.rustdroid_emu.core.SnapshotManager; -import com.mrmichel.rustdroid_emu.ui.snapshots.ChosenSnapshot; -import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotActivity; +import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotPickerActivity; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.InputStream; public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { private static final String TAG = "EmulatorActivty"; + private static final String TAG_EMULATOR_STATE = "EmulatorStateFragment"; + private static final int LOAD_ROM_REQUESTCODE = 123; private static final int LOAD_SNAPSHOT_REQUESTCODE = 124; private static int SAMPLE_RATE_HZ = 44100; + private Menu menu; + + private RomManager.RomMetadataEntry romMetadata; private byte[] bios; - private Emulator emulator = null; - private EmulationRunnable runnable; - private Thread emulationThread; + private EmulationThread emulationThread; private AudioThread audioThread; private AudioTrack audioTrack; private byte[] on_resume_saved_state = null; - private boolean turboMode = false; - private GbaScreenView gbaScreenView; + + private Emulator emulator; + private ScreenView screenView; + private CompoundButton turboButton; + + private boolean isEmulatorRunning() { + return emulator.isOpen() && emulationThread != null; + } @Override public void onClick(View v) { if (v.getId() == R.id.tbTurbo) { - Switch tbTurbo = (Switch) findViewById(R.id.tbTurbo); - this.turboMode = tbTurbo.isChecked(); + if (!isEmulatorRunning()) { + return; + } + emulationThread.setTurbo(((CompoundButton) findViewById(R.id.tbTurbo)).isChecked()); } } @@ -98,65 +117,125 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL 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); + emulator.keypad.onKeyDown(key); } else if (action == MotionEvent.ACTION_UP) { v.setPressed(false); - this.emulator.keypad.onKeyUp(key); + emulator.keypad.onKeyUp(key); + } else if (action == MotionEvent.ACTION_OUTSIDE) { + v.setPressed(false); + emulator.keypad.onKeyUp(key); } } - return action == MotionEvent.ACTION_DOWN; + + return true; } - 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(); + public Keypad.Key keyCodeToGbaKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + return Keypad.Key.Up; + case KeyEvent.KEYCODE_DPAD_DOWN: + return Keypad.Key.Down; + case KeyEvent.KEYCODE_DPAD_LEFT: + return Keypad.Key.Left; + case KeyEvent.KEYCODE_DPAD_RIGHT: + return Keypad.Key.Right; + case KeyEvent.KEYCODE_Z: + return Keypad.Key.ButtonB; + case KeyEvent.KEYCODE_X: + return Keypad.Key.ButtonA; + case KeyEvent.KEYCODE_A: + return Keypad.Key.ButtonL; + case KeyEvent.KEYCODE_S: + return Keypad.Key.ButtonR; + case KeyEvent.KEYCODE_DEL: + return Keypad.Key.Select; + case KeyEvent.KEYCODE_COMMA: + return Keypad.Key.Start; + } + return null; + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (!isEmulatorRunning()) { + return false; + } + Keypad.Key key = keyCodeToGbaKey(keyCode); + Log.d(TAG, "onKeyLongPress(: keyCode = " + keyCode + " GBAKey:" + key); + if (null != key) { + this.emulator.keypad.onKeyDown(key); + return false; + } else { + return super.onKeyDown(keyCode, event); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (!isEmulatorRunning()) { + return false; + } + Keypad.Key key = keyCodeToGbaKey(keyCode); + Log.d(TAG, "onKeyDown: keyCode = " + keyCode + " GBAKey:" + key); + if (null != key) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + this.emulator.keypad.onKeyDown(key); + break; + case KeyEvent.ACTION_UP: + this.emulator.keypad.onKeyUp(key); + break; + } + return event.getAction() == KeyEvent.ACTION_DOWN; + } else { + return super.onKeyDown(keyCode, event); + } } @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); - } - } +// 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"); +// Util.showAlertDiaglogAndExit(this, e); +// } +// } if (requestCode == LOAD_SNAPSHOT_REQUESTCODE) { - byte[] state = ChosenSnapshot.takeSnapshot().load(); - if (emulator.isOpen()) { - try { - emulator.loadState(state); - } catch (EmulatorBindings.NativeBindingException e) { - showAlertDiaglogAndExit(e); - } + Snapshot pickedSnapshot = SnapshotPickerActivity.obtainPickedSnapshot(); + + Toast.makeText(this, "Loading snapshot from " + pickedSnapshot.getTimestamp(), Toast.LENGTH_LONG).show(); + + boolean emulatorWasRunning = isEmulatorRunning(); + + pauseEmulation(); + try { + emulator.loadState(pickedSnapshot.load()); + } catch (Exception e) { + Util.showAlertDiaglogAndExit(this, e); + } + resumeEmulation(); + + if (!emulatorWasRunning) { + createThreads(); } } } else { @@ -164,75 +243,90 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL } } - 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; - } + private void killThreads() { if (audioThread != null) { audioThread.setStopping(true); try { audioThread.join(); } catch (InterruptedException e) { Log.e(TAG, "audio thread join interrupted"); - }; + } audioThread = null; } - - if (emulator.isOpen()) { - emulator.close(); + if (emulationThread != null) { + try { + emulationThread.setStopping(true); + emulationThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "emulation thread join interrupted"); + } + emulationThread = null; } + } - 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(); - + private void createThreads() { + emulationThread = new EmulationThread(emulator, screenView); audioThread = new AudioThread(audioTrack, emulator); + + emulationThread.setTurbo(turboButton.isChecked()); + + emulationThread.start(); audioThread.start(); } - public void loadRomButton(View v) { - if (runnable != null) { - runnable.pauseEmulation(); - } + public void onRomLoaded(byte[] rom, String savePath) { +// killThreads(); +// +// try { +// emulator.open(bios, rom, savePath); +// } catch (EmulatorBindings.NativeBindingException e) { +// Util.showAlertDiaglogAndExit(this, e); +// } +// +// createThreads(); + } + + public void doLoadRom() { 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 onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + if (!isEmulatorRunning()) { + return; + } + // save the emulator state + try { + byte[] savedState = emulator.saveState(); + + File saveFile = new File(getCacheDir(), "saved_state"); + FileOutputStream fis = new FileOutputStream(saveFile); + + fis.write(savedState); + + fis.close(); + + outState.putString("saveFile", saveFile.getPath()); + + outState.putBoolean("turbo", emulationThread.isTurbo()); + + } catch (Exception e) { + Util.showAlertDiaglogAndExit(this, e); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_emulator); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); - - - this.bios = getIntent().getByteArrayExtra("bios"); - this.emulator = new Emulator(); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); if (Build.VERSION.SDK_INT >= 23) { AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder() @@ -259,7 +353,83 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL } this.audioTrack.play(); - this.gbaScreenView = findViewById(R.id.gba_view); + 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.dpad_layout).setOnTouchListener(this); + + turboButton = findViewById(R.id.tbTurbo); + turboButton.setOnClickListener(this); + + this.bios = getIntent().getByteArrayExtra("bios"); + + this.screenView = findViewById(R.id.gba_view); + this.emulator = new Emulator(); + + final String saveFilePath; + + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this /* Activity context */); + Boolean skipBios = sharedPreferences.getBoolean("skip_bios", false); + + if (null != savedInstanceState && (saveFilePath = savedInstanceState.getString("saveFile")) != null) { + final EmulatorActivity thisActivity = this; + + // busy wait until surface view is ready + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + byte[] buffer = new byte[4096]; + File saveFile = new File(saveFilePath); + FileInputStream fis = new FileInputStream(saveFile); + + int read = 0; + while ((read = fis.read(buffer)) != -1) { + outputStream.write(buffer); + } + + fis.close(); + + saveFile.delete(); + + byte[] savedState = outputStream.toByteArray(); + emulator.openSavedState(savedState); + + createThreads(); + + boolean turbo = savedInstanceState.getBoolean("turbo"); + + turboButton.setPressed(turbo); + emulationThread.setTurbo(turbo); + + } catch (Exception e) { + Util.showAlertDiaglogAndExit(thisActivity, e); + } + + } else { + int romId = getIntent().getIntExtra("romId", -1); + if (-1 != romId) { + this.romMetadata = RomManager.getInstance(this).getRomMetadata(romId); + + byte[] romData; + try { + romData = Util.readFile(romMetadata.getRomFile()); + this.emulator.open(bios, romData, romMetadata.getBackupFile().getAbsolutePath(), skipBios); + } catch (Exception e) { + Util.showAlertDiaglogAndExit(this, e); + return; + } + + createThreads(); + } + } } @Override @@ -271,48 +441,73 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - return super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.action_load_rom: + doLoadRom(); + return true; + case R.id.action_view_snapshots: + doViewSnapshots(); + return true; + case R.id.action_save_snapshot: + doSaveSnapshot(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.action_save_snapshot).setEnabled(isEmulatorRunning()); + return super.onPrepareOptionsMenu(menu); + } + + private void pauseEmulation() { + if (null != emulationThread) { + emulationThread.pauseEmulation(); + } + } + + private void resumeEmulation() { + if (null != emulationThread) { + emulationThread.resumeEmulation(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + audioTrack.stop(); + pauseEmulation(); + killThreads(); } @Override protected void onPause() { super.onPause(); audioTrack.stop(); - if (emulator.isOpen()) { - if (runnable != null) { - runnable.pauseEmulation(); - } - Log.d(TAG, "onPause - saving emulator state"); -// try { -// on_resume_saved_state = emulator.saveState(); -// } catch (EmulatorBindings.NativeBindingException e) { -// showAlertDiaglogAndExit(e); -// } - } + pauseEmulation(); + screenView.onPause(); } @Override protected void onResume() { super.onResume(); - if (emulator.isOpen()) { - Log.d(TAG, "onResume - loading emulator state"); -// try { -// emulator.loadState(on_resume_saved_state); -// } catch (EmulatorBindings.NativeBindingException e) { -// showAlertDiaglogAndExit(e); -// } -// on_resume_saved_state = null; - if (runnable != null) { - runnable.resumeEmulation(); - } - } + screenView.onResume(); + resumeEmulation(); audioTrack.play(); } - public void onSaveSnapshot(View v) { + public void doSaveSnapshot() { + if (!isEmulatorRunning()) { + Toast.makeText(this, "No game is running!", Toast.LENGTH_LONG).show(); + return; + } + SnapshotManager snapshotManager = SnapshotManager.getInstance(this); - runnable.pauseEmulation(); + pauseEmulation(); try { String gameCode = emulator.getGameCode(); String gameTitle = emulator.getGameTitle(); @@ -324,73 +519,48 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL } catch (EmulatorBindings.NativeBindingException e) { Log.e(TAG, e.toString()); - showAlertDiaglogAndExit(e); + Util.showAlertDiaglogAndExit(this, e); } finally { - runnable.resumeEmulation(); + resumeEmulation(); } } - - public void onViewSnapshots(View v) { - Intent intent = new Intent(this, SnapshotActivity.class); + public void doViewSnapshots() { + Intent intent = new Intent(this, SnapshotPickerActivity.class); + if (emulator.isOpen()) { + intent.putExtra("gameCode", emulator.getGameCode()); + } startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE); } - private class EmulationRunnable implements Runnable { + @Override + public void onBackPressed() { + boolean emulatorIsRunning = isEmulatorRunning(); - 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(); + if (!emulatorIsRunning) { + super.onBackPressed(); + return; } - private void emulate() { - long startTimer = System.nanoTime(); - emulator.runFrame(); - 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) { - + new AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle("Closing Emulator") + .setCancelable(false) + .setMessage("Are you sure you want to close the emulator?") + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + EmulatorActivity.super.onBackPressed(); } - } - } - - emulatorActivity.gbaScreenView.updateFrame(emulator.getFrameBuffer()); - } - - public void pauseEmulation() { - running = false; - } - - public void resumeEmulation() { - running = true; - } - - public void stop() { - stopping = true; - } - - @Override - public void run() { - while (!stopping) { - if (running) { - emulate(); - } - } - } + }) + .setNeutralButton("Yes - but save snapshot", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + doSaveSnapshot(); + EmulatorActivity.super.onBackPressed(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); } } diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java deleted file mode 100644 index b8842e4..0000000 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.mrmichel.rustdroid_emu.ui; - -import android.os.Bundle; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; -import com.mrmichel.rustdroid_emu.R; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; - -import android.view.View; - -public class RomListActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_rom_list); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show(); - } - }); - } -} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenRenderer.java similarity index 85% rename from AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java rename to AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenRenderer.java index d6719f8..d0a3bce 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/GbaScreenView.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenRenderer.java @@ -1,12 +1,9 @@ package com.mrmichel.rustdroid_emu.ui; -import android.content.Context; import android.graphics.Bitmap; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.opengl.GLUtils; -import android.util.AttributeSet; -import android.view.SurfaceHolder; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -15,8 +12,10 @@ import java.nio.FloatBuffer; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; -public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Renderer { - ScreenTexture texture; +public class ScreenRenderer implements GLSurfaceView.Renderer { + + private ScreenTexture texture; + private boolean ready = false; /** * Private class to manage the screen texture rendering @@ -72,7 +71,8 @@ public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Render "uniform sampler2D s_texture; \n" + "void main() \n" + "{ \n" + - " gl_FragColor = texture2D( s_texture, v_texCoord );\n" + + " vec4 color = texture2D( s_texture, v_texCoord ); \n" + + " gl_FragColor = color; \n" + "} \n"; @@ -191,34 +191,20 @@ public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Render } } - public GbaScreenView(Context context) { - super(context); - init(); - } - - public GbaScreenView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - private void init() { - this.setEGLContextClientVersion(2); - this.setPreserveEGLContextOnPause(true); - this.setRenderer(this); - this.setRenderMode(RENDERMODE_WHEN_DIRTY); - } - - public void updateFrame(int[] frameBuffer) { + public void updateTexture(int[] frameBuffer) { this.texture.update(frameBuffer); - requestRender(); } + public void initTextureIfNotInitialized() { + if (this.texture == null) { + this.texture = new ScreenTexture(); + } + } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - texture = new ScreenTexture(); - - getHolder().setKeepScreenOn(true); + initTextureIfNotInitialized(); + ready = true; } @Override @@ -231,10 +217,7 @@ public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Render this.texture.render(); } - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - holder.setKeepScreenOn(false); - this.texture.destroy(); - super.surfaceDestroyed(holder); + public boolean isReady() { + return ready; } } diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenView.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenView.java new file mode 100644 index 0000000..a4a9a86 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/ScreenView.java @@ -0,0 +1,37 @@ +package com.mrmichel.rustdroid_emu.ui; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; + +public class ScreenView extends GLSurfaceView { + private ScreenRenderer mRenderer; + + public ScreenView(Context context) { + super(context); + init(); + } + + public ScreenView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + this.setEGLContextClientVersion(2); + this.setPreserveEGLContextOnPause(true); + + mRenderer = new ScreenRenderer(); + this.setRenderer(mRenderer); + this.setRenderMode(RENDERMODE_WHEN_DIRTY); + } + + public void updateFrame(int[] frameBuffer) { + mRenderer.updateTexture(frameBuffer); + requestRender(); + } + + public ScreenRenderer getRenderer() { + return mRenderer; + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsActivity.java new file mode 100644 index 0000000..36df4e2 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsActivity.java @@ -0,0 +1,19 @@ +package com.mrmichel.rustdroid_emu.ui; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import com.mrmichel.rustdroid_emu.R; + +public class SettingsActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, new SettingsFragment()) + .commit(); + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsFragment.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsFragment.java new file mode 100644 index 0000000..4e37e31 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SettingsFragment.java @@ -0,0 +1,17 @@ +package com.mrmichel.rustdroid_emu.ui; + +import android.os.Bundle; + +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceFragmentCompat; + +import com.mrmichel.rustdroid_emu.R; + + +public class SettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.app_preferences, rootKey); + } +} 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 index 7dfeb7a..968df47 100644 --- 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 @@ -1,12 +1,5 @@ package com.mrmichel.rustdroid_emu.ui; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - import android.Manifest; import android.app.ActivityManager; import android.content.Context; @@ -19,7 +12,15 @@ import android.os.Bundle; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.ui.library.RomListActivity; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -49,7 +50,7 @@ public class SplashActivity extends AppCompatActivity { } private void checkOpenGLES20() { - ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo(); if (configurationInfo.reqGlEsVersion >= 0x20000) { // Supported @@ -81,7 +82,7 @@ public class SplashActivity extends AppCompatActivity { // No explanation needed; request the permission ActivityCompat.requestPermissions(this , - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_CODE); } else { // Permission has already been granted @@ -90,7 +91,7 @@ public class SplashActivity extends AppCompatActivity { } } - private void cacheBiosInAppFiles(byte[] bios) throws FileNotFoundException, IOException { + private void cacheBiosInAppFiles(byte[] bios) throws IOException { FileOutputStream fos = openFileOutput("gba_bios.bin", MODE_PRIVATE); fos.write(bios); fos.close(); @@ -110,7 +111,7 @@ public class SplashActivity extends AppCompatActivity { cacheBiosInAppFiles(bios); - initEmulator(bios); + startLibraryActivity(bios); } catch (Exception e) { Log.e(TAG, "can't open bios file"); this.finishAffinity(); @@ -126,7 +127,7 @@ public class SplashActivity extends AppCompatActivity { FileInputStream fis = openFileInput("gba_bios.bin"); byte[] bios = new byte[fis.available()]; fis.read(bios); - initEmulator(bios); + startLibraryActivity(bios); } catch (FileNotFoundException e) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.setType("*/*"); @@ -138,9 +139,10 @@ public class SplashActivity extends AppCompatActivity { } } - private void initEmulator(byte[] bios) { - Intent intent = new Intent(this, EmulatorActivity.class); + private void startLibraryActivity(byte[] bios) { + Intent intent = new Intent(this, RomListActivity.class); intent.putExtra("bios", bios); startActivity(intent); + finish(); } } diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListActivity.java new file mode 100644 index 0000000..c78d319 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListActivity.java @@ -0,0 +1,223 @@ +package com.mrmichel.rustdroid_emu.ui.library; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.webkit.MimeTypeMap; +import android.widget.AdapterView; +import android.widget.GridView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.documentfile.provider.DocumentFile; + +import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.Util; +import com.mrmichel.rustdroid_emu.core.RomManager; +import com.mrmichel.rustdroid_emu.ui.SettingsActivity; +import com.mrmichel.rustdroid_emu.ui.SettingsFragment; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class RomListActivity extends AppCompatActivity { + + private static final String TAG = "RomListActivity"; + + private static final int REQUEST_IMPORT_ROM = 100; + private static final int REQUEST_IMPORT_DIR = 101; + private static final int REQUEST_SET_IMAGE = 102; + + private static String[] ALLOWED_EXTENSIONS = {"gba", "zip", "bin"}; + + private GridView mGridView; + private RomListItemAdapter itemAdapter; + + private RomManager.RomMetadataEntry selectedEntry; + + private byte[] bios; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_rom_list); + + this.bios = getIntent().getByteArrayExtra("bios"); + + mGridView = findViewById(R.id.gridview_rom_list); + + final RomManager romManager = RomManager.getInstance(this); + + ArrayList entries = romManager.getAllRomMetaData(); + + itemAdapter = new RomListItemAdapter(this, entries); + mGridView.setAdapter(itemAdapter); + + final Context context = this; + mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + RomManager.RomMetadataEntry entry = itemAdapter.getItem(position); + romManager.updateLastPlayed(entry.getId()); + Util.startEmulator(context, bios, entry.getId()); + } + }); + + registerForContextMenu(mGridView); + } + + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (v.getId() == R.id.gridview_rom_list) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_context_rom, menu); + } + } + + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); + + RomManager romManager = RomManager.getInstance(this); + + RomManager.RomMetadataEntry entry = itemAdapter.getItem(menuInfo.position); + + selectedEntry = entry; + + switch (item.getItemId()) { + case R.id.action_play: + romManager.updateLastPlayed(entry.getId()); + Util.startEmulator(this, this.bios, entry.getId()); + return true; + case R.id.action_delete: + romManager.deleteRomMetadata(itemAdapter.getItem(menuInfo.position)); + return true; + case R.id.action_set_screenshot: + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("image/*"); + intent.putExtra("romId", entry.getId()); + startActivityForResult(intent, REQUEST_SET_IMAGE); + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu_rom_list, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_import_rom: + doImportRom(); + case R.id.action_import_directory: + doImportDirectory(); + return true; + case R.id.action_settings: + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + default: + return super.onOptionsItemSelected(item); + } + } + + String getFileExtension(String name) { + if (name == null) { + return ""; + } + int i = name.lastIndexOf('.'); + String ext = i > 0 ? name.substring(i + 1) : ""; + return ext; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + ContentResolver contentResolver = getContentResolver(); + RomManager romManager = RomManager.getInstance(this); + switch (requestCode) { + case REQUEST_IMPORT_ROM: + Uri uri = data.getData(); + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + + romManager.importRom(DocumentFile.fromSingleUri(this, uri)); + + break; + case REQUEST_IMPORT_DIR: + + Uri treeUri = data.getData(); + + contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); + + for (DocumentFile file : pickedDir.listFiles()) { + + String extension = getFileExtension(file.getName()); + if (Arrays.asList(ALLOWED_EXTENSIONS).contains(extension)) { + Log.d(TAG, "Importing ROM " + file.getName() + " with size " + file.length() + " and type: " + extension); + romManager.importRom(file); + } + } + + break; + case REQUEST_SET_IMAGE: + int romId = selectedEntry.getId(); + + Bitmap bitmap; + try { + bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), data.getData()); + + } + catch (Exception e) { + Util.showAlertDiaglogAndExit(this, e); + return; + } + + Log.d(TAG, "found bitmap"); + romManager.updateScreenshot(romId, bitmap); + + } + + mGridView.setAdapter(new RomListItemAdapter(this, romManager.getAllRomMetaData())); + mGridView.invalidate(); + + } else { + Log.e(TAG, "got error for request code " + requestCode); + } + } + + void doImportRom() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + Log.d(TAG, "pressed import rom"); + Intent chooser = Intent.createChooser(intent, "choose GBA rom file to import"); + startActivityForResult(chooser, REQUEST_IMPORT_ROM); + } + + void doImportDirectory() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, REQUEST_IMPORT_DIR); + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListItemAdapter.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListItemAdapter.java new file mode 100644 index 0000000..448bcef --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/library/RomListItemAdapter.java @@ -0,0 +1,61 @@ +package com.mrmichel.rustdroid_emu.ui.library; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.core.RomManager.RomMetadataEntry; + +import java.util.ArrayList; + +public class RomListItemAdapter extends ArrayAdapter { + + Context context; + ArrayList items; + + public RomListItemAdapter(Context context, ArrayList romEntries) { + super(context, 0, romEntries); + this.context = context; + this.items = romEntries; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + RomMetadataEntry item = getItem(position); + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.rom_item, parent, false); + } + + ImageView screenshotImageView = convertView.findViewById(R.id.imageview_screenshot); + + Bitmap screenshot = item.getScreenshot(); + if (screenshot != null) { + screenshotImageView.setImageBitmap(screenshot); + } else { + screenshotImageView.setImageBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.icon)); + } + + + TextView tvTitle = convertView.findViewById(R.id.textview_game_title); + tvTitle.setText(item.getName()); + + return convertView; + } +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ChosenSnapshot.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ChosenSnapshot.java deleted file mode 100644 index 448472e..0000000 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ChosenSnapshot.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.mrmichel.rustdroid_emu.ui.snapshots; - -import com.mrmichel.rustdroid_emu.core.Snapshot; - -/** - * static class to transfer big byte arrays between activities - */ -public class ChosenSnapshot { - - static Snapshot snapshot; - - public static void setSnapshot(Snapshot snapshot) { - ChosenSnapshot.snapshot = snapshot; - } - - public static Snapshot takeSnapshot() { - Snapshot result = ChosenSnapshot.snapshot; - ChosenSnapshot.snapshot = null; - return result; - } -} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ISnapshotListener.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ISnapshotListener.java new file mode 100644 index 0000000..0df1e87 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ISnapshotListener.java @@ -0,0 +1,8 @@ +package com.mrmichel.rustdroid_emu.ui.snapshots; + +import com.mrmichel.rustdroid_emu.core.Snapshot; + +public interface ISnapshotListener { + + public void onSnapshotClicked(Snapshot snapshot); +} diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java deleted file mode 100644 index 0549cd5..0000000 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.mrmichel.rustdroid_emu.ui.snapshots; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AppCompatActivity; - -import android.view.View; -import android.widget.AdapterView; -import android.widget.GridView; -import android.widget.Toast; - -import com.mrmichel.rustdroid_emu.R; -import com.mrmichel.rustdroid_emu.core.Snapshot; -import com.mrmichel.rustdroid_emu.core.SnapshotManager; - -import java.util.ArrayList; - -public class SnapshotActivity extends AppCompatActivity { - - private ArrayList snapshots; - public static final String EXTRA_GAME_CODE = "GAME_CODE"; - - public void onChosenSnapshot(Snapshot snapshot) { - Toast.makeText(this, "loading snapshot", Toast.LENGTH_SHORT).show(); - Intent intent = new Intent(); - setResult(RESULT_OK, intent); - ChosenSnapshot.setSnapshot(snapshot); - SnapshotActivity.this.finish(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_snapshot); - - SnapshotManager manager = SnapshotManager.getInstance(this); - - String gameCode = getIntent().getStringExtra(EXTRA_GAME_CODE); - - if (gameCode == null) { - snapshots = manager.getAllSnapshots(); - } else { - snapshots = manager.getByGameCode(gameCode); - } - - SnapshotItemAdapter adapter = new SnapshotItemAdapter(this, snapshots); - - GridView view = findViewById(R.id.gridview_snapshots); - view.setAdapter(adapter); - view.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final Snapshot snapshot = snapshots.get(position); - onChosenSnapshot(snapshot); - } - }); - } -} - diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotListFragment.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotListFragment.java new file mode 100644 index 0000000..0b7f337 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotListFragment.java @@ -0,0 +1,116 @@ +package com.mrmichel.rustdroid_emu.ui.snapshots; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.core.Snapshot; +import com.mrmichel.rustdroid_emu.core.SnapshotManager; + +import java.util.ArrayList; + +public class SnapshotListFragment extends Fragment { + + private static final String TAG = "SnapshotListFragment"; + + private GridView mGridView; + + private ArrayList snapshots; + + private ISnapshotListener mListener; + + public SnapshotListFragment() { + super(); + mListener = new ISnapshotListener() { + @Override + public void onSnapshotClicked(Snapshot snapshot) { + Log.d(TAG, "stub onSnapshotClicked"); + } + }; + } + + public SnapshotListFragment(ISnapshotListener listener) { + super(); + mListener = listener; + } + + public static SnapshotListFragment newInstance(ISnapshotListener listener) { + return new SnapshotListFragment(listener); + } + + @Override + public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, @Nullable ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (v.getId() == R.id.gridview_snapshots) { + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.menu_context_snapshot, menu); + } + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + + Snapshot snapshot = snapshots.get(menuInfo.position); + switch (item.getItemId()) { + case R.id.action_delete: + SnapshotManager.getInstance(getContext()).deleteSnapshot(snapshot); + snapshots.remove(menuInfo.position); + + SnapshotItemAdapter adapter = new SnapshotItemAdapter(getContext(), snapshots); + mGridView.setAdapter(adapter); + mGridView.invalidate(); + + return true; + default: + return super.onContextItemSelected(item); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.snapshot_list_fragment, container, false); + } + + @Override + public void onStart() { + super.onStart(); + + Bundle args = getArguments(); + + SnapshotManager manager = SnapshotManager.getInstance(getContext()); + + String gameCode; + if (args != null && (gameCode = args.getString("gameCode")) != null) { + snapshots = manager.getByGameCode(gameCode); + } else { + snapshots = manager.getAllSnapshots(); + } + + mGridView = getActivity().findViewById(R.id.gridview_snapshots); + SnapshotItemAdapter adapter = new SnapshotItemAdapter(getContext(), snapshots); + mGridView.setAdapter(adapter); + mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Snapshot snapshot = snapshots.get(position); + mListener.onSnapshotClicked(snapshot); + } + }); + registerForContextMenu(mGridView); + } +} + diff --git a/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotPickerActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotPickerActivity.java new file mode 100644 index 0000000..2e5add6 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotPickerActivity.java @@ -0,0 +1,39 @@ +package com.mrmichel.rustdroid_emu.ui.snapshots; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; + +import com.mrmichel.rustdroid_emu.R; +import com.mrmichel.rustdroid_emu.core.Snapshot; + +public class SnapshotPickerActivity extends AppCompatActivity implements ISnapshotListener { + + static Snapshot pickedSnapshot; + + public static Snapshot obtainPickedSnapshot() { + Snapshot ret = pickedSnapshot; + pickedSnapshot = null; + return ret; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.snapshot_picker_activity); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, SnapshotListFragment.newInstance(this)) + .commitNow(); + } + } + + @Override + public void onSnapshotClicked(Snapshot snapshot) { + Intent data = new Intent(); + pickedSnapshot = snapshot; + setResult(RESULT_OK, data); + finish(); + } +} diff --git a/AndroidApp/app/src/main/res/drawable/round_button.xml b/AndroidApp/app/src/main/res/drawable/round_button.xml index 0e00740..d4f184d 100644 --- a/AndroidApp/app/src/main/res/drawable/round_button.xml +++ b/AndroidApp/app/src/main/res/drawable/round_button.xml @@ -1,28 +1,5 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/res/layout-land/activity_emulator.xml b/AndroidApp/app/src/main/res/layout-land/activity_emulator.xml new file mode 100644 index 0000000..fdd865b --- /dev/null +++ b/AndroidApp/app/src/main/res/layout-land/activity_emulator.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/res/layout-land/content_emulator.xml b/AndroidApp/app/src/main/res/layout-land/content_emulator.xml new file mode 100644 index 0000000..9b7a511 --- /dev/null +++ b/AndroidApp/app/src/main/res/layout-land/content_emulator.xml @@ -0,0 +1,122 @@ + + + + + + + + + +