From e75c3289cd9daf743cb6aaeb13265bf4495e495e Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Fri, 28 Feb 2020 14:28:58 +0200 Subject: [PATCH] android: WIP snapshot manager Former-commit-id: 7b39e4ac1ea86262a0a1ccf95639382f20801d92 --- AndroidApp/app/src/main/AndroidManifest.xml | 13 +- .../rustboyadvance/EmulatorBindings.java | 21 +- .../mrmichel/rustdroid_emu/core/Emulator.java | 18 +- .../mrmichel/rustdroid_emu/core/Snapshot.java | 50 ++++ .../rustdroid_emu/core/SnapshotManager.java | 230 ++++++++++++++++++ .../rustdroid_emu/ui/EmulatorActivity.java | 109 ++++++--- .../rustdroid_emu/ui/RomListActivity.java | 32 +++ .../rustdroid_emu/ui/SplashActivity.java | 2 +- .../ui/snapshots/ChosenSnapshot.java | 21 ++ .../ui/snapshots/SnapshotActivity.java | 69 ++++++ .../ui/snapshots/SnapshotItemAdapter.java | 66 +++++ .../src/main/res/layout/activity_emulator.xml | 27 +- .../src/main/res/layout/activity_rom_list.xml | 47 ++++ .../src/main/res/layout/activity_snapshot.xml | 59 +++++ .../src/main/res/layout/content_emulator.xml | 18 +- .../src/main/res/layout/content_rom_list.xml | 17 ++ .../app/src/main/res/layout/snapshot_item.xml | 38 +++ ...{main_activity.xml => splash_activity.xml} | 0 .../app/src/main/res/menu/menu_emulator.xml | 20 ++ .../app/src/main/res/menu/menu_rom_list.xml | 10 + .../app/src/main/res/menu/menu_snapshot.xml | 10 + AndroidApp/app/src/main/res/values/dimens.xml | 2 + .../app/src/main/res/values/strings.xml | 95 ++++++++ 23 files changed, 920 insertions(+), 54 deletions(-) create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java create 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/SnapshotActivity.java create mode 100644 AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotItemAdapter.java create mode 100644 AndroidApp/app/src/main/res/layout/activity_rom_list.xml create mode 100644 AndroidApp/app/src/main/res/layout/activity_snapshot.xml create mode 100644 AndroidApp/app/src/main/res/layout/content_rom_list.xml create mode 100644 AndroidApp/app/src/main/res/layout/snapshot_item.xml rename AndroidApp/app/src/main/res/layout/{main_activity.xml => splash_activity.xml} (100%) create mode 100644 AndroidApp/app/src/main/res/menu/menu_emulator.xml create mode 100644 AndroidApp/app/src/main/res/menu/menu_rom_list.xml create mode 100644 AndroidApp/app/src/main/res/menu/menu_snapshot.xml diff --git a/AndroidApp/app/src/main/AndroidManifest.xml b/AndroidApp/app/src/main/AndroidManifest.xml index 617530f..e8c71fa 100644 --- a/AndroidApp/app/src/main/AndroidManifest.xml +++ b/AndroidApp/app/src/main/AndroidManifest.xml @@ -13,10 +13,17 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> + + - + android:theme="@style/AppTheme.NoActionBar" /> + 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 e1c3d27..be93759 100644 --- a/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustboyadvance/EmulatorBindings.java @@ -15,11 +15,12 @@ public class EmulatorBindings { * Open a new emulator context * @param bios bytearray of the GBA bios * @param rom bytearray of the rom to run + * @param frameBuffer frameBuffer render target * @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; + public static native long openEmulator(byte[] bios, byte[] rom, int[] frameBuffer, String save_name) throws NativeBindingException; /** * Make the emulator boot directly into the cartridge @@ -41,15 +42,25 @@ public class EmulatorBindings { * 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; + public static native void runFrame(long ctx, int[] frame_buffer); + + /** + * @param ctx + * @return The loaded ROM title + */ + public static native String getGameTitle(long ctx); + + /** + * @param ctx + * @return The loaded ROM game code + */ + public static native String getGameCode(long ctx); + /** * Sets the keystate * @param keyState - * @return non-zero value on failure */ public static native void setKeyState(long ctx, int keyState); 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 86df815..e2d26b9 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 @@ -1,5 +1,7 @@ package com.mrmichel.rustdroid_emu.core; +import android.graphics.Bitmap; + import com.mrmichel.rustboyadvance.EmulatorBindings; public class Emulator { @@ -29,7 +31,7 @@ public class Emulator { return frameBuffer; } - public synchronized void runFrame() throws EmulatorBindings.NativeBindingException { + public synchronized void runFrame() { EmulatorBindings.setKeyState(this.ctx, this.keypad.getKeyState()); EmulatorBindings.runFrame(this.ctx, this.frameBuffer); } @@ -39,18 +41,18 @@ public class Emulator { } - public byte[] saveState() throws EmulatorBindings.NativeBindingException { + public synchronized byte[] saveState() throws EmulatorBindings.NativeBindingException { return EmulatorBindings.saveState(this.ctx); } - public void loadState(byte[] state) throws EmulatorBindings.NativeBindingException { + public synchronized 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); + this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameBuffer, saveName); } public synchronized void close() { @@ -61,6 +63,14 @@ public class Emulator { } } + public String getGameCode() { + return EmulatorBindings.getGameCode(ctx); + } + + public String getGameTitle() { + return EmulatorBindings.getGameTitle(ctx); + } + public boolean isOpen() { return this.ctx != -1; } 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 new file mode 100644 index 0000000..251d844 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/Snapshot.java @@ -0,0 +1,50 @@ +package com.mrmichel.rustdroid_emu.core; + +import android.graphics.Bitmap; + +import java.io.ByteArrayOutputStream; +import java.io.File; + +public class Snapshot { + private Bitmap preview; + private String gameCode; + private String gameTitle; + private long timestamp; + private File file; + + public Snapshot(File file, String gameCode, String gameTitle, Bitmap preview) { + this.file = file; + this.gameCode = gameCode; + this.gameTitle = gameTitle; + this.preview = preview; + this.timestamp = System.currentTimeMillis(); + } + + public Snapshot(File file, String gameCode, String gameTitle, Bitmap preview, long timestamp) { + this.file = file; + this.gameCode = gameCode; + this.gameTitle = gameTitle; + this.preview = preview; + this.timestamp = timestamp; + } + + public String getGameCode() { + return gameCode; + } + + public String getGameTitle() { + return gameTitle; + } + + public long getTimestamp() { + return timestamp; + } + + public Bitmap getPreview() { + return preview; + } + + public byte[] load() { + return SnapshotManager.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 new file mode 100644 index 0000000..f17e555 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/core/SnapshotManager.java @@ -0,0 +1,230 @@ +package com.mrmichel.rustdroid_emu.core; + +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.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +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"; + + private static final String SNAPSHOT_ROOT = "snapshots"; + private static final String DB_NAME = "snapshots"; + + static SnapshotManager instance; + + private Context context; + + private SnapshotDatabaseHelper dbHelper; + + public class SnapshotDBEntry { + String gameCode; + String gameTitle; + File previewFile; + File snapshotFile; + Timestamp timestamp; + + public SnapshotDBEntry(String gameCode, File previewFile, File snapshotFile, Timestamp timestamp) { + this.gameCode = gameCode; + this.previewFile = previewFile; + this.snapshotFile = snapshotFile; + this.timestamp = timestamp; + } + } + + public class SnapshotDatabaseHelper extends SQLiteOpenHelper { + public static final String TABLE_NAME = "snapshot_table"; + private Context context; + + public SnapshotDatabaseHelper(Context context) { + super(context, DB_NAME, null, 1); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("create table " + TABLE_NAME + + " (id INTEGER PRIMARY KEY, gameCode TEXT, gameTitle TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, previewImageFile TEXT, dataFile TEXT)" + ); + } + + public void insertSnapshot(String gameCode, String gameTitle, File previewCacheFile, File snapshotDataFile) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("gameCode", gameCode); + values.put("gameTitle", gameTitle); + values.put("previewImageFile", previewCacheFile.getPath()); + values.put("dataFile", snapshotDataFile.getPath()); + db.insertOrThrow(TABLE_NAME, null, values); + db.close(); + } + + public ArrayList getEntriesByQuery(String query) { + ArrayList arrayList = new ArrayList<>(); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(query, null); + + if (cursor.moveToFirst()) { + do { + String gameCode = cursor.getString(1); + String gameTitle = cursor.getString(2); + Timestamp timestamp = Timestamp.valueOf(cursor.getString(3)); + File previewImageFile = new File(cursor.getString(4)); + File dataFile = new File(cursor.getString(5)); + + byte[] previewData = readCompressedFile(previewImageFile); + Bitmap previewBitmap = BitmapFactory.decodeByteArray(previewData, 0, previewData.length); + + arrayList.add(new Snapshot(dataFile, gameCode, gameTitle, previewBitmap, timestamp.getTime())); + } while (cursor.moveToNext()); + } + + cursor.close(); + db.close(); + return arrayList; + } + + public ArrayList getAllEntries() { + 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 "); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + } + + 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/EmulatorActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/EmulatorActivity.java index 71b45d4..4fc8563 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,25 +1,37 @@ package com.mrmichel.rustdroid_emu.ui; +import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; 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.ContextMenu; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.WindowManager; import android.widget.ImageView; -import android.widget.ToggleButton; +import android.widget.Switch; +import android.widget.Toast; 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 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 java.io.File; import java.io.InputStream; @@ -28,19 +40,20 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL private static final String TAG = "EmulatorActivty"; private static final int LOAD_ROM_REQUESTCODE = 123; + private static final int LOAD_SNAPSHOT_REQUESTCODE = 124; private byte[] bios; private Emulator emulator = null; private EmulationRunnable runnable; private Thread emulationThread; - private byte[] snapshot = null; + private byte[] on_resume_saved_state = 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); + Switch tbTurbo = (Switch) findViewById(R.id.tbTurbo); this.turboMode = tbTurbo.isChecked(); } } @@ -131,6 +144,16 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL showAlertDiaglogAndExit(e); } } + if (requestCode == LOAD_SNAPSHOT_REQUESTCODE) { + byte[] state = ChosenSnapshot.takeSnapshot().load(); + if (emulator.isOpen()) { + try { + emulator.loadState(state); + } catch (EmulatorBindings.NativeBindingException e) { + showAlertDiaglogAndExit(e); + } + } + } } else { Log.e(TAG, "got error for request code " + requestCode); } @@ -187,11 +210,26 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL 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.screen = findViewById(R.id.gbaMockImageView); this.emulator = new Emulator(); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu_emulator, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + return super.onOptionsItemSelected(item); + } + @Override protected void onPause() { super.onPause(); @@ -200,31 +238,58 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL runnable.pauseEmulation(); } Log.d(TAG, "onPause - saving emulator state"); - try { - snapshot = emulator.saveState(); - } catch (EmulatorBindings.NativeBindingException e) { - showAlertDiaglogAndExit(e); - } +// try { +// on_resume_saved_state = emulator.saveState(); +// } catch (EmulatorBindings.NativeBindingException e) { +// showAlertDiaglogAndExit(e); +// } } } @Override protected void onResume() { super.onResume(); - if (emulator.isOpen() && snapshot != null) { + if (emulator.isOpen()) { Log.d(TAG, "onResume - loading emulator state"); - try { - emulator.loadState(snapshot); - } catch (EmulatorBindings.NativeBindingException e) { - showAlertDiaglogAndExit(e); - } - snapshot = null; +// try { +// emulator.loadState(on_resume_saved_state); +// } catch (EmulatorBindings.NativeBindingException e) { +// showAlertDiaglogAndExit(e); +// } +// on_resume_saved_state = null; if (runnable != null) { runnable.resumeEmulation(); } } } + public void onSaveSnapshot(View v) { + SnapshotManager snapshotManager = SnapshotManager.getInstance(this); + + runnable.pauseEmulation(); + try { + String gameCode = emulator.getGameCode(); + String gameTitle = emulator.getGameTitle(); + byte[] saveState = emulator.saveState(); + Bitmap preview = Bitmap.createBitmap(emulator.getFrameBuffer(), 240, 160, Bitmap.Config.RGB_565); + + snapshotManager.saveSnapshot(gameCode, gameTitle, preview, saveState); + Toast.makeText(this, "Snapshot saved", Toast.LENGTH_LONG).show(); + + } catch (EmulatorBindings.NativeBindingException e) { + Log.e(TAG, e.toString()); + showAlertDiaglogAndExit(e); + } finally { + runnable.resumeEmulation(); + } + } + + + public void onViewSnapshots(View v) { + Intent intent = new Intent(this, SnapshotActivity.class); + startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE); + } + public void updateScreen(Bitmap bmp) { this.screen.setImageBitmap(bmp); } @@ -247,19 +312,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL 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); - } - }); - } - + emulator.runFrame(); if (!emulatorActivity.turboMode) { long currentTime = System.nanoTime(); long timePassed = currentTime - startTimer; 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 new file mode 100644 index 0000000..b8842e4 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/RomListActivity.java @@ -0,0 +1,32 @@ +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/SplashActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/SplashActivity.java index 2cac172..253116f 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 @@ -46,7 +46,7 @@ public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.main_activity); + setContentView(R.layout.splash_activity); if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 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 new file mode 100644 index 0000000..448472e --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/ChosenSnapshot.java @@ -0,0 +1,21 @@ +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/SnapshotActivity.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java new file mode 100644 index 0000000..b995f28 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotActivity.java @@ -0,0 +1,69 @@ +package com.mrmichel.rustdroid_emu.ui.snapshots; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import android.view.View; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ListView; +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/SnapshotItemAdapter.java b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotItemAdapter.java new file mode 100644 index 0000000..df27d87 --- /dev/null +++ b/AndroidApp/app/src/main/java/com/mrmichel/rustdroid_emu/ui/snapshots/SnapshotItemAdapter.java @@ -0,0 +1,66 @@ +package com.mrmichel.rustdroid_emu.ui.snapshots; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +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.Snapshot; + +import org.w3c.dom.Text; + +import java.sql.Timestamp; +import java.util.ArrayList; + +public class SnapshotItemAdapter extends ArrayAdapter { + + Context context; + ArrayList items; + + public SnapshotItemAdapter(Context context, ArrayList items) { + super(context, 0, items); + this.context = context; + this.items = items; + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + Snapshot snapshot = getItem(position); + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.snapshot_item, parent, false); + } + + ImageView preview = (ImageView) convertView.findViewById(R.id.imageview_snapshot_preview); + preview.setImageBitmap(snapshot.getPreview()); + + + TextView tvTitle = (TextView) convertView.findViewById(R.id.textview_snapshot_title); + tvTitle.setText(snapshot.getGameTitle()); + + TextView tvTimestamp = (TextView) convertView.findViewById(R.id.textview_snapshot_timestmap); + Timestamp timestamp = new Timestamp(snapshot.getTimestamp()); + tvTimestamp.setText(timestamp.toString()); + + return convertView; + } +} diff --git a/AndroidApp/app/src/main/res/layout/activity_emulator.xml b/AndroidApp/app/src/main/res/layout/activity_emulator.xml index bce08d8..01679c6 100644 --- a/AndroidApp/app/src/main/res/layout/activity_emulator.xml +++ b/AndroidApp/app/src/main/res/layout/activity_emulator.xml @@ -6,18 +6,31 @@ android:layout_height="match_parent" tools:context=".ui.EmulatorActivity"> + - - - - - - + + + + diff --git a/AndroidApp/app/src/main/res/layout/activity_rom_list.xml b/AndroidApp/app/src/main/res/layout/activity_rom_list.xml new file mode 100644 index 0000000..83da46b --- /dev/null +++ b/AndroidApp/app/src/main/res/layout/activity_rom_list.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidApp/app/src/main/res/layout/activity_snapshot.xml b/AndroidApp/app/src/main/res/layout/activity_snapshot.xml new file mode 100644 index 0000000..7f53792 --- /dev/null +++ b/AndroidApp/app/src/main/res/layout/activity_snapshot.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ 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 index 8aa4153..4fb1594 100644 --- a/AndroidApp/app/src/main/res/layout/content_emulator.xml +++ b/AndroidApp/app/src/main/res/layout/content_emulator.xml @@ -28,7 +28,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.517" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/bSave" /> + app:layout_constraintTop_toBottomOf="@+id/bSnapshotSave" />