diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index 6abbc4c0..15141c95 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -63,11 +63,6 @@ extern "C" JNIEXPORT void JNICALL Java_info_cemu_Cemu_NativeLibrary_setSurfaceSize(JNIEnv *env, jclass clazz, jint width, jint height, jboolean is_main_canvas) { - WindowHandleInfo *windowHandleInfo; - if (is_main_canvas) - windowHandleInfo = &gui_getWindowInfo().canvas_main; - else - windowHandleInfo = &gui_getWindowInfo().canvas_pad; gui_getWindowInfo().width = width; gui_getWindowInfo().height = height; } @@ -125,18 +120,34 @@ public: : m_onGameIconLoadedMID(onGameIconLoadedMID), m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {} - void onIconLoaded(TitleId titleId, const Image &iconData) override { + void onIconLoaded(TitleId titleId) override { JNIUtils::ScopedJNIENV env; jlong jTitleId = *reinterpret_cast(&titleId); - jintArray jIconData = env->NewIntArray(iconData.m_width * iconData.m_height); - env->SetIntArrayRegion(jIconData, 0, iconData.m_width * iconData.m_height, - reinterpret_cast(iconData.m_image)); - env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, jTitleId, - jIconData, iconData.m_width, iconData.m_height); - env->DeleteLocalRef(jIconData); + env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, jTitleId); } }; +extern "C" +JNIEXPORT jobject JNICALL +Java_info_cemu_Cemu_NativeLibrary_getGameIcon(JNIEnv *env, jclass clazz, jlong title_id) { + TitleId titleId = *reinterpret_cast(&title_id); + const Image &iconData = gui_getGameIcon(titleId); + jintArray jIconData = env->NewIntArray(iconData.m_width * iconData.m_height); + env->SetIntArrayRegion(jIconData, 0, iconData.m_width * iconData.m_height, + reinterpret_cast(iconData.m_image)); + jclass bitmapClass = env->FindClass("android/graphics/Bitmap"); + + jmethodID createBitmapMethodID = env->GetStaticMethodID(bitmapClass, "createBitmap", + "([IIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); + jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config"); + jfieldID argb8888FieldID = env->GetStaticFieldID(bitmapConfigClass, "ARGB_8888", + "Landroid/graphics/Bitmap$Config;"); + jobject argb8888Obj = env->GetStaticObjectField(bitmapConfigClass, argb8888FieldID); + jobject bitmapObj = env->CallStaticObjectMethod(bitmapClass, createBitmapMethodID, jIconData, + iconData.m_width, + iconData.m_height, argb8888Obj); + return bitmapObj; +} extern "C" JNIEXPORT void JNICALL Java_info_cemu_Cemu_NativeLibrary_setGameIconLoadedCallback(JNIEnv *env, jclass clazz, @@ -144,8 +155,7 @@ Java_info_cemu_Cemu_NativeLibrary_setGameIconLoadedCallback(JNIEnv *env, jclass jclass gameIconLoadedCallbackClass = env->GetObjectClass(game_icon_loaded_callback); jmethodID gameIconLoadedMID = env->GetMethodID(gameIconLoadedCallbackClass, "onGameIconLoaded", - "(J[III)V"); - env->DeleteLocalRef(gameIconLoadedCallbackClass); + "(J)V"); g_gameIconLoadedCallback = std::make_shared(gameIconLoadedMID, game_icon_loaded_callback); gui_setOnGameIconLoaded(g_gameIconLoadedCallback); @@ -183,7 +193,6 @@ class AndroidFilesystemCallbacks : public FilesystemAndroid::FilesystemCallbacks jmethodID m_existsMid; JNIUtils::Scopedjclass m_fileUtilClass; std::function m_function = nullptr; - std::atomic_bool m_functionFinished; std::mutex m_functionMutex; std::condition_variable m_functionCV; std::thread m_thread; @@ -192,19 +201,20 @@ class AndroidFilesystemCallbacks : public FilesystemAndroid::FilesystemCallbacks std::atomic_bool m_continue = true; bool callBooleanFunction(const std::filesystem::path &uri, jmethodID methodId) { - std::unique_lock functionLock(m_functionMutex); - m_functionFinished = false; - bool condition; - { - std::lock_guard threadLock(m_threadMutex); - m_function = [&, this](JNIEnv *env) { - jstring uriString = env->NewStringUTF(uri.c_str()); - condition = env->CallStaticBooleanMethod(*m_fileUtilClass, methodId, uriString); - env->DeleteLocalRef(uriString); - }; - } + std::lock_guard lock(m_functionMutex); + bool functionFinished = false; + bool condition = false; + m_function = [&, this](JNIEnv *env) { + jstring uriString = env->NewStringUTF(uri.c_str()); + condition = env->CallStaticBooleanMethod(*m_fileUtilClass, methodId, uriString); + env->DeleteLocalRef(uriString); + functionFinished = true; + }; m_threadCV.notify_one(); - m_functionCV.wait(functionLock, [this]() -> bool { return m_functionFinished; }); + { + std::unique_lock threadLock(m_threadMutex); + m_functionCV.wait(threadLock, [&]() -> bool { return functionFinished; }); + } return condition; } @@ -233,7 +243,6 @@ public: return; m_function(*env); m_function = nullptr; - m_functionFinished = true; m_functionCV.notify_one(); } }); @@ -246,48 +255,51 @@ public: } int openContentUri(const std::filesystem::path &uri) override { - std::unique_lock functionLock(m_functionMutex); - m_functionFinished = false; - int fd; - { - std::lock_guard threadLock(m_threadMutex); - m_function = [&, this](JNIEnv *env) { - jstring uriString = env->NewStringUTF(uri.c_str()); - fd = env->CallStaticIntMethod(*m_fileUtilClass, m_openContentUriMid, uriString); - env->DeleteLocalRef(uriString); - }; - } + std::lock_guard lock(m_functionMutex); + bool functionFinished = false; + int fd = -1; + m_function = [&, this](JNIEnv *env) { + jstring uriString = env->NewStringUTF(uri.c_str()); + fd = env->CallStaticIntMethod(*m_fileUtilClass, m_openContentUriMid, uriString); + env->DeleteLocalRef(uriString); + functionFinished = true; + }; m_threadCV.notify_one(); - m_functionCV.wait(functionLock, [this]() -> bool { return m_functionFinished; }); + { + std::unique_lock threadLock(m_threadMutex); + m_functionCV.wait(threadLock, [&]() { return functionFinished; }); + } return fd; } std::vector listFiles(const std::filesystem::path &uri) override { - std::unique_lock functionLock(m_functionMutex); - m_functionFinished = false; + std::lock_guard lock(m_functionMutex); + bool functionFinished = false; std::vector paths; - { - std::lock_guard threadLock(m_threadMutex); - m_function = [&, this](JNIEnv *env) { - jstring uriString = env->NewStringUTF(uri.c_str()); - jobjectArray pathsObjArray = static_cast(env->CallStaticObjectMethod( - *m_fileUtilClass, - m_listFilesMid, uriString)); - env->DeleteLocalRef(uriString); - jsize arrayLength = env->GetArrayLength(pathsObjArray); - paths.reserve(arrayLength); - for (jsize i = 0; i < arrayLength; i++) { - jstring pathStr = static_cast(env->GetObjectArrayElement( - pathsObjArray, - i)); - paths.push_back(JNIUtils::JStringToString(env, pathStr)); - env->DeleteLocalRef(pathStr); - } - env->DeleteLocalRef(pathsObjArray); - }; - } + m_function = [&, this](JNIEnv *env) { + jstring uriString = env->NewStringUTF(uri.c_str()); + jobjectArray pathsObjArray = static_cast(env->CallStaticObjectMethod( + *m_fileUtilClass, + m_listFilesMid, uriString)); + env->DeleteLocalRef(uriString); + jsize arrayLength = env->GetArrayLength(pathsObjArray); + paths.reserve(arrayLength); + for (jsize i = 0; i < arrayLength; i++) { + jstring pathStr = static_cast(env->GetObjectArrayElement( + pathsObjArray, + i)); + paths.push_back(JNIUtils::JStringToString(env, pathStr)); + env->DeleteLocalRef(pathStr); + } + env->DeleteLocalRef(pathsObjArray); + functionFinished = true; + }; m_threadCV.notify_one(); - m_functionCV.wait(functionLock, [this]() -> bool { return m_functionFinished; }); + + { + std::unique_lock threadLock(m_threadMutex); + m_functionCV.wait(threadLock, [&]() { return functionFinished; }); + } return paths; } @@ -618,4 +630,4 @@ Java_info_cemu_Cemu_NativeLibrary_setAudioDeviceVolume(JNIEnv *env, jclass clazz auto &deviceVolume = tv ? g_config.data().tv_volume : g_config.data().pad_volume; deviceVolume = volume; g_config.Save(); -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/Game.java b/src/android/app/src/main/java/info/cemu/Cemu/Game.java index 68bda1f8..c699be7c 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/Game.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/Game.java @@ -12,8 +12,8 @@ public class Game { this.title = title; } - public void setIconData(int[] colors, int width, int height) { - icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + public void setIconData(Bitmap icon) { + this.icon = icon; } public Bitmap getIcon() { diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java index 7df4a747..6605b9ca 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java @@ -1,23 +1,55 @@ package info.cemu.Cemu; +import android.graphics.Bitmap; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - +import java.util.ArrayList; import java.util.HashMap; -import java.util.Vector; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -public class GameAdapter extends RecyclerView.Adapter { - HashMap gameInfoMap; - Vector gameTitleIds; +public class GameAdapter extends RecyclerView.Adapter implements Filterable { + private final Map gameInfoMap; + private final List gameTitleIds; + private List filteredGameTitleIds; private final GameTitleClickAction gameTitleClickAction; + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence charSequence) { + String gameTitle = charSequence.toString(); + if (gameTitle.isEmpty()) + filteredGameTitleIds = gameTitleIds; + else + filteredGameTitleIds = gameTitleIds.stream() + .filter(titleId -> gameInfoMap.get(titleId).title.toLowerCase().contains(gameTitle.toLowerCase())) + .collect(Collectors.toList()); + FilterResults filterResults = new FilterResults(); + filterResults.values = filteredGameTitleIds; + return filterResults; + } + + @Override + protected void publishResults(CharSequence charSequence, FilterResults filterResults) { + filteredGameTitleIds = (List) filterResults.values; + notifyDataSetChanged(); + } + }; + } + + public interface GameTitleClickAction { void action(long titleId); } @@ -25,7 +57,8 @@ public class GameAdapter extends RecyclerView.Adapter { public GameAdapter(GameTitleClickAction gameTitleClickAction) { super(); this.gameTitleClickAction = gameTitleClickAction; - gameTitleIds = new Vector<>(); + gameTitleIds = new ArrayList<>(); + filteredGameTitleIds = new ArrayList<>(); gameInfoMap = new HashMap<>(); } @@ -38,7 +71,7 @@ public class GameAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) { - Game game = gameInfoMap.get(gameTitleIds.get(position)); + Game game = gameInfoMap.get(filteredGameTitleIds.get(position)); if (game != null) { holder.icon.setImageBitmap(game.getIcon()); holder.text.setText(game.getTitle()); @@ -51,27 +84,28 @@ public class GameAdapter extends RecyclerView.Adapter { @Override public int getItemCount() { - return gameTitleIds.size(); + return filteredGameTitleIds.size(); } void clear() { - int itemCount = gameTitleIds.size(); - notifyItemRangeRemoved(0, itemCount); + notifyDataSetChanged(); gameInfoMap.clear(); gameTitleIds.clear(); + filteredGameTitleIds.clear(); } void addGameInfo(long titleId, String title) { - gameTitleIds.addElement(titleId); + gameTitleIds.add(titleId); gameInfoMap.put(titleId, new Game(titleId, title)); - notifyItemInserted(gameTitleIds.size() - 1); + filteredGameTitleIds = gameTitleIds; + notifyDataSetChanged(); } - void setGameIcon(long titleId, int[] colors, int width, int height) { + void setGameIcon(long titleId, Bitmap icon) { Game game = gameInfoMap.get(titleId); if (game != null) { - game.setIconData(colors, width, height); - notifyItemChanged(gameTitleIds.indexOf(titleId)); + game.setIconData(icon); + notifyDataSetChanged(); } } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java index 8d3bb56e..31281c98 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java @@ -3,13 +3,19 @@ package info.cemu.Cemu; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -41,6 +47,19 @@ public class GamesFragment extends Fragment { intent.putExtra(EmulationActivity.GAME_TITLE_ID, titleId); startActivity(intent); }); + gameAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + if (gameAdapter.getItemCount() == 0) { + if (!binding.searchText.getText().toString().isEmpty()) + binding.gamesListMessage.setText(R.string.no_games_found); + binding.gamesListMessage.setVisibility(View.VISIBLE); + + } else { + binding.gamesListMessage.setVisibility(View.GONE); + } + } + }); binding.gamesSwipeRefresh.setOnRefreshListener(() -> { gameAdapter.clear(); NativeLibrary.reloadGameTitles(); @@ -53,10 +72,30 @@ public class GamesFragment extends Fragment { NativeLibrary.requestGameIcon(titleId); }); }); - NativeLibrary.setGameIconLoadedCallback((titleId, colors, width, height) -> { - requireActivity().runOnUiThread(() -> gameAdapter.setGameIcon(titleId, colors, width, height)); + NativeLibrary.setGameIconLoadedCallback((titleId) -> { + Bitmap icon = NativeLibrary.getGameIcon(titleId); + requireActivity().runOnUiThread(() -> gameAdapter.setGameIcon(titleId, icon)); }); NativeLibrary.reloadGameTitles(); + + binding.searchText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + Log.i("TextChanged", charSequence.toString()); + gameAdapter.getFilter().filter(charSequence.toString()); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + return rootView; } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index 18da9436..c44390e4 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -1,5 +1,6 @@ package info.cemu.Cemu; +import android.graphics.Bitmap; import android.view.KeyEvent; import android.view.Surface; @@ -49,13 +50,15 @@ public class NativeLibrary { public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback); public interface GameIconLoadedCallback { - void onGameIconLoaded(long titleId, int[] colors, int width, int height); + void onGameIconLoaded(long titleId); } public static native void setGameIconLoadedCallback(GameIconLoadedCallback gameIconLoadedCallback); public static native void requestGameIcon(long titleId); + public static native Bitmap getGameIcon(long titleId); + public static native void reloadGameTitles(); public static native void initializeActiveSettings(String dataPath, String cachePath); diff --git a/src/android/app/src/main/res/drawable/ic_search.xml b/src/android/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..852066ce --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml index d80fcb10..f010ae43 100644 --- a/src/android/app/src/main/res/layout/fragment_games.xml +++ b/src/android/app/src/main/res/layout/fragment_games.xml @@ -1,14 +1,63 @@ + android:layout_height="match_parent" + android:padding="8dp"> - + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e0963e2a..edc3f375 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -91,4 +91,6 @@ Gamepad channels TV volume Gamepad volume + Search games + No games were found \ No newline at end of file diff --git a/src/gui/AndroidGui/AndroidGuiWrapper.cpp b/src/gui/AndroidGui/AndroidGuiWrapper.cpp index d0c87f3f..f8973b3e 100644 --- a/src/gui/AndroidGui/AndroidGuiWrapper.cpp +++ b/src/gui/AndroidGui/AndroidGuiWrapper.cpp @@ -16,6 +16,11 @@ void gui_setOnGameTitleLoaded(const std::shared_ptr &on g_gameTitleLoader.setOnTitleLoaded(onGameTitleLoaded); } +const Image& gui_getGameIcon(TitleId titleId) +{ + return g_gameIconLoader.getGameIcon(titleId); +} + void gui_setOnGameIconLoaded(const std::shared_ptr& onGameIconLoaded) { g_gameIconLoader.setOnIconLoaded(onGameIconLoaded); diff --git a/src/gui/AndroidGui/AndroidGuiWrapper.h b/src/gui/AndroidGui/AndroidGuiWrapper.h index 66a044e0..9a64bd01 100644 --- a/src/gui/AndroidGui/AndroidGuiWrapper.h +++ b/src/gui/AndroidGui/AndroidGuiWrapper.h @@ -2,13 +2,17 @@ #include "Cafe/TitleList/TitleId.h" #include "gui/guiWrapper.h" +#include "Image.h" void gui_startGame(TitleId titleId); + void gui_setOnGameTitleLoaded(const std::shared_ptr &onGameTitleLoaded); void gui_setOnGameIconLoaded(const std::shared_ptr& onGameIconLoaded); +const Image& gui_getGameIcon(TitleId titleId); + void gui_requestGameIcon(TitleId titleId); void gui_reloadGameTitles(); diff --git a/src/gui/AndroidGui/GameIconLoader.cpp b/src/gui/AndroidGui/GameIconLoader.cpp index cfa83fdc..af33a2a6 100644 --- a/src/gui/AndroidGui/GameIconLoader.cpp +++ b/src/gui/AndroidGui/GameIconLoader.cpp @@ -13,6 +13,10 @@ GameIconLoader::~GameIconLoader() { m_loaderThread.join(); } +const Image &GameIconLoader::getGameIcon(TitleId titleId) { + return m_iconCache.at(titleId); +} + void GameIconLoader::requestIcon(TitleId titleId) { { std::lock_guard lock(m_threadMutex); @@ -39,7 +43,7 @@ void GameIconLoader::loadGameIcons() { continue; if (auto iconIt = m_iconCache.find(titleId); iconIt != m_iconCache.end()) { - if (m_onIconLoaded) m_onIconLoaded->onIconLoaded(titleId, iconIt->second); + if (m_onIconLoaded) m_onIconLoaded->onIconLoaded(titleId); continue; } @@ -51,8 +55,8 @@ void GameIconLoader::loadGameIcons() { int width, height, channels; Image image(tgaData.value()); if (image.isOk()) { - if (m_onIconLoaded) m_onIconLoaded->onIconLoaded(titleId, image); m_iconCache.emplace(titleId, std::move(image)); + if (m_onIconLoaded) m_onIconLoaded->onIconLoaded(titleId); } } else { cemuLog_log(LogType::Force, "Failed to load icon for title {:016x}", titleId); diff --git a/src/gui/AndroidGui/GameIconLoader.h b/src/gui/AndroidGui/GameIconLoader.h index cf0473d1..866f4b30 100644 --- a/src/gui/AndroidGui/GameIconLoader.h +++ b/src/gui/AndroidGui/GameIconLoader.h @@ -5,7 +5,7 @@ class GameIconLoadedCallback { public: - virtual void onIconLoaded(TitleId titleId, const Image &iconData) = 0; + virtual void onIconLoaded(TitleId titleId) = 0; }; class GameIconLoader { @@ -21,6 +21,8 @@ public: ~GameIconLoader(); + const Image &getGameIcon(TitleId titleId); + void setOnIconLoaded(const std::shared_ptr &onIconLoaded); void requestIcon(TitleId titleId);