diff --git a/android/app/src/cpp/CMakeLists.txt b/android/app/src/cpp/CMakeLists.txt index d8408d25c..bee415093 100644 --- a/android/app/src/cpp/CMakeLists.txt +++ b/android/app/src/cpp/CMakeLists.txt @@ -1,6 +1,8 @@ set(SRCS android_host_interface.cpp android_host_interface.h + android_progress_callback.cpp + android_progress_callback.h android_settings_interface.cpp android_settings_interface.h ) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index f6bd3f56e..f626afb8c 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -1,4 +1,5 @@ #include "android_host_interface.h" +#include "android_progress_callback.h" #include "common/assert.h" #include "common/audio_stream.h" #include "common/file_system.h" @@ -522,10 +523,10 @@ void AndroidHostInterface::SetControllerAxisState(u32 index, s32 button_code, fl false); } -void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database) +void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback) { m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface); - m_game_list->Refresh(invalidate_cache, invalidate_database); + m_game_list->Refresh(invalidate_cache, invalidate_database, progress_callback); } void AndroidHostInterface::ApplySettings(bool display_osd_messages) @@ -709,9 +710,10 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject } DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, - jboolean invalidate_database) + jboolean invalidate_database, jobject progress_callback) { - AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database); + AndroidProgressCallback cb(env, progress_callback); + AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database, &cb); } static const char* DiscRegionToString(DiscRegion region) diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index a2ae84012..af23d0557 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -1,6 +1,7 @@ #pragma once #include "android_settings_interface.h" #include "common/event.h" +#include "common/progress_callback.h" #include "frontend-common/common_host_interface.h" #include #include @@ -49,7 +50,7 @@ public: void SetControllerButtonState(u32 index, s32 button_code, bool pressed); void SetControllerAxisState(u32 index, s32 button_code, float value); - void RefreshGameList(bool invalidate_cache, bool invalidate_database); + void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback); void ApplySettings(bool display_osd_messages); protected: diff --git a/android/app/src/cpp/android_progress_callback.cpp b/android/app/src/cpp/android_progress_callback.cpp new file mode 100644 index 000000000..de03bd7db --- /dev/null +++ b/android/app/src/cpp/android_progress_callback.cpp @@ -0,0 +1,105 @@ +#include "android_progress_callback.h" +#include "android_host_interface.h" +#include "common/log.h" +#include "common/assert.h" +Log_SetChannel(AndroidProgressCallback); + +AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object) + : m_java_object(java_object) +{ + jclass cls = env->GetObjectClass(java_object); + m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V"); + m_set_status_text_method = env->GetMethodID(cls, "setStatusText", "(Ljava/lang/String;)V"); + m_set_progress_range_method = env->GetMethodID(cls, "setProgressRange", "(I)V"); + m_set_progress_value_method = env->GetMethodID(cls, "setProgressValue", "(I)V"); + m_modal_error_method = env->GetMethodID(cls, "modalError", "(Ljava/lang/String;)V"); + m_modal_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V"); + m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z"); + Assert(m_set_status_text_method && m_set_progress_range_method && m_set_progress_value_method && m_modal_error_method && m_modal_information_method && m_modal_confirmation_method); +} + +AndroidProgressCallback::~AndroidProgressCallback() = default; + +bool AndroidProgressCallback::IsCancelled() const +{ + return false; +} + +void AndroidProgressCallback::SetCancellable(bool cancellable) +{ + if (m_cancellable == cancellable) + return; + + BaseProgressCallback::SetCancellable(cancellable); +} + +void AndroidProgressCallback::SetTitle(const char* title) +{ + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring text_jstr = env->NewStringUTF(title); + env->CallVoidMethod(m_java_object, m_set_title_method, text_jstr); +} + +void AndroidProgressCallback::SetStatusText(const char* text) +{ + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring text_jstr = env->NewStringUTF(text); + env->CallVoidMethod(m_java_object, m_set_status_text_method, text_jstr); +} + +void AndroidProgressCallback::SetProgressRange(u32 range) +{ + BaseProgressCallback::SetProgressRange(range); + + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + env->CallVoidMethod(m_java_object, m_set_progress_range_method, static_cast(range)); +} + +void AndroidProgressCallback::SetProgressValue(u32 value) +{ + BaseProgressCallback::SetProgressValue(value); + + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + env->CallVoidMethod(m_java_object, m_set_progress_value_method, static_cast(value)); +} + +void AndroidProgressCallback::DisplayError(const char* message) +{ + Log_ErrorPrintf("%s", message); +} + +void AndroidProgressCallback::DisplayWarning(const char* message) +{ + Log_WarningPrintf("%s", message); +} + +void AndroidProgressCallback::DisplayInformation(const char* message) +{ + Log_InfoPrintf("%s", message); +} + +void AndroidProgressCallback::DisplayDebugMessage(const char* message) +{ + Log_DevPrintf("%s", message); +} + +void AndroidProgressCallback::ModalError(const char* message) +{ + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring message_jstr = env->NewStringUTF(message); + env->CallVoidMethod(m_java_object, m_modal_error_method, message_jstr); +} + +bool AndroidProgressCallback::ModalConfirmation(const char* message) +{ + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring message_jstr = env->NewStringUTF(message); + return env->CallBooleanMethod(m_java_object, m_modal_confirmation_method, message_jstr); +} + +void AndroidProgressCallback::ModalInformation(const char* message) +{ + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring message_jstr = env->NewStringUTF(message); + env->CallVoidMethod(m_java_object, m_modal_information_method, message_jstr); +} diff --git a/android/app/src/cpp/android_progress_callback.h b/android/app/src/cpp/android_progress_callback.h new file mode 100644 index 000000000..8b15bca18 --- /dev/null +++ b/android/app/src/cpp/android_progress_callback.h @@ -0,0 +1,38 @@ +#pragma once +#include "common/progress_callback.h" +#include + +class AndroidProgressCallback final : public BaseProgressCallback +{ +public: + AndroidProgressCallback(JNIEnv* env, jobject java_object); + ~AndroidProgressCallback(); + + bool IsCancelled() const override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + +private: + jobject m_java_object; + + jmethodID m_set_title_method; + jmethodID m_set_status_text_method; + jmethodID m_set_progress_range_method; + jmethodID m_set_progress_value_method; + jmethodID m_modal_error_method; + jmethodID m_modal_confirmation_method; + jmethodID m_modal_information_method; +}; 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 baedc0c2a..d7cc59eca 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 @@ -55,7 +55,7 @@ public class AndroidHostInterface { public static native int getControllerAxisCode(String controllerType, String axisName); - public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase); + public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase, AndroidProgressCallback progressCallback); public native GameListEntry[] getGameListEntries(); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java new file mode 100644 index 000000000..d6a676701 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidProgressCallback.java @@ -0,0 +1,140 @@ +package com.github.stenzek.duckstation; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; + +public class AndroidProgressCallback { + private Activity mContext; + private ProgressDialog mDialog; + + public AndroidProgressCallback(Activity context) { + mContext = context; + mDialog = new ProgressDialog(context); + mDialog.setMessage("Please wait..."); + mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + mDialog.setIndeterminate(false); + mDialog.setMax(100); + mDialog.setProgress(0); + mDialog.show(); + } + + public void dismiss() { + mDialog.dismiss(); + } + + public void setTitle(String text) { + mContext.runOnUiThread(() -> { + mDialog.setTitle(text); + }); + } + public void setStatusText(String text) { + mContext.runOnUiThread(() -> { + mDialog.setMessage(text); + }); + } + + public void setProgressRange(int range) { + mContext.runOnUiThread(() -> { + mDialog.setMax(range); + }); + } + + public void setProgressValue(int value) { + mContext.runOnUiThread(() -> { + mDialog.setProgress(value); + }); + } + + public void modalError(String message) { + Object lock = new Object(); + mContext.runOnUiThread(() -> { + new AlertDialog.Builder(mContext) + .setTitle("Error") + .setMessage(message) + .setPositiveButton("OK", (dialog, button) -> { + dialog.dismiss(); + }) + .setOnDismissListener((dialogInterface) -> { + synchronized (lock) { + lock.notify(); + } + }) + .create() + .show(); + }); + + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + } + } + } + + public void modalInformation(String message) { + Object lock = new Object(); + mContext.runOnUiThread(() -> { + new AlertDialog.Builder(mContext) + .setTitle("Information") + .setMessage(message) + .setPositiveButton("OK", (dialog, button) -> { + dialog.dismiss(); + }) + .setOnDismissListener((dialogInterface) -> { + synchronized (lock) { + lock.notify(); + } + }) + .create() + .show(); + }); + + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + } + } + } + + private class ConfirmationResult { + public boolean result = false; + } + + public boolean modalConfirmation(String message) { + ConfirmationResult result = new ConfirmationResult(); + mContext.runOnUiThread(() -> { + new AlertDialog.Builder(mContext) + .setTitle("Confirmation") + .setMessage(message) + .setPositiveButton("Yes", (dialog, button) -> { + result.result = true; + dialog.dismiss(); + }) + .setNegativeButton("No", (dialog, button) -> { + result.result = false; + dialog.dismiss(); + }) + .setOnDismissListener((dialogInterface) -> { + synchronized (result) { + result.notify(); + } + }) + .create() + .show(); + }); + + synchronized (result) { + try { + result.wait(); + } catch (InterruptedException e) { + } + } + + return result.result; + } +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java index ec23b9c56..051720d39 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java @@ -3,6 +3,7 @@ package com.github.stenzek.duckstation; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; +import android.os.AsyncTask; import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; @@ -17,11 +18,11 @@ import java.util.Comparator; import java.util.Set; public class GameList { - private Context mContext; + private Activity mContext; private GameListEntry[] mEntries; private ListViewAdapter mAdapter; - public GameList(Context context) { + public GameList(Activity context) { mContext = context; mAdapter = new ListViewAdapter(); mEntries = new GameListEntry[0]; @@ -35,12 +36,20 @@ public class GameList { } - public void refresh(boolean invalidateCache, boolean invalidateDatabase) { + public void refresh(boolean invalidateCache, boolean invalidateDatabase, Activity parentActivity) { // Search and get entries from native code - AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase); - mEntries = AndroidHostInterface.getInstance().getGameListEntries(); - Arrays.sort(mEntries, new GameListEntryComparator()); - mAdapter.notifyDataSetChanged(); + AndroidProgressCallback progressCallback = new AndroidProgressCallback(mContext); + AsyncTask.execute(() -> { + AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase, progressCallback); + GameListEntry[] newEntries = AndroidHostInterface.getInstance().getGameListEntries(); + Arrays.sort(newEntries, new GameListEntryComparator()); + + mContext.runOnUiThread(() -> { + progressCallback.dismiss(); + mEntries = newEntries; + mAdapter.notifyDataSetChanged(); + }); + }); } public int getEntryCount() { 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 f6425c2a3..ce4324658 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 @@ -127,7 +127,7 @@ public class MainActivity extends AppCompatActivity { throw new RuntimeException("Failed to create host interface"); } - mGameList.refresh(false, false); + mGameList.refresh(false, false, this); } private void startAddGameDirectory() { @@ -163,9 +163,9 @@ public class MainActivity extends AppCompatActivity { } else if (id == R.id.action_add_game_directory) { startAddGameDirectory(); } else if (id == R.id.action_scan_for_new_games) { - mGameList.refresh(false, false); + mGameList.refresh(false, false, this); } else if (id == R.id.action_rescan_all_games) { - mGameList.refresh(true, true); + mGameList.refresh(true, true, this); } else if (id == R.id.action_import_bios) { importBIOSImage(); } else if (id == R.id.action_settings) { @@ -210,7 +210,7 @@ public class MainActivity extends AppCompatActivity { editor.putStringSet("GameList/RecursivePaths", currentValues); editor.apply(); Log.i("MainActivity", "Added path '" + path + "' to game list search directories"); - mGameList.refresh(false, false); + mGameList.refresh(false, false, this); } break;