summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar bunnei2023-02-12 00:17:19 -0800
committerGravatar bunnei2023-06-03 00:05:30 -0700
commit0e52d11ede15d0a47761dce42ebf1a8f7eb83719 (patch)
tree5ce614dc7aaa5c7251733f10dcda141baf33ae4b /src/android
parentandroid: Replace notification icon with yuzu (diff)
downloadyuzu-0e52d11ede15d0a47761dce42ebf1a8f7eb83719.tar.gz
yuzu-0e52d11ede15d0a47761dce42ebf1a8f7eb83719.tar.xz
yuzu-0e52d11ede15d0a47761dce42ebf1a8f7eb83719.zip
android: frontend: Implement game grid view. (#9)
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/build.gradle2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java35
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java6
-rw-r--r--src/android/app/src/main/jni/native.cpp211
-rw-r--r--src/android/app/src/main/jni/native.h8
-rw-r--r--src/android/app/src/main/res/layout/card_game.xml119
-rw-r--r--src/android/app/src/main/res/layout/fragment_grid.xml23
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
15 files changed, 272 insertions, 174 deletions
diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
index c516b2bff..74835113c 100644
--- a/src/android/app/build.gradle
+++ b/src/android/app/build.gradle
@@ -11,7 +11,7 @@ def abiFilter = "arm64-v8a" //, "x86"
11 11
12android { 12android {
13 compileSdkVersion 32 13 compileSdkVersion 32
14 ndkVersion "25.1.8937393" 14 ndkVersion "25.2.9519653"
15 15
16 compileOptions { 16 compileOptions {
17 sourceCompatibility JavaVersion.VERSION_1_8 17 sourceCompatibility JavaVersion.VERSION_1_8
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
index 75395bd4c..7a1ddd38e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
@@ -141,9 +141,9 @@ public final class NativeLibrary {
141 * Gets the embedded icon within the given ROM. 141 * Gets the embedded icon within the given ROM.
142 * 142 *
143 * @param filename the file path to the ROM. 143 * @param filename the file path to the ROM.
144 * @return an integer array containing the color data for the icon. 144 * @return a byte array containing the JPEG data for the icon.
145 */ 145 */
146 public static native int[] GetIcon(String filename); 146 public static native byte[] GetIcon(String filename);
147 147
148 /** 148 /**
149 * Gets the embedded title of the given ISO/ROM. 149 * Gets the embedded title of the given ISO/ROM.
@@ -205,6 +205,11 @@ public final class NativeLibrary {
205 public static native void StopEmulation(); 205 public static native void StopEmulation();
206 206
207 /** 207 /**
208 * Resets the in-memory ROM metadata cache.
209 */
210 public static native void ResetRomMetadata();
211
212 /**
208 * Returns true if emulation is running (or is paused). 213 * Returns true if emulation is running (or is paused).
209 */ 214 */
210 public static native boolean IsRunning(); 215 public static native boolean IsRunning();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
index cd9f823d4..ed1a000c7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
@@ -86,11 +86,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
86 mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); 86 mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
87 87
88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); 88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
89 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); 89 holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
90
91 String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
92 String filename = FileUtil.getFilename(YuzuApplication.getAppContext(), filepath);
93 holder.textFileName.setText(filename);
94 90
95 // TODO These shouldn't be necessary once the move to a DB-based model is complete. 91 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
96 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); 92 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
@@ -98,7 +94,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
98 holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); 94 holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
99 holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); 95 holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
100 holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); 96 holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
101 holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); 97 holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION);
102 98
103 final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled; 99 final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled;
104 View itemView = holder.getItemView(); 100 View itemView = holder.getItemView();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java
index bc1b19bd1..681117268 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.java
@@ -47,7 +47,7 @@ public final class Game {
47 cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), 47 cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
48 cursor.getString(GameDatabase.GAME_COLUMN_PATH), 48 cursor.getString(GameDatabase.GAME_COLUMN_PATH),
49 cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), 49 cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
50 cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); 50 cursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
51 } 51 }
52 52
53 public String getTitle() { 53 public String getTitle() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
index 771e35c69..a10ac6ff2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
@@ -29,7 +29,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
29 public static final int GAME_COLUMN_DESCRIPTION = 3; 29 public static final int GAME_COLUMN_DESCRIPTION = 3;
30 public static final int GAME_COLUMN_REGIONS = 4; 30 public static final int GAME_COLUMN_REGIONS = 4;
31 public static final int GAME_COLUMN_GAME_ID = 5; 31 public static final int GAME_COLUMN_GAME_ID = 5;
32 public static final int GAME_COLUMN_COMPANY = 6; 32 public static final int GAME_COLUMN_CAPTION = 6;
33 public static final int FOLDER_COLUMN_PATH = 1; 33 public static final int FOLDER_COLUMN_PATH = 1;
34 public static final String KEY_DB_ID = "_id"; 34 public static final String KEY_DB_ID = "_id";
35 public static final String KEY_GAME_PATH = "path"; 35 public static final String KEY_GAME_PATH = "path";
@@ -176,6 +176,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
176 return; 176 return;
177 } 177 }
178 178
179 // Ensure keys are loaded so that ROM metadata can be decrypted.
180 NativeLibrary.ReloadKeys();
181
179 MinimalDocumentFile[] children = FileUtil.listFiles(context, parent); 182 MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
180 for (MinimalDocumentFile file : children) { 183 for (MinimalDocumentFile file : children) {
181 if (file.isDirectory()) { 184 if (file.isDirectory()) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
index 7fdd692c2..552232bd3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
@@ -161,6 +161,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
161 if (FileUtil.copyUriToInternalStorage(this, result.getData(), dstPath, "prod.keys")) { 161 if (FileUtil.copyUriToInternalStorage(this, result.getData(), dstPath, "prod.keys")) {
162 if (NativeLibrary.ReloadKeys()) { 162 if (NativeLibrary.ReloadKeys()) {
163 Toast.makeText(this, R.string.install_keys_success, Toast.LENGTH_SHORT).show(); 163 Toast.makeText(this, R.string.install_keys_success, Toast.LENGTH_SHORT).show();
164 refreshFragment();
164 } else { 165 } else {
165 Toast.makeText(this, R.string.install_keys_failure, Toast.LENGTH_SHORT).show(); 166 Toast.makeText(this, R.string.install_keys_failure, Toast.LENGTH_SHORT).show();
166 launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS); 167 launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS);
@@ -184,6 +185,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
184 185
185 private void refreshFragment() { 186 private void refreshFragment() {
186 if (mPlatformGamesFragment != null) { 187 if (mPlatformGamesFragment != null) {
188 NativeLibrary.ResetRomMetadata();
187 mPlatformGamesFragment.refresh(); 189 mPlatformGamesFragment.refresh();
188 } 190 }
189 } 191 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java
index 6c327b1b8..2d74f43ca 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.java
@@ -5,6 +5,7 @@ import android.os.Bundle;
5import android.view.LayoutInflater; 5import android.view.LayoutInflater;
6import android.view.View; 6import android.view.View;
7import android.view.ViewGroup; 7import android.view.ViewGroup;
8import android.view.ViewTreeObserver;
8import android.widget.TextView; 9import android.widget.TextView;
9 10
10import androidx.core.content.ContextCompat; 11import androidx.core.content.ContextCompat;
@@ -13,6 +14,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
13import androidx.recyclerview.widget.RecyclerView; 14import androidx.recyclerview.widget.RecyclerView;
14import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 15import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
15 16
17import org.yuzu.yuzu_emu.NativeLibrary;
16import org.yuzu.yuzu_emu.YuzuApplication; 18import org.yuzu.yuzu_emu.YuzuApplication;
17import org.yuzu.yuzu_emu.R; 19import org.yuzu.yuzu_emu.R;
18import org.yuzu.yuzu_emu.adapters.GameAdapter; 20import org.yuzu.yuzu_emu.adapters.GameAdapter;
@@ -43,19 +45,34 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
43 45
44 @Override 46 @Override
45 public void onViewCreated(View view, Bundle savedInstanceState) { 47 public void onViewCreated(View view, Bundle savedInstanceState) {
46 int columns = getResources().getInteger(R.integer.game_grid_columns);
47 RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
48 mAdapter = new GameAdapter(); 48 mAdapter = new GameAdapter();
49 49
50 mRecyclerView.setLayoutManager(layoutManager); 50 // Organize our grid layout based on the current view.
51 mRecyclerView.setAdapter(mAdapter); 51 if (isAdded()) {
52 mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); 52 view.getViewTreeObserver()
53 .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
54 @Override
55 public void onGlobalLayout() {
56 if (view.getMeasuredWidth() == 0) {
57 return;
58 }
59
60 int columns = view.getMeasuredWidth() /
61 requireContext().getResources().getDimensionPixelSize(R.dimen.card_width);
62 if (columns == 0) {
63 columns = 1;
64 }
65 view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
66 GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
67 mRecyclerView.setLayoutManager(layoutManager);
68 mRecyclerView.setAdapter(mAdapter);
69 }
70 });
71 }
53 72
54 // Add swipe down to refresh gesture 73 // Add swipe down to refresh gesture
55 final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); 74 final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.swipe_refresh);
56 pullToRefresh.setOnRefreshListener(() -> { 75 pullToRefresh.setOnRefreshListener(() -> {
57 GameDatabase databaseHelper = YuzuApplication.databaseHelper;
58 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
59 refresh(); 76 refresh();
60 pullToRefresh.setRefreshing(false); 77 pullToRefresh.setRefreshing(false);
61 }); 78 });
@@ -63,6 +80,8 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
63 80
64 @Override 81 @Override
65 public void refresh() { 82 public void refresh() {
83 GameDatabase databaseHelper = YuzuApplication.databaseHelper;
84 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
66 mPresenter.refresh(); 85 mPresenter.refresh();
67 updateTextView(); 86 updateTextView();
68 } 87 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java
index b75dc9a62..fd43575de 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconRequestHandler.java
@@ -1,6 +1,7 @@
1package org.yuzu.yuzu_emu.utils; 1package org.yuzu.yuzu_emu.utils;
2 2
3import android.graphics.Bitmap; 3import android.graphics.Bitmap;
4import android.graphics.BitmapFactory;
4 5
5import com.squareup.picasso.Picasso; 6import com.squareup.picasso.Picasso;
6import com.squareup.picasso.Request; 7import com.squareup.picasso.Request;
@@ -13,15 +14,16 @@ import java.nio.IntBuffer;
13public class GameIconRequestHandler extends RequestHandler { 14public class GameIconRequestHandler extends RequestHandler {
14 @Override 15 @Override
15 public boolean canHandleRequest(Request data) { 16 public boolean canHandleRequest(Request data) {
16 return "iso".equals(data.uri.getScheme()); 17 return "content".equals(data.uri.getScheme());
17 } 18 }
18 19
19 @Override 20 @Override
20 public Result load(Request request, int networkPolicy) { 21 public Result load(Request request, int networkPolicy) {
21 String url = request.uri.getHost() + request.uri.getPath(); 22 String gamePath = request.uri.toString();
22 int[] vector = NativeLibrary.GetIcon(url); 23 byte[] data = NativeLibrary.GetIcon(gamePath);
23 Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); 24 BitmapFactory.Options options = new BitmapFactory.Options();
24 bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); 25 options.inMutable = true;
26 Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
25 return new Result(bitmap, Picasso.LoadedFrom.DISK); 27 return new Result(bitmap, Picasso.LoadedFrom.DISK);
26 } 28 }
27} 29}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java
index 5033691b3..504dc5b6d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PicassoUtils.java
@@ -31,7 +31,7 @@ public class PicassoUtils {
31 public static void loadGameIcon(ImageView imageView, String gamePath) { 31 public static void loadGameIcon(ImageView imageView, String gamePath) {
32 Picasso 32 Picasso
33 .get() 33 .get()
34 .load(Uri.parse("iso:/" + gamePath)) 34 .load(Uri.parse(gamePath))
35 .fit() 35 .fit()
36 .centerInside() 36 .centerInside()
37 .config(Bitmap.Config.RGB_565) 37 .config(Bitmap.Config.RGB_565)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java
index 2dc0f34f3..41b8c6a27 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.java
@@ -16,8 +16,7 @@ public class GameViewHolder extends RecyclerView.ViewHolder {
16 private View itemView; 16 private View itemView;
17 public ImageView imageIcon; 17 public ImageView imageIcon;
18 public TextView textGameTitle; 18 public TextView textGameTitle;
19 public TextView textCompany; 19 public TextView textGameCaption;
20 public TextView textFileName;
21 20
22 public String gameId; 21 public String gameId;
23 22
@@ -36,8 +35,7 @@ public class GameViewHolder extends RecyclerView.ViewHolder {
36 35
37 imageIcon = itemView.findViewById(R.id.image_game_screen); 36 imageIcon = itemView.findViewById(R.id.image_game_screen);
38 textGameTitle = itemView.findViewById(R.id.text_game_title); 37 textGameTitle = itemView.findViewById(R.id.text_game_title);
39 textCompany = itemView.findViewById(R.id.text_company); 38 textGameCaption = itemView.findViewById(R.id.text_game_caption);
40 textFileName = itemView.findViewById(R.id.text_filename);
41 } 39 }
42 40
43 public View getItemView() { 41 public View getItemView() {
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 6d1e75c40..2bd908308 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -24,6 +24,7 @@
24#include "core/file_sys/vfs_real.h" 24#include "core/file_sys/vfs_real.h"
25#include "core/hid/hid_core.h" 25#include "core/hid/hid_core.h"
26#include "core/hle/service/filesystem/filesystem.h" 26#include "core/hle/service/filesystem/filesystem.h"
27#include "core/loader/loader.h"
27#include "core/perf_stats.h" 28#include "core/perf_stats.h"
28#include "jni/config.h" 29#include "jni/config.h"
29#include "jni/emu_window/emu_window.h" 30#include "jni/emu_window/emu_window.h"
@@ -34,7 +35,11 @@ namespace {
34 35
35class EmulationSession final { 36class EmulationSession final {
36public: 37public:
37 EmulationSession() = default; 38 EmulationSession() {
39 m_system.Initialize();
40 m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
41 }
42
38 ~EmulationSession() = default; 43 ~EmulationSession() = default;
39 44
40 static EmulationSession& GetInstance() { 45 static EmulationSession& GetInstance() {
@@ -42,151 +47,205 @@ public:
42 } 47 }
43 48
44 const Core::System& System() const { 49 const Core::System& System() const {
45 return system; 50 return m_system;
46 } 51 }
47 52
48 Core::System& System() { 53 Core::System& System() {
49 return system; 54 return m_system;
50 } 55 }
51 56
52 const EmuWindow_Android& Window() const { 57 const EmuWindow_Android& Window() const {
53 return *window; 58 return *m_window;
54 } 59 }
55 60
56 EmuWindow_Android& Window() { 61 EmuWindow_Android& Window() {
57 return *window; 62 return *m_window;
58 } 63 }
59 64
60 ANativeWindow* NativeWindow() const { 65 ANativeWindow* NativeWindow() const {
61 return native_window; 66 return m_native_window;
62 } 67 }
63 68
64 void SetNativeWindow(ANativeWindow* native_window_) { 69 void SetNativeWindow(ANativeWindow* m_native_window_) {
65 native_window = native_window_; 70 m_native_window = m_native_window_;
66 } 71 }
67 72
68 bool IsRunning() const { 73 bool IsRunning() const {
69 std::scoped_lock lock(mutex); 74 std::scoped_lock lock(m_mutex);
70 return is_running; 75 return m_is_running;
71 } 76 }
72 77
73 const Core::PerfStatsResults& PerfStats() const { 78 const Core::PerfStatsResults& PerfStats() const {
74 std::scoped_lock perf_stats_lock(perf_stats_mutex); 79 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
75 return perf_stats; 80 return m_perf_stats;
76 } 81 }
77 82
78 void SurfaceChanged() { 83 void SurfaceChanged() {
79 if (!IsRunning()) { 84 if (!IsRunning()) {
80 return; 85 return;
81 } 86 }
82 window->OnSurfaceChanged(native_window); 87 m_window->OnSurfaceChanged(m_native_window);
83 } 88 }
84 89
85 Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { 90 Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
86 std::scoped_lock lock(mutex); 91 std::scoped_lock lock(m_mutex);
87 92
88 // Loads the configuration. 93 // Loads the configuration.
89 Config{}; 94 Config{};
90 95
91 // Create the render window. 96 // Create the render window.
92 window = std::make_unique<EmuWindow_Android>(&input_subsystem, native_window); 97 m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window);
93 98
94 // Initialize system. 99 // Initialize system.
95 system.SetShuttingDown(false); 100 m_system.SetShuttingDown(false);
96 system.Initialize(); 101 m_system.Initialize();
97 system.ApplySettings(); 102 m_system.ApplySettings();
98 system.HIDCore().ReloadInputDevices(); 103 m_system.HIDCore().ReloadInputDevices();
99 system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); 104 m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
100 system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); 105 m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
101 system.GetFileSystemController().CreateFactories(*system.GetFilesystem()); 106 m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
102 107
103 // Load the ROM. 108 // Load the ROM.
104 load_result = system.Load(EmulationSession::GetInstance().Window(), filepath); 109 m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
105 if (load_result != Core::SystemResultStatus::Success) { 110 if (m_load_result != Core::SystemResultStatus::Success) {
106 return load_result; 111 return m_load_result;
107 } 112 }
108 113
109 // Complete initialization. 114 // Complete initialization.
110 system.GPU().Start(); 115 m_system.GPU().Start();
111 system.GetCpuManager().OnGpuReady(); 116 m_system.GetCpuManager().OnGpuReady();
112 system.RegisterExitCallback([&] { HaltEmulation(); }); 117 m_system.RegisterExitCallback([&] { HaltEmulation(); });
113 118
114 return Core::SystemResultStatus::Success; 119 return Core::SystemResultStatus::Success;
115 } 120 }
116 121
117 void ShutdownEmulation() { 122 void ShutdownEmulation() {
118 std::scoped_lock lock(mutex); 123 std::scoped_lock lock(m_mutex);
119 124
120 is_running = false; 125 m_is_running = false;
121 126
122 // Unload user input. 127 // Unload user input.
123 system.HIDCore().UnloadInputDevices(); 128 m_system.HIDCore().UnloadInputDevices();
124 129
125 // Shutdown the main emulated process 130 // Shutdown the main emulated process
126 if (load_result == Core::SystemResultStatus::Success) { 131 if (m_load_result == Core::SystemResultStatus::Success) {
127 system.DetachDebugger(); 132 m_system.DetachDebugger();
128 system.ShutdownMainProcess(); 133 m_system.ShutdownMainProcess();
129 detached_tasks.WaitForAllTasks(); 134 m_detached_tasks.WaitForAllTasks();
130 load_result = Core::SystemResultStatus::ErrorNotInitialized; 135 m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
131 } 136 }
132 137
133 // Tear down the render window. 138 // Tear down the render window.
134 window.reset(); 139 m_window.reset();
140 }
141
142 void PauseEmulation() {
143 std::scoped_lock lock(m_mutex);
144 m_system.Pause();
145 }
146
147 void UnPauseEmulation() {
148 std::scoped_lock lock(m_mutex);
149 m_system.Run();
135 } 150 }
136 151
137 void HaltEmulation() { 152 void HaltEmulation() {
138 std::scoped_lock lock(mutex); 153 std::scoped_lock lock(m_mutex);
139 is_running = false; 154 m_is_running = false;
140 cv.notify_one(); 155 m_cv.notify_one();
141 } 156 }
142 157
143 void RunEmulation() { 158 void RunEmulation() {
144 { 159 {
145 std::scoped_lock lock(mutex); 160 std::scoped_lock lock(m_mutex);
146 is_running = true; 161 m_is_running = true;
147 } 162 }
148 163
149 void(system.Run()); 164 void(m_system.Run());
150 165
151 if (system.DebuggerEnabled()) { 166 if (m_system.DebuggerEnabled()) {
152 system.InitializeDebugger(); 167 m_system.InitializeDebugger();
153 } 168 }
154 169
155 while (true) { 170 while (true) {
156 { 171 {
157 std::unique_lock lock(mutex); 172 std::unique_lock lock(m_mutex);
158 if (cv.wait_for(lock, std::chrono::milliseconds(100), 173 if (m_cv.wait_for(lock, std::chrono::milliseconds(100),
159 [&]() { return !is_running; })) { 174 [&]() { return !m_is_running; })) {
160 // Emulation halted. 175 // Emulation halted.
161 break; 176 break;
162 } 177 }
163 } 178 }
164 { 179 {
165 // Refresh performance stats. 180 // Refresh performance stats.
166 std::scoped_lock perf_stats_lock(perf_stats_mutex); 181 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
167 perf_stats = system.GetAndResetPerfStats(); 182 m_perf_stats = m_system.GetAndResetPerfStats();
168 } 183 }
169 } 184 }
170 } 185 }
171 186
187 std::string GetRomTitle(const std::string& path) {
188 return GetRomMetadata(path).title;
189 }
190
191 std::vector<u8> GetRomIcon(const std::string& path) {
192 return GetRomMetadata(path).icon;
193 }
194
195 void ResetRomMetadata() {
196 m_rom_metadata_cache.clear();
197 }
198
172private: 199private:
173 static EmulationSession s_instance; 200 struct RomMetadata {
201 std::string title;
202 std::vector<u8> icon;
203 };
204
205 RomMetadata GetRomMetadata(const std::string& path) {
206 if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
207 return search->second;
208 }
209
210 return CacheRomMetadata(path);
211 }
174 212
175 ANativeWindow* native_window{}; 213 RomMetadata CacheRomMetadata(const std::string& path) {
214 const auto file = Core::GetGameFileFromPath(m_vfs, path);
215 const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
176 216
177 InputCommon::InputSubsystem input_subsystem; 217 RomMetadata entry;
178 Common::DetachedTasks detached_tasks; 218 loader->ReadTitle(entry.title);
179 Core::System system; 219 loader->ReadIcon(entry.icon);
180 220
181 Core::PerfStatsResults perf_stats{}; 221 m_rom_metadata_cache[path] = entry;
182 222
183 std::unique_ptr<EmuWindow_Android> window; 223 return entry;
184 std::condition_variable_any cv; 224 }
185 bool is_running{}; 225
186 Core::SystemResultStatus load_result{Core::SystemResultStatus::ErrorNotInitialized}; 226private:
227 static EmulationSession s_instance;
187 228
188 mutable std::mutex perf_stats_mutex; 229 // Frontend management
189 mutable std::mutex mutex; 230 std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
231
232 // Window management
233 std::unique_ptr<EmuWindow_Android> m_window;
234 ANativeWindow* m_native_window{};
235
236 // Core emulation
237 Core::System m_system;
238 InputCommon::InputSubsystem m_input_subsystem;
239 Common::DetachedTasks m_detached_tasks;
240 Core::PerfStatsResults m_perf_stats{};
241 std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
242 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
243 bool m_is_running{};
244
245 // Synchronization
246 std::condition_variable_any m_cv;
247 mutable std::mutex m_perf_stats_mutex;
248 mutable std::mutex m_mutex;
190}; 249};
191 250
192/*static*/ EmulationSession EmulationSession::s_instance; 251/*static*/ EmulationSession EmulationSession::s_instance;
@@ -275,16 +334,25 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env,
275} 334}
276 335
277void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, 336void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
278 [[maybe_unused]] jclass clazz) {} 337 [[maybe_unused]] jclass clazz) {
338 EmulationSession::GetInstance().UnPauseEmulation();
339}
279 340
280void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env, 341void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env,
281 [[maybe_unused]] jclass clazz) {} 342 [[maybe_unused]] jclass clazz) {
343 EmulationSession::GetInstance().PauseEmulation();
344}
282 345
283void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env, 346void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env,
284 [[maybe_unused]] jclass clazz) { 347 [[maybe_unused]] jclass clazz) {
285 EmulationSession::GetInstance().HaltEmulation(); 348 EmulationSession::GetInstance().HaltEmulation();
286} 349}
287 350
351void Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata([[maybe_unused]] JNIEnv* env,
352 [[maybe_unused]] jclass clazz) {
353 EmulationSession::GetInstance().ResetRomMetadata();
354}
355
288jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env, 356jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env,
289 [[maybe_unused]] jclass clazz) { 357 [[maybe_unused]] jclass clazz) {
290 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); 358 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
@@ -347,16 +415,21 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv*
347 } 415 }
348} 416}
349 417
350jintArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env, 418jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env,
351 [[maybe_unused]] jclass clazz, 419 [[maybe_unused]] jclass clazz,
352 [[maybe_unused]] jstring j_file) { 420 [[maybe_unused]] jstring j_filename) {
353 return {}; 421 auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
422 jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
423 env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
424 reinterpret_cast<jbyte*>(icon_data.data()));
425 return icon;
354} 426}
355 427
356jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env, 428jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env,
357 [[maybe_unused]] jclass clazz, 429 [[maybe_unused]] jclass clazz,
358 [[maybe_unused]] jstring j_filename) { 430 [[maybe_unused]] jstring j_filename) {
359 return env->NewStringUTF(""); 431 auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
432 return env->NewStringUTF(title.c_str());
360} 433}
361 434
362jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env, 435jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env,
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 210976201..bbc783aa8 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -19,6 +19,9 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIE
19JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, 19JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
20 jclass clazz); 20 jclass clazz);
21 21
22JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata(JNIEnv* env,
23 jclass clazz);
24
22JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, 25JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
23 jclass clazz); 26 jclass clazz);
24 27
@@ -39,8 +42,9 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JN
39JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, 42JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
40 jfloat x, jfloat y); 43 jfloat x, jfloat y);
41 44
42JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz, 45JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
43 jstring j_file); 46 jclass clazz,
47 jstring j_file);
44 48
45JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz, 49JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
46 jstring j_filename); 50 jstring j_filename);
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
index 217f02d34..a0d453719 100644
--- a/src/android/app/src/main/res/layout/card_game.xml
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -1,81 +1,76 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" 2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools" 5 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent" 6 android:layout_width="match_parent"
6 android:layout_height="wrap_content" 7 android:layout_height="wrap_content"
8 android:background="?attr/selectableItemBackground"
7 android:clickable="true" 9 android:clickable="true"
10 android:clipToPadding="false"
8 android:focusable="true" 11 android:focusable="true"
9 android:foreground="?android:attr/selectableItemBackground" 12 android:paddingStart="4dp"
10 android:transitionName="card_game" 13 android:paddingTop="8dp"
11 tools:layout_width="match_parent"> 14 android:paddingEnd="4dp"
15 android:paddingBottom="8dp"
16 android:transitionName="card_game">
12 17
13 <androidx.constraintlayout.widget.ConstraintLayout 18 <androidx.cardview.widget.CardView
14 android:id="@+id/linearLayout" 19 android:id="@+id/card_game_art"
15 android:layout_width="match_parent" 20 android:layout_width="150dp"
16 android:layout_height="wrap_content" 21 android:layout_height="150dp"
17 android:padding="8dp"> 22 app:cardCornerRadius="4dp"
23 app:layout_constraintEnd_toEndOf="parent"
24 app:layout_constraintStart_toStartOf="parent"
25 app:layout_constraintTop_toTopOf="parent">
18 26
19 <ImageView 27 <ImageView
20 android:id="@+id/image_game_screen" 28 android:id="@+id/image_game_screen"
21 android:layout_width="56dp" 29 android:layout_width="match_parent"
22 android:layout_height="56dp" 30 android:layout_height="match_parent"
23 android:adjustViewBounds="false" 31 android:layout_weight="1" />
24 android:cropToPadding="false"
25 android:scaleType="fitCenter"
26 app:layout_constraintBottom_toBottomOf="parent"
27 app:layout_constraintStart_toStartOf="parent"
28 app:layout_constraintTop_toTopOf="parent"
29 tools:scaleType="fitCenter" />
30 32
31 <TextView 33 <TextView
32 android:id="@+id/text_game_title" 34 android:id="@+id/text_game_title_inner"
33 style="@android:style/TextAppearance.Material.Subhead" 35 style="@android:style/TextAppearance.Material.Subhead"
34 android:layout_width="0dp" 36 android:layout_width="match_parent"
35 android:layout_height="wrap_content" 37 android:layout_height="match_parent"
36 android:layout_marginStart="8dp"
37 android:baselineAligned="false"
38 android:ellipsize="end" 38 android:ellipsize="end"
39 android:gravity="center_vertical" 39 android:gravity="center|top"
40 android:lines="1" 40 android:maxLines="2"
41 android:maxLines="1" 41 android:paddingLeft="2dp"
42 android:textAlignment="viewStart" 42 android:paddingRight="2dp"
43 app:layout_constraintEnd_toEndOf="parent" 43 android:paddingTop="8dp"
44 app:layout_constraintStart_toEndOf="@+id/image_game_screen" 44 android:visibility="visible"
45 app:layout_constraintTop_toTopOf="parent" 45 tools:text="The Legend of Zelda: The Wind Waker" />
46 tools:text="The Legend of Zelda\nOcarina of Time 3D"
47 android:textColor="@color/header_text" />
48 46
49 <TextView 47 </androidx.cardview.widget.CardView>
50 android:id="@+id/text_company"
51 style="@android:style/TextAppearance.Material.Caption"
52 android:layout_width="wrap_content"
53 android:layout_height="wrap_content"
54 android:ellipsize="end"
55 android:lines="1"
56 android:maxLines="1"
57 app:layout_constraintBottom_toBottomOf="@+id/image_game_screen"
58 app:layout_constraintStart_toStartOf="@+id/text_game_title"
59 app:layout_constraintTop_toBottomOf="@+id/text_game_title"
60 app:layout_constraintVertical_bias="0.842"
61 tools:text="Nintendo"
62 android:textColor="@color/header_subtext" />
63 48
64 <TextView 49 <TextView
65 android:id="@+id/text_filename" 50 android:id="@+id/text_game_title"
66 style="@android:style/TextAppearance.Material.Caption" 51 style="@android:style/TextAppearance.Material.Subhead"
67 android:layout_width="wrap_content" 52 android:layout_width="150dp"
68 android:layout_height="wrap_content" 53 android:layout_height="wrap_content"
69 android:ellipsize="end" 54 android:ellipsize="end"
70 android:lines="1" 55 android:maxLines="2"
71 android:maxLines="1" 56 android:paddingTop="8dp"
72 app:layout_constraintBottom_toBottomOf="@+id/image_game_screen" 57 app:layout_constraintEnd_toEndOf="@+id/card_game_art"
73 app:layout_constraintStart_toStartOf="@+id/text_game_title" 58 app:layout_constraintStart_toStartOf="@+id/card_game_art"
74 app:layout_constraintTop_toBottomOf="@+id/text_game_title" 59 app:layout_constraintTop_toBottomOf="@+id/card_game_art"
75 app:layout_constraintVertical_bias="0.0" 60 tools:text="The Legend of Zelda: The Wind Waker" />
76 tools:text="Pilotwings_Resort.cxi"
77 android:textColor="@color/header_subtext" />
78 61
79 </androidx.constraintlayout.widget.ConstraintLayout> 62 <TextView
63 android:id="@+id/text_game_caption"
64 style="@android:style/TextAppearance.Material.Caption"
65 android:layout_width="150dp"
66 android:layout_height="wrap_content"
67 android:ellipsize="end"
68 android:lines="1"
69 android:maxLines="1"
70 android:paddingTop="8dp"
71 app:layout_constraintEnd_toEndOf="@+id/card_game_art"
72 app:layout_constraintStart_toStartOf="@+id/card_game_art"
73 app:layout_constraintTop_toBottomOf="@+id/text_game_title"
74 tools:text="Nintendo" />
80 75
81</androidx.cardview.widget.CardView> 76</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml
index f5b6c2e19..01399e18d 100644
--- a/src/android/app/src/main/res/layout/fragment_grid.xml
+++ b/src/android/app/src/main/res/layout/fragment_grid.xml
@@ -1,12 +1,12 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools" 3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent" 4 android:layout_width="match_parent"
5 android:layout_height="match_parent"> 5 android:layout_height="match_parent">
6 6
7 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 7 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
8 android:id="@+id/refresh_grid_games" 8 android:id="@+id/swipe_refresh"
9 android:layout_width="match_parent" 9 android:layout_width="wrap_content"
10 android:layout_height="wrap_content"> 10 android:layout_height="wrap_content">
11 11
12 <RelativeLayout 12 <RelativeLayout
@@ -22,12 +22,13 @@
22 android:textSize="18sp" 22 android:textSize="18sp"
23 android:gravity="center" /> 23 android:gravity="center" />
24 24
25 <androidx.recyclerview.widget.RecyclerView 25 <androidx.recyclerview.widget.RecyclerView
26 android:id="@+id/grid_games" 26 android:id="@+id/grid_games"
27 android:layout_width="match_parent" 27 android:layout_width="match_parent"
28 android:layout_height="match_parent" 28 android:layout_height="match_parent"
29 tools:listitem="@layout/card_game" /> 29 android:clipToPadding="false"
30 tools:listitem="@layout/card_game" />
30 </RelativeLayout> 31 </RelativeLayout>
31 32
32 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> 33 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
33</FrameLayout> \ No newline at end of file 34</FrameLayout>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index a3bb0c2c5..0b028a167 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -6,7 +6,7 @@
6 <dimen name="spacing_list">64dp</dimen> 6 <dimen name="spacing_list">64dp</dimen>
7 <dimen name="spacing_fab">72dp</dimen> 7 <dimen name="spacing_fab">72dp</dimen>
8 <dimen name="menu_width">256dp</dimen> 8 <dimen name="menu_width">256dp</dimen>
9 <dimen name="card_width">135dp</dimen> 9 <dimen name="card_width">150dp</dimen>
10 10
11 <dimen name="dialog_margin">20dp</dimen> 11 <dimen name="dialog_margin">20dp</dimen>
12 <dimen name="elevated_app_bar">3dp</dimen> 12 <dimen name="elevated_app_bar">3dp</dimen>