diff --git a/android/.idea/codeStyles/codeStyleConfig.xml b/android/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 000000000..a55e7a179
--- /dev/null
+++ b/android/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp
index 9b9fa79cf..99b21248f 100644
--- a/android/app/src/cpp/android_host_interface.cpp
+++ b/android/app/src/cpp/android_host_interface.cpp
@@ -5,6 +5,7 @@
#include "android_audio_stream.h"
#include "android_gles_host_display.h"
#include "core/gpu.h"
+#include "core/game_list.h"
#include "core/host_display.h"
#include "core/system.h"
#include
@@ -35,6 +36,9 @@ static AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj)
static std::string JStringToString(JNIEnv* env, jstring str)
{
+ if (str == nullptr)
+ return {};
+
jsize length = env->GetStringUTFLength(str);
if (length == 0)
return {};
@@ -427,3 +431,47 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, j
hi->RunOnEmulationThread(
[hi, native_surface, format, width, height]() { hi->SurfaceChanged(native_surface, format, width, height); }, true);
}
+
+DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_cache_path, jstring j_redump_dat_path, jarray j_search_directories, jboolean search_recursively)
+{
+ const std::string cache_path = JStringToString(env, j_cache_path);
+ const std::string redump_dat_path = JStringToString(env, j_redump_dat_path);
+
+ GameList gl;
+ if (!redump_dat_path.empty())
+ gl.ParseRedumpDatabase(redump_dat_path.c_str());
+
+ const jsize search_directories_size = env->GetArrayLength(j_search_directories);
+ for (jsize i = 0; i < search_directories_size; i++)
+ {
+ jobject search_dir_obj = env->GetObjectArrayElement(reinterpret_cast(j_search_directories), i);
+ const std::string search_dir = JStringToString(env, reinterpret_cast(search_dir_obj));
+ if (!search_dir.empty())
+ gl.AddDirectory(search_dir.c_str(), search_recursively);
+ }
+
+ jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry");
+ Assert(entry_class != nullptr);
+
+ jmethodID entry_constructor = env->GetMethodID(entry_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V");
+ Assert(entry_constructor != nullptr);
+
+ jobjectArray entry_array = env->NewObjectArray(gl.GetEntryCount(), entry_class, nullptr);
+ Assert(entry_array != nullptr);
+
+ u32 counter = 0;
+ for (const GameList::GameListEntry& entry : gl.GetEntries())
+ {
+ jstring path = env->NewStringUTF(entry.path.c_str());
+ jstring code = env->NewStringUTF(entry.code.c_str());
+ jstring title = env->NewStringUTF(entry.title.c_str());
+ jstring region = env->NewStringUTF(Settings::GetConsoleRegionName(entry.region));
+ jlong size = entry.total_size;
+
+ jobject entry_jobject = env->NewObject(entry_class, entry_constructor, path, code, title, region, size);
+
+ env->SetObjectArrayElement(entry_array, counter++, entry_jobject);
+ }
+
+ return entry_array;
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java b/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java
new file mode 100644
index 000000000..0f56417a1
--- /dev/null
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/ConsoleRegion.java
@@ -0,0 +1,8 @@
+package com.github.stenzek.duckstation;
+
+public enum ConsoleRegion {
+ AutoDetect,
+ NTSC_J,
+ NTSC_U,
+ PAL
+}
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
index f598d5fd1..d8f7a024c 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
@@ -21,7 +21,9 @@ import androidx.core.app.NavUtils;
* status bar and navigation/system bar) with user interaction.
*/
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
- /** Interface to the native emulator core */
+ /**
+ * Interface to the native emulator core
+ */
AndroidHostInterface mHostInterface;
/**
@@ -104,11 +106,13 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return;
}
- String filename = new String();
- String state_filename = new String();
- if (!mHostInterface.startEmulationThread(holder.getSurface(),filename, state_filename))
- {
+ String bootPath = getIntent().getStringExtra("bootPath");
+ String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath");
+
+ if (!mHostInterface
+ .startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) {
Log.e("EmulationActivity", "Failed to start emulation thread");
+ finishActivity(0);
return;
}
}
@@ -133,7 +137,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
}
mVisible = true;
- mContentView = (SurfaceView)findViewById(R.id.fullscreen_content);
+ mContentView = (SurfaceView) findViewById(R.id.fullscreen_content);
Log.e("EmulationActivity", "adding callback");
mContentView.getHolder().addCallback(this);
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java
new file mode 100644
index 000000000..98825fd55
--- /dev/null
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java
@@ -0,0 +1,95 @@
+package com.github.stenzek.duckstation;
+
+// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.storage.StorageManager;
+import android.provider.DocumentsContract;
+
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+
+public final class FileUtil {
+ static String TAG="TAG";
+ private static final String PRIMARY_VOLUME_NAME = "primary";
+
+ @Nullable
+ public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
+ if (treeUri == null) return null;
+ String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con);
+ if (volumePath == null) return File.separator;
+ if (volumePath.endsWith(File.separator))
+ volumePath = volumePath.substring(0, volumePath.length() - 1);
+
+ String documentPath = getDocumentPathFromTreeUri(treeUri);
+ if (documentPath.endsWith(File.separator))
+ documentPath = documentPath.substring(0, documentPath.length() - 1);
+
+ if (documentPath.length() > 0) {
+ if (documentPath.startsWith(File.separator))
+ return volumePath + documentPath;
+ else
+ return volumePath + File.separator + documentPath;
+ }
+ else return volumePath;
+ }
+
+
+ @SuppressLint("ObsoleteSdkInt")
+ private static String getVolumePath(final String volumeId, Context context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
+ try {
+ StorageManager mStorageManager =
+ (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+ Class> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
+ Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
+ Method getUuid = storageVolumeClazz.getMethod("getUuid");
+ Method getPath = storageVolumeClazz.getMethod("getPath");
+ Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
+ Object result = getVolumeList.invoke(mStorageManager);
+
+ final int length = Array.getLength(result);
+ for (int i = 0; i < length; i++) {
+ Object storageVolumeElement = Array.get(result, i);
+ String uuid = (String) getUuid.invoke(storageVolumeElement);
+ Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
+
+ // primary volume?
+ if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
+ return (String) getPath.invoke(storageVolumeElement);
+
+ // other volumes?
+ if (uuid != null && uuid.equals(volumeId))
+ return (String) getPath.invoke(storageVolumeElement);
+ }
+ // not found.
+ return null;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static String getVolumeIdFromTreeUri(final Uri treeUri) {
+ final String docId = DocumentsContract.getTreeDocumentId(treeUri);
+ final String[] split = docId.split(":");
+ if (split.length > 0) return split[0];
+ else return null;
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static String getDocumentPathFromTreeUri(final Uri treeUri) {
+ final String docId = DocumentsContract.getTreeDocumentId(treeUri);
+ final String[] split = docId.split(":");
+ if ((split.length >= 2) && (split[1] != null)) return split[1];
+ else return File.separator;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java
new file mode 100644
index 000000000..94ee34279
--- /dev/null
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java
@@ -0,0 +1,102 @@
+package com.github.stenzek.duckstation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceManager;
+
+import java.util.Set;
+
+public class GameList {
+ static {
+ System.loadLibrary("duckstation-native");
+ }
+
+ private Context mContext;
+ private String mCachePath;
+ private String mRedumpDatPath;
+ private String[] mSearchDirectories;
+ private boolean mSearchRecursively;
+ private GameListEntry[] mEntries;
+
+ static private native GameListEntry[] getEntries(String cachePath, String redumpDatPath,
+ String[] searchDirectories,
+ boolean searchRecursively);
+
+ public GameList(Context context) {
+ mContext = context;
+ refresh();
+ }
+
+ public void refresh() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mCachePath = preferences.getString("GameList/CachePath", "");
+ mRedumpDatPath = preferences.getString("GameList/RedumpDatPath", "");
+
+ Set searchDirectories =
+ preferences.getStringSet("GameList/SearchDirectories", null);
+ if (searchDirectories != null) {
+ mSearchDirectories = new String[searchDirectories.size()];
+ searchDirectories.toArray(mSearchDirectories);
+ } else {
+ mSearchDirectories = new String[0];
+ }
+
+ mSearchRecursively = preferences.getBoolean("GameList/SearchRecursively", true);
+
+ // Search and get entries from native code
+ mEntries = getEntries(mCachePath, mRedumpDatPath, mSearchDirectories, mSearchRecursively);
+ }
+
+ public int getEntryCount() {
+ return mEntries.length;
+ }
+
+ public GameListEntry getEntry(int index) {
+ return mEntries[index];
+ }
+
+ private class ListViewAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mEntries.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mEntries[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(mContext)
+ .inflate(R.layout.game_list_view_entry, parent, false);
+ }
+
+ mEntries[position].fillView(convertView);
+ return convertView;
+ }
+ }
+
+ public BaseAdapter getListViewAdapter() {
+ return new ListViewAdapter();
+ }
+}
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java
new file mode 100644
index 000000000..d4d4cab30
--- /dev/null
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java
@@ -0,0 +1,69 @@
+package com.github.stenzek.duckstation;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+
+public class GameListEntry {
+ private String mPath;
+ private String mCode;
+ private String mTitle;
+ private ConsoleRegion mRegion;
+ private long mSize;
+
+ public GameListEntry(String path, String code, String title, String region, long size) {
+ mPath = path;
+ mCode = code;
+ mTitle = title;
+ mSize = size;
+
+ try {
+ mRegion = ConsoleRegion.valueOf(region);
+ } catch (IllegalArgumentException e) {
+ mRegion = ConsoleRegion.NTSC_U;
+ }
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public String getCode() {
+ return mCode;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public ConsoleRegion getRegion() {
+ return mRegion;
+ }
+
+ public void fillView(View view) {
+ ((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle);
+ ((TextView) view.findViewById(R.id.game_list_view_entry_path)).setText(mPath);
+
+ String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0);
+ ((TextView) view.findViewById(R.id.game_list_view_entry_size)).setText(sizeString);
+
+ int drawableId;
+ switch (mRegion) {
+ case NTSC_J:
+ drawableId = R.drawable.flag_jp;
+ break;
+ case NTSC_U:
+ default:
+ drawableId = R.drawable.flag_us;
+ break;
+ case PAL:
+ drawableId = R.drawable.flag_eu;
+ break;
+ }
+
+ ((ImageView) view.findViewById(R.id.game_list_view_entry_region_icon))
+ .setImageDrawable(ContextCompat.getDrawable(view.getContext(), drawableId));
+ }
+}
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
index 3250312a8..38bdbecde 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
@@ -1,7 +1,9 @@
package com.github.stenzek.duckstation;
import android.Manifest;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -11,13 +13,32 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.preference.PreferenceManager;
import android.content.Intent;
+
+import androidx.collection.ArraySet;
+
+import android.util.Log;
+import android.view.Gravity;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.prefs.Preferences;
public class MainActivity extends AppCompatActivity {
+ private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1;
+ private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2;
+
+ private GameList mGameList;
+ private ListView mGameListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -30,9 +51,39 @@ public class MainActivity extends AppCompatActivity {
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- /*Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
- .setAction("Action", null).show();*/
- startEmulation("nonexistant.cue");
+ Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ i.addCategory(Intent.CATEGORY_DEFAULT);
+ i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+ startActivityForResult(Intent.createChooser(i, "Choose directory"),
+ REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
+ }
+ });
+
+ // Set up game list view.
+ mGameList = new GameList(this);
+ mGameListView = findViewById(R.id.game_list_view);
+ mGameListView.setAdapter(mGameList.getListViewAdapter());
+ mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ startEmulation(mGameList.getEntry(position).getPath());
+ }
+ });
+ mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, int position,
+ long id) {
+ PopupMenu menu = new PopupMenu(MainActivity.this, view,
+ Gravity.RIGHT | Gravity.TOP);
+ menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
+ menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return false;
+ }
+ });
+ menu.show();
+ return true;
}
});
}
@@ -61,22 +112,71 @@ public class MainActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
+ if (resultCode != RESULT_OK)
+ return;
+
+ Uri treeUri = data.getData();
+ String path = FileUtil.getFullPathFromTreeUri(treeUri, this);
+ if (path.length() < 5) {
+ // sanity check for non-external paths.. do we need permissions or something?
+ return;
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ Set currentValues = prefs.getStringSet("GameList/SearchDirectories", null);
+ if (currentValues == null)
+ currentValues = new HashSet();
+
+ currentValues.add(path);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet("GameList/SearchDirectories", currentValues);
+ editor.apply();
+ Log.i("MainActivity", "Added path '" + path + "' to game list search directories");
+ mGameList.refresh();
+ }
+ break;
+ }
+ }
+
private boolean checkForExternalStoragePermissions() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
- ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
- {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
+ PackageManager.PERMISSION_GRANTED &&
+ ContextCompat
+ .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
+ PackageManager.PERMISSION_GRANTED) {
return true;
}
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ REQUEST_EXTERNAL_STORAGE_PERMISSIONS);
return false;
}
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ // check that all were successful
+ for (int i = 0; i < grantResults.length; i++) {
+ if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
+ Snackbar.make(mGameListView,
+ "External storage permissions are required to start emulation.",
+ Snackbar.LENGTH_LONG);
+ }
+ }
+ }
+
private boolean startEmulation(String bootPath) {
if (!checkForExternalStoragePermissions()) {
- Snackbar.make(findViewById(R.id.fab), "External storage permissions are required to start emulation.", Snackbar.LENGTH_LONG);
return false;
}
+
Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath);
startActivity(intent);
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java b/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java
deleted file mode 100644
index 2f7e759ed..000000000
--- a/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.stenzek.duckstation;
-
-public class NativeLibrary {
- static
- {
- System.loadLibrary("duckstation-native");
- }
-
- public native boolean createSystem();
- public native boolean bootSystem(String filename, String stateFilename);
- public native void runFrame();
-}
diff --git a/android/app/src/main/res/drawable/flag_eu.xml b/android/app/src/main/res/drawable/flag_eu.xml
new file mode 100644
index 000000000..a3bf3c7f0
--- /dev/null
+++ b/android/app/src/main/res/drawable/flag_eu.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/flag_jp.xml b/android/app/src/main/res/drawable/flag_jp.xml
new file mode 100644
index 000000000..8ff9d256f
--- /dev/null
+++ b/android/app/src/main/res/drawable/flag_jp.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/flag_us.xml b/android/app/src/main/res/drawable/flag_us.xml
new file mode 100644
index 000000000..08c798d33
--- /dev/null
+++ b/android/app/src/main/res/drawable/flag_us.xml
@@ -0,0 +1,332 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index ddd8a31b1..39a7c463f 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -29,6 +29,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
- app:srcCompat="@android:drawable/ic_dialog_email" />
+ app:backgroundTint="@android:color/background_light"
+ app:srcCompat="@android:drawable/ic_input_add" />
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/content_main.xml b/android/app/src/main/res/layout/content_main.xml
index a2189464f..1ccf56cdf 100644
--- a/android/app/src/main/res/layout/content_main.xml
+++ b/android/app/src/main/res/layout/content_main.xml
@@ -1,21 +1,20 @@
-
+ tools:context=".MainActivity"
+ tools:showIn="@layout/activity_main">
-
-
+ app:layout_constraintTop_toTopOf="parent"
+/>
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/game_list_view_entry.xml b/android/app/src/main/res/layout/game_list_view_entry.xml
new file mode 100644
index 000000000..1ba1ab962
--- /dev/null
+++ b/android/app/src/main/res/layout/game_list_view_entry.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/menu/menu_game_list_entry.xml b/android/app/src/main/res/menu/menu_game_list_entry.xml
new file mode 100644
index 000000000..45bbbb8c6
--- /dev/null
+++ b/android/app/src/main/res/menu/menu_game_list_entry.xml
@@ -0,0 +1,22 @@
+
+
\ No newline at end of file