android: frontend: Implement game grid view. (#9)

This commit is contained in:
bunnei 2023-02-12 00:17:19 -08:00
parent 5ed8d46340
commit 0e52d11ede
15 changed files with 272 additions and 174 deletions

View file

@ -11,7 +11,7 @@ def abiFilter = "arm64-v8a" //, "x86"
android { android {
compileSdkVersion 32 compileSdkVersion 32
ndkVersion "25.1.8937393" ndkVersion "25.2.9519653"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8

View file

@ -141,9 +141,9 @@ public final class NativeLibrary {
* Gets the embedded icon within the given ROM. * Gets the embedded icon within the given ROM.
* *
* @param filename the file path to the ROM. * @param filename the file path to the ROM.
* @return an integer array containing the color data for the icon. * @return a byte array containing the JPEG data for the icon.
*/ */
public static native int[] GetIcon(String filename); public static native byte[] GetIcon(String filename);
/** /**
* Gets the embedded title of the given ISO/ROM. * Gets the embedded title of the given ISO/ROM.
@ -204,6 +204,11 @@ public final class NativeLibrary {
*/ */
public static native void StopEmulation(); public static native void StopEmulation();
/**
* Resets the in-memory ROM metadata cache.
*/
public static native void ResetRomMetadata();
/** /**
* Returns true if emulation is running (or is paused). * Returns true if emulation is running (or is paused).
*/ */

View file

@ -86,11 +86,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
String filename = FileUtil.getFilename(YuzuApplication.getAppContext(), filepath);
holder.textFileName.setText(filename);
// TODO These shouldn't be necessary once the move to a DB-based model is complete. // TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
@ -98,7 +94,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION);
final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled; final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled;
View itemView = holder.getItemView(); View itemView = holder.getItemView();

View file

@ -47,7 +47,7 @@ public final class Game {
cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
cursor.getString(GameDatabase.GAME_COLUMN_PATH), cursor.getString(GameDatabase.GAME_COLUMN_PATH),
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); cursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
} }
public String getTitle() { public String getTitle() {

View file

@ -29,7 +29,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
public static final int GAME_COLUMN_DESCRIPTION = 3; public static final int GAME_COLUMN_DESCRIPTION = 3;
public static final int GAME_COLUMN_REGIONS = 4; public static final int GAME_COLUMN_REGIONS = 4;
public static final int GAME_COLUMN_GAME_ID = 5; public static final int GAME_COLUMN_GAME_ID = 5;
public static final int GAME_COLUMN_COMPANY = 6; public static final int GAME_COLUMN_CAPTION = 6;
public static final int FOLDER_COLUMN_PATH = 1; public static final int FOLDER_COLUMN_PATH = 1;
public static final String KEY_DB_ID = "_id"; public static final String KEY_DB_ID = "_id";
public static final String KEY_GAME_PATH = "path"; public static final String KEY_GAME_PATH = "path";
@ -176,6 +176,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
return; return;
} }
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.ReloadKeys();
MinimalDocumentFile[] children = FileUtil.listFiles(context, parent); MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
for (MinimalDocumentFile file : children) { for (MinimalDocumentFile file : children) {
if (file.isDirectory()) { if (file.isDirectory()) {

View file

@ -161,6 +161,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
if (FileUtil.copyUriToInternalStorage(this, result.getData(), dstPath, "prod.keys")) { if (FileUtil.copyUriToInternalStorage(this, result.getData(), dstPath, "prod.keys")) {
if (NativeLibrary.ReloadKeys()) { if (NativeLibrary.ReloadKeys()) {
Toast.makeText(this, R.string.install_keys_success, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.install_keys_success, Toast.LENGTH_SHORT).show();
refreshFragment();
} else { } else {
Toast.makeText(this, R.string.install_keys_failure, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.install_keys_failure, Toast.LENGTH_SHORT).show();
launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS); launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS);
@ -184,6 +185,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
private void refreshFragment() { private void refreshFragment() {
if (mPlatformGamesFragment != null) { if (mPlatformGamesFragment != null) {
NativeLibrary.ResetRomMetadata();
mPlatformGamesFragment.refresh(); mPlatformGamesFragment.refresh();
} }
} }

View file

@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.TextView; import android.widget.TextView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -13,6 +14,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.yuzu.yuzu_emu.NativeLibrary;
import org.yuzu.yuzu_emu.YuzuApplication; import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.R; import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.adapters.GameAdapter; import org.yuzu.yuzu_emu.adapters.GameAdapter;
@ -43,19 +45,34 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
int columns = getResources().getInteger(R.integer.game_grid_columns);
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
mAdapter = new GameAdapter(); mAdapter = new GameAdapter();
mRecyclerView.setLayoutManager(layoutManager); // Organize our grid layout based on the current view.
mRecyclerView.setAdapter(mAdapter); if (isAdded()) {
mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); view.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (view.getMeasuredWidth() == 0) {
return;
}
int columns = view.getMeasuredWidth() /
requireContext().getResources().getDimensionPixelSize(R.dimen.card_width);
if (columns == 0) {
columns = 1;
}
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
}
});
}
// Add swipe down to refresh gesture // Add swipe down to refresh gesture
final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.swipe_refresh);
pullToRefresh.setOnRefreshListener(() -> { pullToRefresh.setOnRefreshListener(() -> {
GameDatabase databaseHelper = YuzuApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
refresh(); refresh();
pullToRefresh.setRefreshing(false); pullToRefresh.setRefreshing(false);
}); });
@ -63,6 +80,8 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
@Override @Override
public void refresh() { public void refresh() {
GameDatabase databaseHelper = YuzuApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mPresenter.refresh(); mPresenter.refresh();
updateTextView(); updateTextView();
} }

View file

@ -1,6 +1,7 @@
package org.yuzu.yuzu_emu.utils; package org.yuzu.yuzu_emu.utils;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.squareup.picasso.Request; import com.squareup.picasso.Request;
@ -13,15 +14,16 @@ import java.nio.IntBuffer;
public class GameIconRequestHandler extends RequestHandler { public class GameIconRequestHandler extends RequestHandler {
@Override @Override
public boolean canHandleRequest(Request data) { public boolean canHandleRequest(Request data) {
return "iso".equals(data.uri.getScheme()); return "content".equals(data.uri.getScheme());
} }
@Override @Override
public Result load(Request request, int networkPolicy) { public Result load(Request request, int networkPolicy) {
String url = request.uri.getHost() + request.uri.getPath(); String gamePath = request.uri.toString();
int[] vector = NativeLibrary.GetIcon(url); byte[] data = NativeLibrary.GetIcon(gamePath);
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); BitmapFactory.Options options = new BitmapFactory.Options();
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
return new Result(bitmap, Picasso.LoadedFrom.DISK); return new Result(bitmap, Picasso.LoadedFrom.DISK);
} }
} }

View file

@ -31,7 +31,7 @@ public class PicassoUtils {
public static void loadGameIcon(ImageView imageView, String gamePath) { public static void loadGameIcon(ImageView imageView, String gamePath) {
Picasso Picasso
.get() .get()
.load(Uri.parse("iso:/" + gamePath)) .load(Uri.parse(gamePath))
.fit() .fit()
.centerInside() .centerInside()
.config(Bitmap.Config.RGB_565) .config(Bitmap.Config.RGB_565)

View file

@ -16,8 +16,7 @@ public class GameViewHolder extends RecyclerView.ViewHolder {
private View itemView; private View itemView;
public ImageView imageIcon; public ImageView imageIcon;
public TextView textGameTitle; public TextView textGameTitle;
public TextView textCompany; public TextView textGameCaption;
public TextView textFileName;
public String gameId; public String gameId;
@ -36,8 +35,7 @@ public class GameViewHolder extends RecyclerView.ViewHolder {
imageIcon = itemView.findViewById(R.id.image_game_screen); imageIcon = itemView.findViewById(R.id.image_game_screen);
textGameTitle = itemView.findViewById(R.id.text_game_title); textGameTitle = itemView.findViewById(R.id.text_game_title);
textCompany = itemView.findViewById(R.id.text_company); textGameCaption = itemView.findViewById(R.id.text_game_caption);
textFileName = itemView.findViewById(R.id.text_filename);
} }
public View getItemView() { public View getItemView() {

View file

@ -24,6 +24,7 @@
#include "core/file_sys/vfs_real.h" #include "core/file_sys/vfs_real.h"
#include "core/hid/hid_core.h" #include "core/hid/hid_core.h"
#include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "core/perf_stats.h" #include "core/perf_stats.h"
#include "jni/config.h" #include "jni/config.h"
#include "jni/emu_window/emu_window.h" #include "jni/emu_window/emu_window.h"
@ -34,7 +35,11 @@ namespace {
class EmulationSession final { class EmulationSession final {
public: public:
EmulationSession() = default; EmulationSession() {
m_system.Initialize();
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
}
~EmulationSession() = default; ~EmulationSession() = default;
static EmulationSession& GetInstance() { static EmulationSession& GetInstance() {
@ -42,151 +47,205 @@ public:
} }
const Core::System& System() const { const Core::System& System() const {
return system; return m_system;
} }
Core::System& System() { Core::System& System() {
return system; return m_system;
} }
const EmuWindow_Android& Window() const { const EmuWindow_Android& Window() const {
return *window; return *m_window;
} }
EmuWindow_Android& Window() { EmuWindow_Android& Window() {
return *window; return *m_window;
} }
ANativeWindow* NativeWindow() const { ANativeWindow* NativeWindow() const {
return native_window; return m_native_window;
} }
void SetNativeWindow(ANativeWindow* native_window_) { void SetNativeWindow(ANativeWindow* m_native_window_) {
native_window = native_window_; m_native_window = m_native_window_;
} }
bool IsRunning() const { bool IsRunning() const {
std::scoped_lock lock(mutex); std::scoped_lock lock(m_mutex);
return is_running; return m_is_running;
} }
const Core::PerfStatsResults& PerfStats() const { const Core::PerfStatsResults& PerfStats() const {
std::scoped_lock perf_stats_lock(perf_stats_mutex); std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
return perf_stats; return m_perf_stats;
} }
void SurfaceChanged() { void SurfaceChanged() {
if (!IsRunning()) { if (!IsRunning()) {
return; return;
} }
window->OnSurfaceChanged(native_window); m_window->OnSurfaceChanged(m_native_window);
} }
Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
std::scoped_lock lock(mutex); std::scoped_lock lock(m_mutex);
// Loads the configuration. // Loads the configuration.
Config{}; Config{};
// Create the render window. // Create the render window.
window = std::make_unique<EmuWindow_Android>(&input_subsystem, native_window); m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window);
// Initialize system. // Initialize system.
system.SetShuttingDown(false); m_system.SetShuttingDown(false);
system.Initialize(); m_system.Initialize();
system.ApplySettings(); m_system.ApplySettings();
system.HIDCore().ReloadInputDevices(); m_system.HIDCore().ReloadInputDevices();
system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
system.GetFileSystemController().CreateFactories(*system.GetFilesystem()); m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
// Load the ROM. // Load the ROM.
load_result = system.Load(EmulationSession::GetInstance().Window(), filepath); m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
if (load_result != Core::SystemResultStatus::Success) { if (m_load_result != Core::SystemResultStatus::Success) {
return load_result; return m_load_result;
} }
// Complete initialization. // Complete initialization.
system.GPU().Start(); m_system.GPU().Start();
system.GetCpuManager().OnGpuReady(); m_system.GetCpuManager().OnGpuReady();
system.RegisterExitCallback([&] { HaltEmulation(); }); m_system.RegisterExitCallback([&] { HaltEmulation(); });
return Core::SystemResultStatus::Success; return Core::SystemResultStatus::Success;
} }
void ShutdownEmulation() { void ShutdownEmulation() {
std::scoped_lock lock(mutex); std::scoped_lock lock(m_mutex);
is_running = false; m_is_running = false;
// Unload user input. // Unload user input.
system.HIDCore().UnloadInputDevices(); m_system.HIDCore().UnloadInputDevices();
// Shutdown the main emulated process // Shutdown the main emulated process
if (load_result == Core::SystemResultStatus::Success) { if (m_load_result == Core::SystemResultStatus::Success) {
system.DetachDebugger(); m_system.DetachDebugger();
system.ShutdownMainProcess(); m_system.ShutdownMainProcess();
detached_tasks.WaitForAllTasks(); m_detached_tasks.WaitForAllTasks();
load_result = Core::SystemResultStatus::ErrorNotInitialized; m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
} }
// Tear down the render window. // Tear down the render window.
window.reset(); m_window.reset();
}
void PauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Pause();
}
void UnPauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Run();
} }
void HaltEmulation() { void HaltEmulation() {
std::scoped_lock lock(mutex); std::scoped_lock lock(m_mutex);
is_running = false; m_is_running = false;
cv.notify_one(); m_cv.notify_one();
} }
void RunEmulation() { void RunEmulation() {
{ {
std::scoped_lock lock(mutex); std::scoped_lock lock(m_mutex);
is_running = true; m_is_running = true;
} }
void(system.Run()); void(m_system.Run());
if (system.DebuggerEnabled()) { if (m_system.DebuggerEnabled()) {
system.InitializeDebugger(); m_system.InitializeDebugger();
} }
while (true) { while (true) {
{ {
std::unique_lock lock(mutex); std::unique_lock lock(m_mutex);
if (cv.wait_for(lock, std::chrono::milliseconds(100), if (m_cv.wait_for(lock, std::chrono::milliseconds(100),
[&]() { return !is_running; })) { [&]() { return !m_is_running; })) {
// Emulation halted. // Emulation halted.
break; break;
} }
} }
{ {
// Refresh performance stats. // Refresh performance stats.
std::scoped_lock perf_stats_lock(perf_stats_mutex); std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
perf_stats = system.GetAndResetPerfStats(); m_perf_stats = m_system.GetAndResetPerfStats();
} }
} }
} }
std::string GetRomTitle(const std::string& path) {
return GetRomMetadata(path).title;
}
std::vector<u8> GetRomIcon(const std::string& path) {
return GetRomMetadata(path).icon;
}
void ResetRomMetadata() {
m_rom_metadata_cache.clear();
}
private:
struct RomMetadata {
std::string title;
std::vector<u8> icon;
};
RomMetadata GetRomMetadata(const std::string& path) {
if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
return search->second;
}
return CacheRomMetadata(path);
}
RomMetadata CacheRomMetadata(const std::string& path) {
const auto file = Core::GetGameFileFromPath(m_vfs, path);
const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
RomMetadata entry;
loader->ReadTitle(entry.title);
loader->ReadIcon(entry.icon);
m_rom_metadata_cache[path] = entry;
return entry;
}
private: private:
static EmulationSession s_instance; static EmulationSession s_instance;
ANativeWindow* native_window{}; // Frontend management
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
InputCommon::InputSubsystem input_subsystem; // Window management
Common::DetachedTasks detached_tasks; std::unique_ptr<EmuWindow_Android> m_window;
Core::System system; ANativeWindow* m_native_window{};
Core::PerfStatsResults perf_stats{}; // Core emulation
Core::System m_system;
InputCommon::InputSubsystem m_input_subsystem;
Common::DetachedTasks m_detached_tasks;
Core::PerfStatsResults m_perf_stats{};
std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{};
std::unique_ptr<EmuWindow_Android> window; // Synchronization
std::condition_variable_any cv; std::condition_variable_any m_cv;
bool is_running{}; mutable std::mutex m_perf_stats_mutex;
Core::SystemResultStatus load_result{Core::SystemResultStatus::ErrorNotInitialized}; mutable std::mutex m_mutex;
mutable std::mutex perf_stats_mutex;
mutable std::mutex mutex;
}; };
/*static*/ EmulationSession EmulationSession::s_instance; /*static*/ EmulationSession EmulationSession::s_instance;
@ -275,16 +334,25 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env,
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {} [[maybe_unused]] jclass clazz) {
EmulationSession::GetInstance().UnPauseEmulation();
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {} [[maybe_unused]] jclass clazz) {
EmulationSession::GetInstance().PauseEmulation();
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) { [[maybe_unused]] jclass clazz) {
EmulationSession::GetInstance().HaltEmulation(); EmulationSession::GetInstance().HaltEmulation();
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {
EmulationSession::GetInstance().ResetRomMetadata();
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env, jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) { [[maybe_unused]] jclass clazz) {
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
@ -347,16 +415,21 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv*
} }
} }
jintArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env, jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz, [[maybe_unused]] jclass clazz,
[[maybe_unused]] jstring j_file) { [[maybe_unused]] jstring j_filename) {
return {}; auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
reinterpret_cast<jbyte*>(icon_data.data()));
return icon;
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env, jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz, [[maybe_unused]] jclass clazz,
[[maybe_unused]] jstring j_filename) { [[maybe_unused]] jstring j_filename) {
return env->NewStringUTF(""); auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
return env->NewStringUTF(title.c_str());
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env, jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env,

View file

@ -19,6 +19,9 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIE
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
jclass clazz); jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata(JNIEnv* env,
jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
jclass clazz); jclass clazz);
@ -39,8 +42,9 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JN
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
jfloat x, jfloat y); jfloat x, jfloat y);
JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz, JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
jstring j_file); jclass clazz,
jstring j_file);
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz, JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
jstring j_filename); jstring j_filename);

View file

@ -1,81 +1,76 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:clipToPadding="false"
android:focusable="true" android:focusable="true"
android:foreground="?android:attr/selectableItemBackground" android:paddingStart="4dp"
android:transitionName="card_game" android:paddingTop="8dp"
tools:layout_width="match_parent"> android:paddingEnd="4dp"
android:paddingBottom="8dp"
android:transitionName="card_game">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.cardview.widget.CardView
android:id="@+id/linearLayout" android:id="@+id/card_game_art"
android:layout_width="match_parent" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="150dp"
android:padding="8dp"> app:cardCornerRadius="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView <ImageView
android:id="@+id/image_game_screen" android:id="@+id/image_game_screen"
android:layout_width="56dp" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:adjustViewBounds="false" android:layout_weight="1" />
android:cropToPadding="false"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:scaleType="fitCenter" />
<TextView <TextView
android:id="@+id/text_game_title" android:id="@+id/text_game_title_inner"
style="@android:style/TextAppearance.Material.Subhead" style="@android:style/TextAppearance.Material.Subhead"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:baselineAligned="false"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center|top"
android:lines="1" android:maxLines="2"
android:maxLines="1" android:paddingLeft="2dp"
android:textAlignment="viewStart" android:paddingRight="2dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingTop="8dp"
app:layout_constraintStart_toEndOf="@+id/image_game_screen" android:visibility="visible"
app:layout_constraintTop_toTopOf="parent" tools:text="The Legend of Zelda: The Wind Waker" />
tools:text="The Legend of Zelda\nOcarina of Time 3D"
android:textColor="@color/header_text" />
<TextView </androidx.cardview.widget.CardView>
android:id="@+id/text_company"
style="@android:style/TextAppearance.Material.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/image_game_screen"
app:layout_constraintStart_toStartOf="@+id/text_game_title"
app:layout_constraintTop_toBottomOf="@+id/text_game_title"
app:layout_constraintVertical_bias="0.842"
tools:text="Nintendo"
android:textColor="@color/header_subtext" />
<TextView <TextView
android:id="@+id/text_filename" android:id="@+id/text_game_title"
style="@android:style/TextAppearance.Material.Caption" style="@android:style/TextAppearance.Material.Subhead"
android:layout_width="wrap_content" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:maxLines="2"
android:maxLines="1" android:paddingTop="8dp"
app:layout_constraintBottom_toBottomOf="@+id/image_game_screen" app:layout_constraintEnd_toEndOf="@+id/card_game_art"
app:layout_constraintStart_toStartOf="@+id/text_game_title" app:layout_constraintStart_toStartOf="@+id/card_game_art"
app:layout_constraintTop_toBottomOf="@+id/text_game_title" app:layout_constraintTop_toBottomOf="@+id/card_game_art"
app:layout_constraintVertical_bias="0.0" tools:text="The Legend of Zelda: The Wind Waker" />
tools:text="Pilotwings_Resort.cxi"
android:textColor="@color/header_subtext" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/text_game_caption"
style="@android:style/TextAppearance.Material.Caption"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="@+id/card_game_art"
app:layout_constraintStart_toStartOf="@+id/card_game_art"
app:layout_constraintTop_toBottomOf="@+id/text_game_title"
tools:text="Nintendo" />
</androidx.cardview.widget.CardView> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_grid_games" android:id="@+id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<RelativeLayout <RelativeLayout
@ -22,11 +22,12 @@
android:textSize="18sp" android:textSize="18sp"
android:gravity="center" /> android:gravity="center" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_games" android:id="@+id/grid_games"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/card_game" /> android:clipToPadding="false"
tools:listitem="@layout/card_game" />
</RelativeLayout> </RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View file

@ -6,7 +6,7 @@
<dimen name="spacing_list">64dp</dimen> <dimen name="spacing_list">64dp</dimen>
<dimen name="spacing_fab">72dp</dimen> <dimen name="spacing_fab">72dp</dimen>
<dimen name="menu_width">256dp</dimen> <dimen name="menu_width">256dp</dimen>
<dimen name="card_width">135dp</dimen> <dimen name="card_width">150dp</dimen>
<dimen name="dialog_margin">20dp</dimen> <dimen name="dialog_margin">20dp</dimen>
<dimen name="elevated_app_bar">3dp</dimen> <dimen name="elevated_app_bar">3dp</dimen>