diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 1fa11eb14..106cc9cb6 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -1,9 +1,11 @@ #include "android_host_interface.h" #include "common/assert.h" #include "common/audio_stream.h" +#include "common/file_system.h" #include "common/log.h" #include "common/string.h" #include "common/timestamp.h" +#include "core/bios.h" #include "core/cheats.h" #include "core/controller.h" #include "core/gpu.h" @@ -195,7 +197,7 @@ void AndroidHostInterface::PauseEmulationThread(bool paused) void AndroidHostInterface::StopEmulationThread() { if (!IsEmulationThreadRunning()) - return; + return; Log_InfoPrint("Stopping emulation thread..."); { @@ -848,3 +850,37 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_addOSDMessage, jobject obj, js AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); hi->AddOSDMessage(AndroidHelpers::JStringToString(env, message), duration); } + +DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasAnyBIOSImages, jobject obj) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + return hi->HasAnyBIOSImages(); +} + +DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_importBIOSImage, jobject obj, jbyteArray data) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + + const jsize len = env->GetArrayLength(data); + if (len != BIOS::BIOS_SIZE) + return nullptr; + + BIOS::Image image; + image.resize(static_cast(len)); + env->GetByteArrayRegion(data, 0, len, reinterpret_cast(image.data())); + + const BIOS::Hash hash = BIOS::GetHash(image); + const BIOS::ImageInfo* ii = BIOS::GetImageInfoForHash(hash); + + const std::string dest_path(hi->GetUserDirectoryRelativePath("bios/%s.bin", hash.ToString().c_str())); + if (FileSystem::FileExists(dest_path.c_str()) || + !FileSystem::WriteBinaryFile(dest_path.c_str(), image.data(), image.size())) + { + return nullptr; + } + + if (ii) + return env->NewStringUTF(ii->description); + else + return env->NewStringUTF(hash.ToString().c_str()); +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index d4bda0643..baedc0c2a 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -76,6 +76,9 @@ public class AndroidHostInterface { public native void addOSDMessage(String message, float duration); + public native boolean hasAnyBIOSImages(); + public native String importBIOSImage(byte[] data); + static { System.loadLibrary("duckstation-native"); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index b63cbce5d..f29c22a99 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -1,6 +1,7 @@ package com.github.stenzek.duckstation; import android.Manifest; +import android.content.ContentResolver; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; @@ -33,7 +34,11 @@ import android.widget.ListView; import android.widget.PopupMenu; import android.widget.Toast; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.HashSet; import java.util.Set; import java.util.prefs.Preferences; @@ -43,6 +48,7 @@ import static com.google.android.material.snackbar.Snackbar.make; public class MainActivity extends AppCompatActivity { private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1; private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2; + private static final int REQUEST_IMPORT_BIOS_IMAGE = 3; private GameList mGameList; private ListView mGameListView; @@ -153,11 +159,11 @@ public class MainActivity extends AppCompatActivity { startAddGameDirectory(); } else if (id == R.id.action_scan_for_new_games) { mGameList.refresh(false, false); - } - if (id == R.id.action_rescan_all_games) { + } else if (id == R.id.action_rescan_all_games) { mGameList.refresh(true, true); - } - if (id == R.id.action_settings) { + } else if (id == R.id.action_import_bios) { + importBIOSImage(); + } else if (id == R.id.action_settings) { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); return true; @@ -202,6 +208,14 @@ public class MainActivity extends AppCompatActivity { mGameList.refresh(false, false); } break; + + case REQUEST_IMPORT_BIOS_IMAGE: { + if (resultCode != RESULT_OK) + return; + + onImportBIOSImageResult(data.getData()); + } + break; } } @@ -240,10 +254,65 @@ public class MainActivity extends AppCompatActivity { } private boolean startEmulation(String bootPath, boolean resumeState) { + if (!doBIOSCheck()) + return false; + Intent intent = new Intent(this, EmulationActivity.class); intent.putExtra("bootPath", bootPath); intent.putExtra("resumeState", resumeState); startActivity(intent); return true; } + + private boolean doBIOSCheck() { + if (AndroidHostInterface.getInstance().hasAnyBIOSImages()) + return true; + + new AlertDialog.Builder(this) + .setTitle("Missing BIOS Image") + .setMessage("No BIOS image was found in DuckStation's bios directory. Do you with to locate and import a BIOS image now?") + .setPositiveButton("Yes", (dialog, button) -> importBIOSImage()) + .setNegativeButton("No", (dialog, button) -> {}) + .create() + .show(); + + return false; + } + + private void importBIOSImage() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, "Choose BIOS Image"), REQUEST_IMPORT_BIOS_IMAGE); + } + + private void onImportBIOSImageResult(Uri uri) { + InputStream stream = null; + try { + stream = getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + Toast.makeText(this, "Failed to open BIOS image.", Toast.LENGTH_LONG); + return; + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[512 * 1024]; + int len; + while ((len = stream.read(buffer)) > 0) + os.write(buffer, 0, len); + } catch (IOException e) { + Toast.makeText(this, "Failed to read BIOS image.", Toast.LENGTH_LONG); + return; + } + + String importResult = AndroidHostInterface.getInstance().importBIOSImage(os.toByteArray()); + String message = (importResult == null) ? "This BIOS image is invalid, or has already been imported." : ("BIOS '" + importResult + "' imported."); + + new AlertDialog.Builder(this) + .setMessage(message) + .setPositiveButton("OK", (dialog, button) -> {}) + .create() + .show(); + } } diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml index 9ad26feca..d29b74200 100644 --- a/android/app/src/main/res/menu/menu_main.xml +++ b/android/app/src/main/res/menu/menu_main.xml @@ -20,6 +20,9 @@ + stream = FileSystem::OpenFile(filename, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); diff --git a/src/core/host_interface.h b/src/core/host_interface.h index b1e7166fe..9844cf532 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -22,8 +22,7 @@ class GameList; struct SystemBootParameters; -namespace BIOS -{ +namespace BIOS { struct ImageInfo; } @@ -131,6 +130,9 @@ public: /// Returns a list of filenames and descriptions for BIOS images in a directory. std::vector> FindBIOSImagesInDirectory(const char* directory); + /// Returns true if any BIOS images are found in the configured BIOS directory. + bool HasAnyBIOSImages(); + virtual void OnRunningGameChanged(); virtual void OnSystemPerformanceCountersUpdated();