android: Putting in work

- Landscape layout.
- Snapshot viewer is now a fragment for future re-use.
- Bugfixes
- Etc'


Former-commit-id: 34365fd143cefdc2b1bb3b68a62f62849f442fa1
This commit is contained in:
Michel Heily 2020-02-29 23:40:05 +02:00
parent 46287ca88d
commit 2aa0eec95e
49 changed files with 2030 additions and 735 deletions

View file

@ -5,7 +5,7 @@ android {
buildToolsVersion "29.0.3" buildToolsVersion "29.0.3"
defaultConfig { defaultConfig {
applicationId "com.mrmichel.rustdroid_emu" applicationId "com.mrmichel.rustdroid_emu"
minSdkVersion 19 minSdkVersion 21
targetSdkVersion 23 targetSdkVersion 23
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -49,6 +49,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.google.android.material:material:1.1.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' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

View file

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mrmichel.rustdroid_emu"> package="com.mrmichel.rustdroid_emu">
<uses-feature android:glEsVersion="0x00200000" android:required="true"></uses-feature> <uses-feature
android:glEsVersion="0x00200000"
android:required="true" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@ -13,17 +16,18 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".ui.snapshots.SnapshotPickerActivity"></activity>
<activity <activity
android:name=".ui.snapshots.SnapshotActivity" android:name=".ui.snapshots.SnapshotListFragment"
android:label="@string/title_activity_snapshot" android:label="@string/title_activity_snapshot" />
></activity>
<activity <activity
android:name=".ui.RomListActivity" android:name=".ui.library.RomListActivity"
android:label="@string/title_activity_rom_list" /> android:label="@string/title_activity_rom_list" />
<activity <activity
android:name=".ui.EmulatorActivity" android:name=".ui.EmulatorActivity"
android:label="@string/title_activity_emulator" android:label="@string/title_activity_emulator" />
android:theme="@style/AppTheme.NoActionBar" /> <activity android:name=".ui.SettingsActivity"
android:label="Settings" />
<activity android:name=".ui.SplashActivity"> <activity android:name=".ui.SplashActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -5,6 +5,10 @@ package com.mrmichel.rustboyadvance;
*/ */
public class EmulatorBindings { public class EmulatorBindings {
static {
System.loadLibrary("rustboyadvance_jni");
}
public class NativeBindingException extends Exception { public class NativeBindingException extends Exception {
public NativeBindingException(String errorMessage) { public NativeBindingException(String errorMessage) {
super(errorMessage); super(errorMessage);
@ -17,10 +21,20 @@ public class EmulatorBindings {
* @param rom bytearray of the rom to run * @param rom bytearray of the rom to run
* @param frameBuffer frameBuffer render target * @param frameBuffer frameBuffer render target
* @param save_name name of the save file TODO remove this * @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 * @return the emulator context to use pass to other methods in this class
* @throws NativeBindingException * @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 * Make the emulator boot directly into the cartridge

View file

@ -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);
}

View file

@ -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());
}
}

View file

@ -16,13 +16,23 @@ public class Emulator {
private int[] frameBuffer; private int[] frameBuffer;
public Keypad keypad; public Keypad keypad;
static { public Emulator() {
System.loadLibrary("rustboyadvance_jni"); this.frameBuffer = new int[240 * 160];
this.keypad = new Keypad();
} }
public Emulator() { public Emulator(long ctx) {
frameBuffer = new int[240 * 160]; this.ctx = ctx;
keypad = new Keypad(); 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() { public int[] getFrameBuffer() {
@ -49,12 +59,20 @@ public class Emulator {
public synchronized void loadState(byte[] state) throws EmulatorBindings.NativeBindingException { 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 { public synchronized void open(byte[] bios, byte[] rom, String saveName, boolean skipBios) throws EmulatorBindings.NativeBindingException {
this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameBuffer, saveName); 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() { public synchronized void close() {
@ -66,11 +84,19 @@ public class Emulator {
} }
public String getGameCode() { public String getGameCode() {
return EmulatorBindings.getGameCode(ctx); if (ctx != -1) {
return EmulatorBindings.getGameCode(ctx);
} else {
return null;
}
} }
public String getGameTitle() { public String getGameTitle() {
return EmulatorBindings.getGameTitle(ctx); if (ctx != -1) {
return EmulatorBindings.getGameTitle(ctx);
} else {
return null;
}
} }
public boolean isOpen() { public boolean isOpen() {

View file

@ -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<RomMetadataEntry> getAllRomMetaData() {
return this.dbHelper.queryRomMetadata("SELECT * FROM " + RomDatabaseHelper.TABLE_METADATA + " ORDER BY lastPlayed DESC");
}
public RomMetadataEntry getRomMetadata(byte[] romData) {
String romHash = getHash(romData);
ArrayList<RomMetadataEntry> 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<RomMetadataEntry> 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<RomMetadataEntry> queryRomMetadata(String query) {
ArrayList<RomMetadataEntry> 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);
}
}
}

View file

@ -2,6 +2,8 @@ package com.mrmichel.rustdroid_emu.core;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import com.mrmichel.rustdroid_emu.Util;
import java.io.File; import java.io.File;
public class Snapshot { public class Snapshot {
@ -27,6 +29,10 @@ public class Snapshot {
this.timestamp = timestamp; this.timestamp = timestamp;
} }
public File getFile() {
return file;
}
public String getGameCode() { public String getGameCode() {
return gameCode; return gameCode;
} }
@ -44,6 +50,6 @@ public class Snapshot {
} }
public byte[] load() { public byte[] load() {
return SnapshotManager.readCompressedFile(this.file); return Util.readCompressedFile(this.file);
} }
} }

View file

@ -7,18 +7,13 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.util.Log;
import com.mrmichel.rustdroid_emu.Util;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; 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.sql.Timestamp;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class SnapshotManager { public class SnapshotManager {
private static final String TAG = "SnapshotManager"; private static final String TAG = "SnapshotManager";
@ -32,6 +27,65 @@ public class SnapshotManager {
private SnapshotDatabaseHelper dbHelper; 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<Snapshot> getAllSnapshots() {
return this.dbHelper.getEntries();
}
public ArrayList<Snapshot> getByGameCode(String gameCode) {
return this.dbHelper.getEntries(gameCode);
}
public class SnapshotDBEntry { public class SnapshotDBEntry {
String gameCode; String gameCode;
String gameTitle; String gameTitle;
@ -88,7 +142,7 @@ public class SnapshotManager {
File previewImageFile = new File(cursor.getString(4)); File previewImageFile = new File(cursor.getString(4));
File dataFile = new File(cursor.getString(5)); File dataFile = new File(cursor.getString(5));
byte[] previewData = readCompressedFile(previewImageFile); byte[] previewData = Util.readCompressedFile(previewImageFile);
Bitmap previewBitmap = BitmapFactory.decodeByteArray(previewData, 0, previewData.length); Bitmap previewBitmap = BitmapFactory.decodeByteArray(previewData, 0, previewData.length);
arrayList.add(new Snapshot(dataFile, gameCode, gameTitle, previewBitmap, timestamp.getTime())); arrayList.add(new Snapshot(dataFile, gameCode, gameTitle, previewBitmap, timestamp.getTime()));
@ -100,12 +154,12 @@ public class SnapshotManager {
return arrayList; return arrayList;
} }
public ArrayList<Snapshot> getAllEntries() { public ArrayList<Snapshot> getEntries() {
return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + " ORDER BY timestamp DESC "); return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + " ORDER BY timestamp DESC ");
} }
public ArrayList<Snapshot> getAllEntries(String gameCode) { public ArrayList<Snapshot> getEntries(String gameCode) {
return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + "where gameCode = " + gameCode + " ORDER BY timestamp DESC "); return getEntriesByQuery("SELECT * FROM " + TABLE_NAME + " where gameCode = '" + gameCode + "' ORDER BY timestamp DESC ");
} }
@Override @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<Snapshot> getAllSnapshots() {
return this.dbHelper.getAllEntries();
}
public ArrayList<Snapshot> getByGameCode(String gameCode) {
return this.dbHelper.getAllEntries(gameCode);
}
} }

View file

@ -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());
}
}
}
}

View file

@ -1,7 +1,9 @@
package com.mrmichel.rustdroid_emu.ui; package com.mrmichel.rustdroid_emu.ui;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
@ -9,57 +11,74 @@ import android.media.AudioTrack;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; 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.util.Log;
import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Switch; import android.widget.CompoundButton;
import android.widget.Toast; 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.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.AudioThread;
import com.mrmichel.rustdroid_emu.core.Emulator; import com.mrmichel.rustdroid_emu.core.Emulator;
import com.mrmichel.rustdroid_emu.core.Keypad; 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.core.SnapshotManager;
import com.mrmichel.rustdroid_emu.ui.snapshots.ChosenSnapshot; import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotPickerActivity;
import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotActivity;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
private static final String TAG = "EmulatorActivty"; 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_ROM_REQUESTCODE = 123;
private static final int LOAD_SNAPSHOT_REQUESTCODE = 124; private static final int LOAD_SNAPSHOT_REQUESTCODE = 124;
private static int SAMPLE_RATE_HZ = 44100; private static int SAMPLE_RATE_HZ = 44100;
private Menu menu;
private RomManager.RomMetadataEntry romMetadata;
private byte[] bios; private byte[] bios;
private Emulator emulator = null; private EmulationThread emulationThread;
private EmulationRunnable runnable;
private Thread emulationThread;
private AudioThread audioThread; private AudioThread audioThread;
private AudioTrack audioTrack; private AudioTrack audioTrack;
private byte[] on_resume_saved_state = null; 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 @Override
public void onClick(View v) { public void onClick(View v) {
if (v.getId() == R.id.tbTurbo) { if (v.getId() == R.id.tbTurbo) {
Switch tbTurbo = (Switch) findViewById(R.id.tbTurbo); if (!isEmulatorRunning()) {
this.turboMode = tbTurbo.isChecked(); 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; key = Keypad.Key.Select;
break; break;
} }
;
int action = event.getAction(); int action = event.getAction();
if (key != null) { if (key != null) {
if (action == MotionEvent.ACTION_DOWN) { if (action == MotionEvent.ACTION_DOWN) {
v.setPressed(true); v.setPressed(true);
this.emulator.keypad.onKeyDown(key); emulator.keypad.onKeyDown(key);
} else if (action == MotionEvent.ACTION_UP) { } else if (action == MotionEvent.ACTION_UP) {
v.setPressed(false); 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) { public Keypad.Key keyCodeToGbaKey(int keyCode) {
new AlertDialog.Builder(this) switch (keyCode) {
.setTitle("Exception") case KeyEvent.KEYCODE_DPAD_UP:
.setMessage(e.getMessage()) return Keypad.Key.Up;
// Specifying a listener allows you to take an action before dismissing the dialog. case KeyEvent.KEYCODE_DPAD_DOWN:
// The dialog is automatically dismissed when a dialog button is clicked. return Keypad.Key.Down;
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { case KeyEvent.KEYCODE_DPAD_LEFT:
public void onClick(DialogInterface dialog, int which) { return Keypad.Key.Left;
finishAffinity(); case KeyEvent.KEYCODE_DPAD_RIGHT:
} return Keypad.Key.Right;
}) case KeyEvent.KEYCODE_Z:
.setIcon(android.R.drawable.ic_dialog_alert) return Keypad.Key.ButtonB;
.show(); 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 @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
if (requestCode == LOAD_ROM_REQUESTCODE) { // if (requestCode == LOAD_ROM_REQUESTCODE) {
Uri uri = data.getData(); // Uri uri = data.getData();
try { // try {
InputStream inputStream = getContentResolver().openInputStream(uri); // InputStream inputStream = getContentResolver().openInputStream(uri);
byte[] rom = new byte[inputStream.available()]; // byte[] rom = new byte[inputStream.available()];
inputStream.read(rom); // inputStream.read(rom);
inputStream.close(); // inputStream.close();
//
String filename = new File(uri.getPath()).getName(); // String filename = new File(uri.getPath()).getName();
//
File saveRoot = getFilesDir(); // File saveRoot = getFilesDir();
String savePath = saveRoot.getAbsolutePath() + "/" + filename + ".sav"; // String savePath = saveRoot.getAbsolutePath() + "/" + filename + ".sav";
onRomLoaded(rom, savePath); // onRomLoaded(rom, savePath);
} catch (Exception e) { // } catch (Exception e) {
Log.e(TAG, "got error while reading rom file"); // Log.e(TAG, "got error while reading rom file");
showAlertDiaglogAndExit(e); // Util.showAlertDiaglogAndExit(this, e);
} // }
} // }
if (requestCode == LOAD_SNAPSHOT_REQUESTCODE) { if (requestCode == LOAD_SNAPSHOT_REQUESTCODE) {
byte[] state = ChosenSnapshot.takeSnapshot().load(); Snapshot pickedSnapshot = SnapshotPickerActivity.obtainPickedSnapshot();
if (emulator.isOpen()) {
try { Toast.makeText(this, "Loading snapshot from " + pickedSnapshot.getTimestamp(), Toast.LENGTH_LONG).show();
emulator.loadState(state);
} catch (EmulatorBindings.NativeBindingException e) { boolean emulatorWasRunning = isEmulatorRunning();
showAlertDiaglogAndExit(e);
} pauseEmulation();
try {
emulator.loadState(pickedSnapshot.load());
} catch (Exception e) {
Util.showAlertDiaglogAndExit(this, e);
}
resumeEmulation();
if (!emulatorWasRunning) {
createThreads();
} }
} }
} else { } else {
@ -164,75 +243,90 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
} }
} }
public void onRomLoaded(byte[] rom, String savePath) { private void killThreads() {
if (emulationThread != null) {
runnable.stop();
try {
emulationThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "emulation thread join interrupted");
}
emulationThread = null;
}
if (audioThread != null) { if (audioThread != null) {
audioThread.setStopping(true); audioThread.setStopping(true);
try { try {
audioThread.join(); audioThread.join();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Log.e(TAG, "audio thread join interrupted"); Log.e(TAG, "audio thread join interrupted");
}; }
audioThread = null; audioThread = null;
} }
if (emulationThread != null) {
if (emulator.isOpen()) { try {
emulator.close(); emulationThread.setStopping(true);
emulationThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "emulation thread join interrupted");
}
emulationThread = null;
} }
}
findViewById(R.id.bStart).setOnTouchListener(this); private void createThreads() {
findViewById(R.id.bSelect).setOnTouchListener(this); emulationThread = new EmulationThread(emulator, screenView);
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();
audioThread = new AudioThread(audioTrack, emulator); audioThread = new AudioThread(audioTrack, emulator);
emulationThread.setTurbo(turboButton.isChecked());
emulationThread.start();
audioThread.start(); audioThread.start();
} }
public void loadRomButton(View v) { public void onRomLoaded(byte[] rom, String savePath) {
if (runnable != null) { // killThreads();
runnable.pauseEmulation(); //
} // 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 intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*"); intent.setType("*/*");
intent.putExtra("android.content.extra.SHOW_ADVANCED", true); intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
startActivityForResult(intent, LOAD_ROM_REQUESTCODE); 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_emulator); setContentView(R.layout.activity_emulator);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
this.bios = getIntent().getByteArrayExtra("bios");
this.emulator = new Emulator();
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder() AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder()
@ -259,7 +353,83 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
} }
this.audioTrack.play(); 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 @Override
@ -271,48 +441,73 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { 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 @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
audioTrack.stop(); audioTrack.stop();
if (emulator.isOpen()) { pauseEmulation();
if (runnable != null) { screenView.onPause();
runnable.pauseEmulation();
}
Log.d(TAG, "onPause - saving emulator state");
// try {
// on_resume_saved_state = emulator.saveState();
// } catch (EmulatorBindings.NativeBindingException e) {
// showAlertDiaglogAndExit(e);
// }
}
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (emulator.isOpen()) { screenView.onResume();
Log.d(TAG, "onResume - loading emulator state"); resumeEmulation();
// try {
// emulator.loadState(on_resume_saved_state);
// } catch (EmulatorBindings.NativeBindingException e) {
// showAlertDiaglogAndExit(e);
// }
// on_resume_saved_state = null;
if (runnable != null) {
runnable.resumeEmulation();
}
}
audioTrack.play(); 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); SnapshotManager snapshotManager = SnapshotManager.getInstance(this);
runnable.pauseEmulation(); pauseEmulation();
try { try {
String gameCode = emulator.getGameCode(); String gameCode = emulator.getGameCode();
String gameTitle = emulator.getGameTitle(); String gameTitle = emulator.getGameTitle();
@ -324,73 +519,48 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
} catch (EmulatorBindings.NativeBindingException e) { } catch (EmulatorBindings.NativeBindingException e) {
Log.e(TAG, e.toString()); Log.e(TAG, e.toString());
showAlertDiaglogAndExit(e); Util.showAlertDiaglogAndExit(this, e);
} finally { } finally {
runnable.resumeEmulation(); resumeEmulation();
} }
} }
public void doViewSnapshots() {
public void onViewSnapshots(View v) { Intent intent = new Intent(this, SnapshotPickerActivity.class);
Intent intent = new Intent(this, SnapshotActivity.class); if (emulator.isOpen()) {
intent.putExtra("gameCode", emulator.getGameCode());
}
startActivityForResult(intent, LOAD_SNAPSHOT_REQUESTCODE); 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; if (!emulatorIsRunning) {
public static final long FRAME_TIME = 1000000000 / 60; super.onBackPressed();
return;
EmulatorActivity emulatorActivity;
Emulator emulator;
boolean running;
boolean stopping;
public EmulationRunnable(Emulator emulator, EmulatorActivity emulatorActivity) {
this.emulator = emulator;
this.emulatorActivity = emulatorActivity;
resumeEmulation();
} }
private void emulate() { new AlertDialog.Builder(this)
long startTimer = System.nanoTime(); .setIcon(android.R.drawable.ic_dialog_alert)
emulator.runFrame(); .setTitle("Closing Emulator")
if (!emulatorActivity.turboMode) { .setCancelable(false)
long currentTime = System.nanoTime(); .setMessage("Are you sure you want to close the emulator?")
long timePassed = currentTime - startTimer; .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
long delay = FRAME_TIME - timePassed; public void onClick(DialogInterface dialog, int which) {
if (delay > 0) { EmulatorActivity.super.onBackPressed();
try {
Thread.sleep(delay / NANOSECONDS_PER_MILLISECOND);
} catch (Exception e) {
} }
} })
} .setNeutralButton("Yes - but save snapshot", new DialogInterface.OnClickListener() {
@Override
emulatorActivity.gbaScreenView.updateFrame(emulator.getFrameBuffer()); public void onClick(DialogInterface dialog, int which) {
} doSaveSnapshot();
EmulatorActivity.super.onBackPressed();
public void pauseEmulation() { }
running = false; })
} .setNegativeButton(android.R.string.no, null)
.show();
public void resumeEmulation() {
running = true;
}
public void stop() {
stopping = true;
}
@Override
public void run() {
while (!stopping) {
if (running) {
emulate();
}
}
}
} }
} }

View file

@ -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();
}
});
}
}

View file

@ -1,12 +1,9 @@
package com.mrmichel.rustdroid_emu.ui; package com.mrmichel.rustdroid_emu.ui;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.opengl.GLUtils; import android.opengl.GLUtils;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
@ -15,8 +12,10 @@ import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Renderer { public class ScreenRenderer implements GLSurfaceView.Renderer {
ScreenTexture texture;
private ScreenTexture texture;
private boolean ready = false;
/** /**
* Private class to manage the screen texture rendering * 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" + "uniform sampler2D s_texture; \n" +
"void main() \n" + "void main() \n" +
"{ \n" + "{ \n" +
" gl_FragColor = texture2D( s_texture, v_texCoord );\n" + " vec4 color = texture2D( s_texture, v_texCoord ); \n" +
" gl_FragColor = color; \n" +
"} \n"; "} \n";
@ -191,34 +191,20 @@ public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Render
} }
} }
public GbaScreenView(Context context) { public void updateTexture(int[] frameBuffer) {
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) {
this.texture.update(frameBuffer); this.texture.update(frameBuffer);
requestRender();
} }
public void initTextureIfNotInitialized() {
if (this.texture == null) {
this.texture = new ScreenTexture();
}
}
@Override @Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) { public void onSurfaceCreated(GL10 gl, EGLConfig config) {
texture = new ScreenTexture(); initTextureIfNotInitialized();
ready = true;
getHolder().setKeepScreenOn(true);
} }
@Override @Override
@ -231,10 +217,7 @@ public class GbaScreenView extends GLSurfaceView implements GLSurfaceView.Render
this.texture.render(); this.texture.render();
} }
@Override public boolean isReady() {
public void surfaceDestroyed(SurfaceHolder holder) { return ready;
holder.setKeepScreenOn(false);
this.texture.destroy();
super.surfaceDestroyed(holder);
} }
} }

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -1,12 +1,5 @@
package com.mrmichel.rustdroid_emu.ui; 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.Manifest;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
@ -19,7 +12,15 @@ import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.widget.Toast; 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.R;
import com.mrmichel.rustdroid_emu.ui.library.RomListActivity;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -49,7 +50,7 @@ public class SplashActivity extends AppCompatActivity {
} }
private void checkOpenGLES20() { private void checkOpenGLES20() {
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo(); ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo();
if (configurationInfo.reqGlEsVersion >= 0x20000) { if (configurationInfo.reqGlEsVersion >= 0x20000) {
// Supported // Supported
@ -81,7 +82,7 @@ public class SplashActivity extends AppCompatActivity {
// No explanation needed; request the permission // No explanation needed; request the permission
ActivityCompat.requestPermissions(this 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); REQUEST_PERMISSION_CODE);
} else { } else {
// Permission has already been granted // 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); FileOutputStream fos = openFileOutput("gba_bios.bin", MODE_PRIVATE);
fos.write(bios); fos.write(bios);
fos.close(); fos.close();
@ -110,7 +111,7 @@ public class SplashActivity extends AppCompatActivity {
cacheBiosInAppFiles(bios); cacheBiosInAppFiles(bios);
initEmulator(bios); startLibraryActivity(bios);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "can't open bios file"); Log.e(TAG, "can't open bios file");
this.finishAffinity(); this.finishAffinity();
@ -126,7 +127,7 @@ public class SplashActivity extends AppCompatActivity {
FileInputStream fis = openFileInput("gba_bios.bin"); FileInputStream fis = openFileInput("gba_bios.bin");
byte[] bios = new byte[fis.available()]; byte[] bios = new byte[fis.available()];
fis.read(bios); fis.read(bios);
initEmulator(bios); startLibraryActivity(bios);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*"); intent.setType("*/*");
@ -138,9 +139,10 @@ public class SplashActivity extends AppCompatActivity {
} }
} }
private void initEmulator(byte[] bios) { private void startLibraryActivity(byte[] bios) {
Intent intent = new Intent(this, EmulatorActivity.class); Intent intent = new Intent(this, RomListActivity.class);
intent.putExtra("bios", bios); intent.putExtra("bios", bios);
startActivity(intent); startActivity(intent);
finish();
} }
} }

View file

@ -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<RomManager.RomMetadataEntry> 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);
}
}

View file

@ -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<RomMetadataEntry> {
Context context;
ArrayList<RomMetadataEntry> items;
public RomListItemAdapter(Context context, ArrayList<RomMetadataEntry> 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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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<Snapshot> 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);
}
});
}
}

View file

@ -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<Snapshot> 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);
}
}

View file

@ -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();
}
}

View file

@ -1,28 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"
<selector xmlns:android="http://schemas.android.com/apk/res/android"> android:shape="rectangle">
<item android:state_pressed="false"> <solid android:color="@color/colorAccent" />
<shape android:shape="rectangle"> <corners android:radius="500dp" />
<corners android:radius="1000dp" /> </shape>
<stroke
android:width="2dip"/>
<padding
android:bottom="4dp"
android:left="4dp"
android:right="4dp"
android:top="4dp" />
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="1000dp" />
<stroke
android:width="2dip"
android:color="#03ae3c" />
<padding
android:bottom="4dp"
android:left="4dp"
android:right="4dp"
android:top="4dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.EmulatorActivity">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/content_emulator" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/gbaBackground"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mrmichel.rustdroid_emu.ui.ScreenView
android:id="@+id/gba_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="V, 3:2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<include
android:id="@+id/include"
layout="@layout/dpad"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:alpha=".5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/buttonB"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="52dp"
android:layout_height="54dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="32dp"
android:background="@drawable/round_button"
android:text="B"
android:alpha=".5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonA"
tools:text="B" />
<Button
android:id="@+id/buttonA"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="52dp"
android:layout_height="54dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="48dp"
android:background="@drawable/round_button"
android:text="A"
android:alpha=".5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="A" />
<Button
android:id="@+id/bSelect"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="select"
android:textSize="8dp"
app:layout_constraintBottom_toTopOf="@+id/bStart"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/bStart"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="20dp"
android:text="start"
android:textSize="8dp"
app:layout_constraintBottom_toTopOf="@+id/include"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/buttonL"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="L"
android:alpha=".5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/buttonR"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="R"
android:alpha=".5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ToggleButton
android:id="@+id/tbTurbo"
android:layout_width="98dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="16dp"
android:checked="false"
android:text="Turbo Mode"
android:textOff="Turbo OFF"
android:textOn="Turbo ON"
app:layout_constraintBottom_toTopOf="@+id/bSelect"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,35 +4,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/gbaBackground"
tools:context=".ui.EmulatorActivity"> tools:context=".ui.EmulatorActivity">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.appbar.AppBarLayout android:id="@+id/fragment_container"
android:id="@+id/app_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height" android:layout_height="match_parent">
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<com.google.android.material.appbar.CollapsingToolbarLayout <include layout="@layout/content_emulator" />
android:id="@+id/toolbar_layout" </FrameLayout>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/toolbar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_emulator" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".ui.RomListActivity"> tools:context=".ui.library.RomListActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@ -19,7 +19,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary" app:contentScrim="?attr/colorAccent"
app:layout_scrollFlags="scroll|exitUntilCollapsed" app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/toolbar"> app:toolbarId="@+id/toolbar">
@ -33,8 +33,6 @@
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_rom_list" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -42,6 +40,8 @@
android:layout_margin="@dimen/fab_margin" android:layout_margin="@dimen/fab_margin"
app:layout_anchor="@id/app_bar" app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end" app:layout_anchorGravity="bottom|end"
app:srcCompat="@android:drawable/ic_dialog_email" /> app:srcCompat="@android:drawable/ic_media_play" />
<include layout="@layout/content_rom_list" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/settings_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</LinearLayout>

View file

@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="#6c45c0" android:background="@color/gbaBackground"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -16,17 +16,16 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<com.mrmichel.rustdroid_emu.ui.ScreenView
<com.mrmichel.rustdroid_emu.ui.GbaScreenView
android:id="@+id/gba_view" android:id="@+id/gba_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="120dp" android:layout_marginTop="32dp"
app:layout_constraintDimensionRatio="H, 3:2" app:layout_constraintDimensionRatio="H, 3:2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.517" app:layout_constraintHorizontal_bias="0.517"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bSnapshotSave" /> app:layout_constraintTop_toTopOf="parent" />
<Button <Button
android:id="@+id/buttonB" android:id="@+id/buttonB"
@ -113,53 +112,12 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/bLoadRom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:onClick="loadRomButton"
android:text="Load Rom"
app:layout_constraintBottom_toTopOf="@+id/gbaMockImageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bSnapshotSave"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:onClick="onSaveSnapshot"
android:text="@string/action_save_snapshot"
app:layout_constraintEnd_toStartOf="@+id/bRestore"
app:layout_constraintStart_toEndOf="@+id/bLoadRom"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bRestore"
style="@style/Widget.AppCompat.Button.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:onClick="onViewSnapshots"
android:text="@string/action_view_snapshot"
app:layout_constraintBottom_toTopOf="@+id/gbaMockImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView <TextView
android:id="@+id/tvFPS" android:id="@+id/tvFPS"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/gbaMockImageView" app:layout_constraintTop_toBottomOf="@+id/gba_view"
tools:layout_editor_absoluteX="176dp" /> tools:layout_editor_absoluteX="176dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,17 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ui.RomListActivity" tools:context=".ui.library.RomListActivity"
tools:showIn="@layout/activity_rom_list"> tools:showIn="@layout/activity_rom_list">
<TextView <GridView
android:layout_width="wrap_content" android:id="@+id/gridview_rom_list"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_margin="@dimen/text_margin" android:layout_height="match_parent"
android:text="@string/large_text" /> android:gravity="center"
android:numColumns="auto_fit"
tools:context=".ui.library.RomListActivity"></GridView>
</androidx.core.widget.NestedScrollView> </LinearLayout>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="5dp"
>
<ImageView
android:id="@+id/imageview_screenshot"
android:layout_width="180dp"
android:layout_height="120dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_margin="5dp"
android:scaleType="centerCrop"
android:background="@drawable/round_button"
app:srcCompat="@drawable/icon" />
<TextView
android:id="@+id/textview_game_title"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingRight="8dp"
android:text="Cool Game - III"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textSize="12sp"
android:textStyle="bold"
android:layout_below="@+id/imageview_screenshot"
/>
</RelativeLayout>

View file

@ -5,12 +5,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:layout_margin="10dp"
> >
<ImageView <ImageView
android:id="@+id/imageview_snapshot_preview" android:id="@+id/imageview_snapshot_preview"
android:layout_width="180dp" android:layout_width="180dp"
android:layout_height="160dp" android:layout_height="120dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:srcCompat="@drawable/icon" /> app:srcCompat="@drawable/icon" />

View file

@ -4,27 +4,10 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#6c45c0" android:background="@color/colorAccent"
tools:context=".ui.snapshots.SnapshotActivity"> tools:context=".ui.snapshots.SnapshotListFragment">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="126dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="126dp"
android:gravity="center"
android:text="Choose snapshot to load"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
android:textSize="14sp"
android:textStyle="bold"
android:typeface="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edittext_snapshot_filter" />
<EditText <EditText
android:id="@+id/edittext_snapshot_filter" android:id="@+id/edittext_snapshot_filter"
style="@style/Widget.AppCompat.EditText" style="@style/Widget.AppCompat.EditText"
@ -50,9 +33,9 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" app:layout_constraintTop_toBottomOf="@+id/edittext_snapshot_filter"
tools:context=".ui.snapshots.SnapshotActivity" tools:context=".ui.snapshots.SnapshotListFragment"
tools:showIn="@layout/activity_snapshot"> tools:showIn="@layout/snapshot_list_fragment">
</GridView> </GridView>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.snapshots.SnapshotPickerActivity" />

View file

@ -5,7 +5,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#6c45c0"
tools:context=".ui.SplashActivity"> tools:context=".ui.SplashActivity">
<ImageView <ImageView

View file

@ -0,0 +1,31 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/menu_context_rom"
tools:context="com.mrmichel.rustdroid_emu.ui.library.RomListActivity">
<item
android:id="@+id/action_play"
android:icon="@android:drawable/ic_media_play"
android:title="@string/action_load_rom"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_set_screenshot"
android:title="@string/action_set_screenshot"
/>
<item
android:title="@string/action_view_snapshot"
app:showAsAction="withText"
android:id="@+id/action_view_snapshots" />
<item
android:id="@+id/action_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/action_delete"
app:showAsAction="ifRoom|withText" />
</menu>

View file

@ -0,0 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>
<item
android:id="@+id/action_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/action_delete"
app:showAsAction="ifRoom|withText" />
</menu>

View file

@ -1,20 +1,40 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.mrmichel.rustdroid_emu.ui.RomListActivity"> tools:context="com.mrmichel.rustdroid_emu.ui.EmulatorActivity">
<item
android:id="@+id/action_load_rom"
android:orderInCategory="1"
android:title="@string/action_load_rom" />
<!-- <item-->
<!-- android:title="@string/submenu_snapshots"-->
<!-- android:orderInCategory="2"-->
<!-- >-->
<!-- <menu>-->
<item
android:id="@+id/action_save_snapshot"
android:enabled="false"
android:icon="@android:drawable/ic_menu_save"
android:orderInCategory="100"
android:title="@string/action_save_snapshot"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_view_snapshots"
android:icon="@android:drawable/ic_menu_gallery"
android:orderInCategory="101"
android:title="@string/action_view_snapshot"
app:showAsAction="ifRoom|withText" />
<!-- </menu>-->
<!-- </item>-->
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"
android:menuCategory="system"
android:title="@string/action_settings" android:title="@string/action_settings"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_save_snapshot"
android:icon="@android:drawable/ic_input_add"
android:orderInCategory="101"
android:title="@string/action_save_snapshot"
app:showAsAction="withText" />
<item
android:id="@+id/action_view_snapshots"
android:orderInCategory="102"
android:title="@string/action_view_snapshot" />
</menu> </menu>

View file

@ -1,7 +1,14 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.mrmichel.rustdroid_emu.ui.RomListActivity"> tools:context="com.mrmichel.rustdroid_emu.ui.library.RomListActivity">
<item
android:title="@string/action_load_rom"
android:id="@+id/action_import_rom" />
<item
android:title="@string/action_import_directory"
android:id="@+id/action_import_directory" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"

View file

@ -1,7 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotActivity"> tools:context="com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotListFragment">
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#008577</color> <color name="colorPrimary">#6c45c0</color>
<color name="colorPrimaryDark">#00574B</color> <color name="colorPrimaryDark">#331573</color>
<color name="colorAccent">#D81B60</color> <color name="colorAccent">#ad8cf4</color>
<color name="gbaBackground">#6c45c0</color>
</resources> </resources>

View file

@ -1,99 +1,19 @@
<resources> <resources>
<string name="app_name">RustdroidAdvance</string> <string name="app_name">RustdroidAdvance</string>
<string name="title_activity_emulator">RustdroidAdvance</string> <string name="title_activity_emulator">RustdroidAdvance</string>
<string name="title_activity_rom_list">Roms</string> <string name="title_activity_rom_list">Rom Library</string>
<string name="large_text">
"Material is the metaphor.\n\n"
"A material metaphor is the unifying theory of a rationalized space and a system of motion." <string name="action_load_rom">Load ROM</string>
"The material is grounded in tactile reality, inspired by the study of paper and ink, yet " <string name="action_import_directory">Import Directory</string>
"technologically advanced and open to imagination and magic.\n"
"Surfaces and edges of the material provide visual cues that are grounded in reality. The "
"use of familiar tactile attributes helps users quickly understand affordances. Yet the "
"flexibility of the material creates new affordances that supercede those in the physical "
"world, without breaking the rules of physics.\n"
"The fundamentals of light, surface, and movement are key to conveying how objects move, "
"interact, and exist in space and in relation to each other. Realistic lighting shows "
"seams, divides space, and indicates moving parts.\n\n"
"Bold, graphic, intentional.\n\n"
"The foundational elements of print based design typography, grids, space, scale, color, "
"and use of imagery guide visual treatments. These elements do far more than please the "
"eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
"imagery, large scale typography, and intentional white space create a bold and graphic "
"interface that immerse the user in the experience.\n"
"An emphasis on user actions makes core functionality immediately apparent and provides "
"waypoints for the user.\n\n"
"Motion provides meaning.\n\n"
"Motion respects and reinforces the user as the prime mover. Primary user actions are "
"inflection points that initiate motion, transforming the whole design.\n"
"All action takes place in a single environment. Objects are presented to the user without "
"breaking the continuity of experience even as they transform and reorganize.\n"
"Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
"Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
"3D world.\n\n"
"The material environment is a 3D space, which means all objects have x, y, and z "
"dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
"positive z-axis extending towards the viewer. Every sheet of material occupies a single "
"position along the z-axis and has a standard 1dp thickness.\n"
"On the web, the z-axis is used for layering and not for perspective. The 3D world is "
"emulated by manipulating the y-axis.\n\n"
"Light and shadow.\n\n"
"Within the material environment, virtual lights illuminate the scene. Key lights create "
"directional shadows, while ambient light creates soft shadows from all angles.\n"
"Shadows in the material environment are cast by these two light sources. In Android "
"development, shadows occur when light sources are blocked by sheets of material at "
"various positions along the z-axis. On the web, shadows are depicted by manipulating the "
"y-axis only. The following example shows the card with a height of 6dp.\n\n"
"Resting elevation.\n\n"
"All material objects, regardless of size, have a resting elevation, or default elevation "
"that does not change. If an object changes elevation, it should return to its resting "
"elevation as soon as possible.\n\n"
"Component elevations.\n\n"
"The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
"does not vary from 6dp in one app to 16dp in another app).\n"
"Components may have different resting elevations across platforms, depending on the depth "
"of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
"Responsive elevation and dynamic elevation offsets.\n\n"
"Some component types have responsive elevation, meaning they change elevation in response "
"to user input (e.g., normal, focused, and pressed) or system events. These elevation "
"changes are consistently implemented using dynamic elevation offsets.\n"
"Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
"to the components resting state. They ensure that elevation changes are consistent "
"across actions and component types. For example, all components that lift on press have "
"the same elevation change relative to their resting elevation.\n"
"Once the input event is completed or cancelled, the component will return to its resting "
"elevation.\n\n"
"Avoiding elevation interference.\n\n"
"Components with responsive elevations may encounter other components as they move between "
"their resting elevations and dynamic elevation offsets. Because material cannot pass "
"through other material, components avoid interfering with one another any number of ways, "
"whether on a per component basis or using the entire app layout.\n"
"On a component level, components can move or be removed before they cause interference. "
"For example, a floating action button (FAB) can disappear or move off screen before a "
"user picks up a card, or it can move if a snackbar appears.\n"
"On the layout level, design your app layout to minimize opportunities for interference. "
"For example, position the FAB to one side of stream of a cards so the FAB wont interfere "
"when a user tries to pick up one of cards.\n\n"
</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="submenu_snapshots">Snapshots</string>
<string name="action_save_snapshot">Save Snapshot</string> <string name="action_save_snapshot">Save Snapshot</string>
<string name="action_view_snapshot">View Snapshots</string> <string name="action_view_snapshot">View Snapshots</string>
<string name="action_set_screenshot">Set Image</string>
<string name="action_delete">Delete</string>
<string name="title_activity_snapshot">Snapshot Manager</string> <string name="title_activity_snapshot">Snapshot Manager</string>
</resources> </resources>

View file

@ -0,0 +1,8 @@
<PreferenceScreen
xmlns:app="http://schemas.android.com/apk/res-auto">
<CheckBoxPreference
app:key="skip_bios"
app:title="Skip bios boot animation"/>
</PreferenceScreen>

View file

@ -1,5 +1,7 @@
/// JNI Bindings for rustboyadvance /// JNI Bindings for rustboyadvance
/// ///
mod rom_helper;
use std::cell::RefCell; use std::cell::RefCell;
use std::os::raw::c_void; use std::os::raw::c_void;
use std::path::Path; use std::path::Path;
@ -85,6 +87,7 @@ unsafe fn internal_open_context(
rom: jbyteArray, rom: jbyteArray,
frame_buffer: jintArray, frame_buffer: jintArray,
save_file: JString, save_file: JString,
skip_bios: jboolean,
) -> Result<Context, String> { ) -> Result<Context, String> {
let bios = env let bios = env
.convert_byte_array(bios) .convert_byte_array(bios)
@ -119,7 +122,12 @@ unsafe fn internal_open_context(
}; };
let hw = Rc::new(RefCell::new(hw)); let hw = Rc::new(RefCell::new(hw));
let gba = GameBoyAdvance::new(bios, gamepak, hw.clone(), hw.clone(), hw.clone()); let mut gba = GameBoyAdvance::new(bios, gamepak, hw.clone(), hw.clone(), hw.clone());
if skip_bios != 0 {
debug!("skipping bios");
gba.skip_bios();
}
debug!("creating context"); debug!("creating context");
let context = Context { let context = Context {
@ -182,12 +190,64 @@ pub mod bindings {
rom: jbyteArray, rom: jbyteArray,
frame_buffer: jintArray, frame_buffer: jintArray,
save_file: JString, save_file: JString,
skip_bios: jboolean
) -> jlong { ) -> jlong {
match internal_open_context(&env, bios, rom, frame_buffer, save_file) { match internal_open_context(&env, bios, rom, frame_buffer, save_file, skip_bios) {
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong, Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
Err(msg) => { Err(msg) => {
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap(); env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
0 -1
}
}
}
fn inetrnal_open_saved_state(
env: &JNIEnv,
state: jbyteArray,
frame_buffer: jintArray,
) -> Result<Context, String> {
let state = env
.convert_byte_array(state)
.map_err(|e| format!("could not get state buffer, error {}", e))?;
let frame_buffer_global_ref = env
.new_global_ref(JObject::from(frame_buffer))
.map_err(|e| format!("failed to add new global ref, error: {:?}", e))?;
let hw = Hardware {
jvm: env.get_java_vm().unwrap(),
frame_buffer_global_ref: frame_buffer_global_ref,
audio_buffer: AudioRingBuffer::new(),
key_state: 0xffff,
};
let hw = Rc::new(RefCell::new(hw));
let gba = GameBoyAdvance::from_saved_state(&state, hw.clone(), hw.clone(), hw.clone())
.map_err(|e| {
format!(
"failed to create GameBoyAdvance from saved state, error {:?}",
e
)
})?;
Ok(Context {
gba: gba,
hwif: hw.clone(),
})
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_openSavedState(
env: JNIEnv,
_obj: JClass,
state: jbyteArray,
frame_buffer: jintArray,
) -> jlong {
match inetrnal_open_saved_state(&env, state, frame_buffer) {
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
Err(msg) => {
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
-1
} }
} }
} }

View file

@ -0,0 +1,30 @@
use jni::objects::*;
use jni::sys::*;
use jni::{JNIEnv, JavaVM};
use rustboyadvance_ng::core::cartridge;
fn parse_rom_header(env: &JNIEnv, barr: jbyteArray) -> cartridge::header::CartridgeHeader {
let rom_data = env.convert_byte_array(barr).unwrap();
cartridge::header::parse(&rom_data)
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
env: JNIEnv,
_obj: JClass,
rom_data: jbyteArray,
) -> jstring {
let header = parse_rom_header(&env, rom_data);
env.new_string(header.game_code).unwrap().into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameTitle(
env: JNIEnv,
_obj: JClass,
rom_data: jbyteArray,
) -> jstring {
let header = parse_rom_header(&env, rom_data);
env.new_string(header.game_title).unwrap().into_inner()
}

View file

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use super::{Addr, Bus}; use super::{Addr, Bus};
mod header; pub mod header;
use header::CartridgeHeader; use header::CartridgeHeader;
mod backup; mod backup;

View file

@ -68,6 +68,26 @@ impl GameBoyAdvance {
gba gba
} }
pub fn from_saved_state(
savestate: &[u8],
video_device: Rc<RefCell<dyn VideoInterface>>,
audio_device: Rc<RefCell<dyn AudioInterface>>,
input_device: Rc<RefCell<dyn InputInterface>>,
) -> bincode::Result<GameBoyAdvance> {
let decoded: Box<SaveState> = bincode::deserialize_from(savestate)?;
Ok(GameBoyAdvance {
cpu: decoded.cpu,
sysbus: decoded.sysbus,
video_device: video_device,
audio_device: audio_device,
input_device: input_device,
cycles_to_next_event: 1,
})
}
pub fn save_state(&self) -> bincode::Result<Vec<u8>> { pub fn save_state(&self) -> bincode::Result<Vec<u8>> {
let s = SaveState { let s = SaveState {
cpu: self.cpu.clone(), cpu: self.cpu.clone(),