Add option to search for games

This commit is contained in:
SSimco
2023-07-06 06:53:56 +03:00
parent 26f5d8dfab
commit fb87aa7261
12 changed files with 257 additions and 93 deletions
+76 -64
View File
@@ -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<jlong *>(&titleId);
jintArray jIconData = env->NewIntArray(iconData.m_width * iconData.m_height);
env->SetIntArrayRegion(jIconData, 0, iconData.m_width * iconData.m_height,
reinterpret_cast<const jint *>(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<TitleId *>(&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<const jint *>(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<AndroidGameIconLoadedCallback>(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<void(JNIEnv *)> 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<std::filesystem::path> 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<std::filesystem::path> paths;
{
std::lock_guard threadLock(m_threadMutex);
m_function = [&, this](JNIEnv *env) {
jstring uriString = env->NewStringUTF(uri.c_str());
jobjectArray pathsObjArray = static_cast<jobjectArray>(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<jstring>(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<jobjectArray>(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<jstring>(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();
}
}
@@ -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() {
@@ -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<GameAdapter.ViewHolder> {
HashMap<Long, Game> gameInfoMap;
Vector<Long> gameTitleIds;
public class GameAdapter extends RecyclerView.Adapter<GameAdapter.ViewHolder> implements Filterable {
private final Map<Long, Game> gameInfoMap;
private final List<Long> gameTitleIds;
private List<Long> 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<Long>) filterResults.values;
notifyDataSetChanged();
}
};
}
public interface GameTitleClickAction {
void action(long titleId);
}
@@ -25,7 +57,8 @@ public class GameAdapter extends RecyclerView.Adapter<GameAdapter.ViewHolder> {
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<GameAdapter.ViewHolder> {
@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<GameAdapter.ViewHolder> {
@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();
}
}
@@ -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;
}
@@ -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);
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M796,839L533,576Q503,602 463.04,616.5Q423.08,631 378,631Q269.84,631 194.92,556Q120,481 120,375Q120,269 195,194Q270,119 376.5,119Q483,119 557.5,194Q632,269 632,375.15Q632,418 618,458Q604,498 576,533L840,795L796,839ZM377,571Q458.25,571 515.13,513.5Q572,456 572,375Q572,294 515.13,236.5Q458.25,179 377,179Q294.92,179 237.46,236.5Q180,294 180,375Q180,456 237.46,513.5Q294.92,571 377,571Z"/>
</vector>
@@ -1,14 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/games_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/games_recycler_view"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/layout_game" />
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="8dp"
app:cardCornerRadius="30dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/game_search_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_search" />
<EditText
android:id="@+id/search_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:hint="@string/search_games"
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/games_list_message"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/games_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -91,4 +91,6 @@
<string name="gamepad_channels">Gamepad channels</string>
<string name="tv_volume">TV volume</string>
<string name="pad_volume">Gamepad volume</string>
<string name="search_games">Search games</string>
<string name="no_games_found">No games were found</string>
</resources>
+5
View File
@@ -16,6 +16,11 @@ void gui_setOnGameTitleLoaded(const std::shared_ptr<GameTitleLoadedCallback> &on
g_gameTitleLoader.setOnTitleLoaded(onGameTitleLoaded);
}
const Image& gui_getGameIcon(TitleId titleId)
{
return g_gameIconLoader.getGameIcon(titleId);
}
void gui_setOnGameIconLoaded(const std::shared_ptr<class GameIconLoadedCallback>& onGameIconLoaded)
{
g_gameIconLoader.setOnIconLoaded(onGameIconLoaded);
+4
View File
@@ -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<class GameTitleLoadedCallback> &onGameTitleLoaded);
void gui_setOnGameIconLoaded(const std::shared_ptr<class GameIconLoadedCallback>& onGameIconLoaded);
const Image& gui_getGameIcon(TitleId titleId);
void gui_requestGameIcon(TitleId titleId);
void gui_reloadGameTitles();
+6 -2
View File
@@ -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);
+3 -1
View File
@@ -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 <GameIconLoadedCallback> &onIconLoaded);
void requestIcon(TitleId titleId);