diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 7c64b9b84..df2e0f0ff 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -28,6 +28,7 @@ Log_SetChannel(AndroidHostInterface); #endif static JavaVM* s_jvm; +static jclass s_String_class; static jclass s_AndroidHostInterface_class; static jmethodID s_AndroidHostInterface_constructor; static jfieldID s_AndroidHostInterface_field_mNativePointer; @@ -618,7 +619,10 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) // Create global reference so it doesn't get cleaned up. JNIEnv* env = AndroidHelpers::GetJNIEnv(); - if ((s_AndroidHostInterface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == + if ((s_String_class = env->FindClass("java/lang/String")) == nullptr || + (s_String_class = static_cast(env->NewGlobalRef(s_String_class))) == + nullptr || + (s_AndroidHostInterface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr || (s_AndroidHostInterface_class = static_cast(env->NewGlobalRef(s_AndroidHostInterface_class))) == nullptr || @@ -1011,3 +1015,46 @@ DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_importBIOSImage, jobject ob else return env->NewStringUTF(hash.ToString().c_str()); } + +DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getMediaPlaylistPaths, jobject obj) +{ + if (!System::IsValid()) + return nullptr; + + const u32 count = System::GetMediaPlaylistCount(); + if (count == 0) + return nullptr; + + jobjectArray arr = env->NewObjectArray(static_cast(count), s_String_class, nullptr); + for (u32 i = 0; i < count; i++) + { + jstring str = env->NewStringUTF(System::GetMediaPlaylistPath(i).c_str()); + env->SetObjectArrayElement(arr, static_cast(i), str); + } + + return arr; +} + +DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getMediaPlaylistIndex, jobject obj) +{ + if (!System::IsValid()) + return -1; + + return System::GetMediaPlaylistIndex(); +} + +DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_setMediaPlaylistIndex, jobject obj, jint index) +{ + if (!System::IsValid() || index < 0 || static_cast(index) >= System::GetMediaPlaylistCount()) + return false; + + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([index, hi]() { + if (System::IsValid()) { + if (!System::SwitchMediaFromPlaylist(index)) + hi->AddOSDMessage("Disc switch failed. Please make sure the file exists."); + } + }); + + return true; +} \ No newline at end of file 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 f73fb0557..a558ccd7e 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 @@ -83,6 +83,10 @@ public class AndroidHostInterface { public native boolean isFastForwardEnabled(); public native void setFastForwardEnabled(boolean enabled); + public native String[] getMediaPlaylistPaths(); + public native int getMediaPlaylistIndex(); + public native boolean setMediaPlaylistIndex(int index); + static { System.loadLibrary("duckstation-native"); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index d7a4dd1ca..93daa9f73 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -409,7 +409,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde case 2: // Change Disc { - onMenuClosed(); + showDiscChangeMenu(); return; } @@ -482,6 +482,30 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } } + private void showDiscChangeMenu() { + final String[] paths = AndroidHostInterface.getInstance().getMediaPlaylistPaths(); + final int currentPath = AndroidHostInterface.getInstance().getMediaPlaylistIndex(); + if (paths == null) + { + onMenuClosed(); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + + CharSequence[] items = new CharSequence[paths.length]; + for (int i = 0; i < paths.length; i++) + items[i] = GameListEntry.getFileNameForPath(paths[i]); + + builder.setSingleChoiceItems(items, currentPath, (dialogInterface, i) -> { + AndroidHostInterface.getInstance().setMediaPlaylistIndex(i); + dialogInterface.dismiss(); + onMenuClosed(); + }); + builder.setOnCancelListener(dialogInterface -> onMenuClosed()); + builder.create().show(); + } + /** * Touchscreen controller overlay */ diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java index 2936fa3bd..9c29de547 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java @@ -9,7 +9,8 @@ import androidx.core.content.ContextCompat; public class GameListEntry { public enum EntryType { Disc, - PSExe + PSExe, + Playlist } public enum CompatibilityRating { @@ -90,15 +91,17 @@ public class GameListEntry { return mCompatibilityRating; } - private String getSubTitle() { - String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0); - String fileName; - int lastSlash = mPath.lastIndexOf('/'); - if (lastSlash > 0 && lastSlash < mPath.length() - 1) - fileName = mPath.substring(lastSlash + 1); + public static String getFileNameForPath(String path) { + int lastSlash = path.lastIndexOf('/'); + if (lastSlash > 0 && lastSlash < path.length() - 1) + return path.substring(lastSlash + 1); else - fileName = mPath; + return path; + } + private String getSubTitle() { + String fileName = getFileNameForPath(mPath); + String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0); return String.format("%s (%s)", fileName, sizeString); } @@ -134,6 +137,10 @@ public class GameListEntry { case PSExe: typeDrawableId = R.drawable.ic_emblem_system; break; + + case Playlist: + typeDrawableId = R.drawable.ic_baseline_playlist_play_24; + break; } ((ImageView) view.findViewById(R.id.game_list_view_entry_type_icon)) diff --git a/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml b/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml new file mode 100644 index 000000000..d663aeb3d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_playlist_play_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/frontend-common/game_list.cpp b/src/frontend-common/game_list.cpp index 66829c5b1..d571e84d6 100644 --- a/src/frontend-common/game_list.cpp +++ b/src/frontend-common/game_list.cpp @@ -25,7 +25,7 @@ GameList::~GameList() = default; const char* GameList::EntryTypeToString(GameListEntryType type) { - static std::array names = {{"Disc", "PSExe"}}; + static std::array names = {{"Disc", "PSExe", "Playlist"}}; return names[static_cast(type)]; }