diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle
index 8f5ce60a76..9668d8af95 100644
--- a/Source/Android/app/build.gradle
+++ b/Source/Android/app/build.gradle
@@ -1,7 +1,8 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 21
+ // Leanback support requires >22
+ compileSdkVersion 22
buildToolsVersion "22.0.1"
lintOptions {
@@ -80,6 +81,9 @@ dependencies {
compile 'com.android.support:recyclerview-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
+ // Android TV UI libraries.
+ compile 'com.android.support:leanback-v17:22.2.0'
+
// For showing the banner as a circle a-la Material Design Guidelines
compile 'de.hdodenhof:circleimageview:1.2.2'
diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml
index 30fffd11e9..e08b2dd0a8 100644
--- a/Source/Android/app/src/main/AndroidManifest.xml
+++ b/Source/Android/app/src/main/AndroidManifest.xml
@@ -33,11 +33,22 @@
-
+
+
+
+
+
+
+
+
+
+
{
- private static final int REQUEST_ADD_DIRECTORY = 1;
+ public static final int REQUEST_ADD_DIRECTORY = 1;
public static final int REQUEST_EMULATE_GAME = 2;
/**
@@ -139,6 +139,7 @@ public final class MainActivity extends AppCompatActivity implements LoaderManag
{
fragment.refreshScreenshotAtPosition(resultCode);
}
+ break;
}
}
@@ -226,7 +227,7 @@ public final class MainActivity extends AppCompatActivity implements LoaderManag
GameProvider.URI_GAME, // URI of table to query
null, // Return all columns
GameDatabase.KEY_GAME_PLATFORM + " = ?", // Select by platform
- new String[]{Integer.toString(id)}, // Platform id is Loader id minus 1
+ new String[]{Integer.toString(id)}, // Platform id is Loader id
GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
);
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/TvMainActivity.java
new file mode 100644
index 0000000000..3f3b8c168d
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/TvMainActivity.java
@@ -0,0 +1,290 @@
+package org.dolphinemu.dolphinemu.activities;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v17.leanback.app.BrowseFragment;
+import android.support.v17.leanback.database.CursorMapper;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.CursorObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.widget.Toast;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.adapters.GameRowPresenter;
+import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter;
+import org.dolphinemu.dolphinemu.model.Game;
+import org.dolphinemu.dolphinemu.model.GameDatabase;
+import org.dolphinemu.dolphinemu.model.GameProvider;
+import org.dolphinemu.dolphinemu.model.TvSettingsItem;
+import org.dolphinemu.dolphinemu.services.AssetCopyService;
+import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
+
+public final class TvMainActivity extends Activity
+{
+ protected BrowseFragment mBrowseFragment;
+
+ private ArrayObjectAdapter mRowsAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_tv_main);
+
+ final FragmentManager fragmentManager = getFragmentManager();
+ mBrowseFragment = (BrowseFragment) fragmentManager.findFragmentById(
+ R.id.fragment_game_list);
+
+ // Set display parameters for the BrowseFragment
+ mBrowseFragment.setHeadersState(BrowseFragment.HEADERS_ENABLED);
+ mBrowseFragment.setTitle(getString(R.string.app_name));
+ mBrowseFragment.setBadgeDrawable(getResources().getDrawable(
+ R.drawable.ic_launcher, null));
+ mBrowseFragment.setBrandColor(getResources().getColor(R.color.dolphin_blue_dark));
+
+ buildRowsAdapter();
+
+ mBrowseFragment.setOnItemViewClickedListener(
+ new OnItemViewClickedListener()
+ {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)
+ {
+ // Special case: user clicked on a settings row item.
+ if (item instanceof TvSettingsItem)
+ {
+ TvSettingsItem settingsItem = (TvSettingsItem) item;
+
+ switch (settingsItem.getItemId())
+ {
+ case R.id.menu_refresh:
+ getContentResolver().insert(GameProvider.URI_REFRESH, null);
+
+ // TODO Let the Activity know the data is refreshed in some other, better way.
+ recreate();
+ break;
+
+ case R.id.menu_settings:
+ // Launch the Settings Actvity.
+ Intent settings = new Intent(TvMainActivity.this, SettingsActivity.class);
+ startActivity(settings);
+ break;
+
+ case R.id.button_add_directory:
+ Intent fileChooser = new Intent(TvMainActivity.this, AddDirectoryActivity.class);
+
+ // The second argument to this method is read below in onActivityResult().
+ startActivityForResult(fileChooser, MainActivity.REQUEST_ADD_DIRECTORY);
+
+ break;
+
+ default:
+ Toast.makeText(TvMainActivity.this, "Unimplemented menu option.", Toast.LENGTH_SHORT).show();
+ break;
+ }
+ }
+ else
+ {
+ TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder;
+ // Start the emulation activity and send the path of the clicked ISO to it.
+ Intent intent = new Intent(TvMainActivity.this, EmulationActivity.class);
+
+ intent.putExtra("SelectedGame", holder.path);
+ intent.putExtra("SelectedTitle", holder.title);
+ intent.putExtra("ScreenPath", holder.screenshotPath);
+
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ TvMainActivity.this,
+ holder.imageScreenshot,
+ "image_game_screenshot");
+
+ startActivity(intent, options.toBundle());
+ }
+ }
+ });
+
+ // Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
+ if (savedInstanceState == null)
+ {
+ NativeLibrary.SetUserDirectory(""); // Auto-Detect
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean assetsCopied = preferences.getBoolean("assetsCopied", false);
+
+ // Only perform these extensive copy operations once.
+ if (!assetsCopied)
+ {
+ // Copy assets into appropriate locations.
+ Intent copyAssets = new Intent(this, AssetCopyService.class);
+ startService(copyAssets);
+ }
+ }
+ }
+
+ /**
+ * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity.
+ *
+ * @param requestCode An int describing whether the Activity that is returning did so successfully.
+ * @param resultCode An int describing what Activity is giving us this callback.
+ * @param result The information the returning Activity is providing us.
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent result)
+ {
+ switch (requestCode)
+ {
+ case MainActivity.REQUEST_ADD_DIRECTORY:
+ // If the user picked a file, as opposed to just backing out.
+ if (resultCode == RESULT_OK)
+ {
+ // Sanity check to make sure the Activity that just returned was the AddDirectoryActivity;
+ // other activities might use this callback in the future (don't forget to change Javadoc!)
+ if (requestCode == MainActivity.REQUEST_ADD_DIRECTORY)
+ {
+ // TODO Let the Activity know the data is refreshed in some other, better way.
+ recreate();
+ }
+ }
+ break;
+ }
+ }
+
+ private void buildRowsAdapter()
+ {
+ mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+
+ // For each platform
+ for (int platformIndex = 0; platformIndex <= Game.PLATFORM_ALL; ++platformIndex)
+ {
+ ListRow row = buildGamesRow(platformIndex);
+
+ // Add row to the adapter only if it is not empty.
+ if (row != null)
+ {
+ mRowsAdapter.add(row);
+ }
+ }
+
+ ListRow settingsRow = buildSettingsRow();
+ mRowsAdapter.add(settingsRow);
+
+ mBrowseFragment.setAdapter(mRowsAdapter);
+ }
+
+ private ListRow buildGamesRow(int platform)
+ {
+ // Create an adapter for this row.
+ CursorObjectAdapter row = new CursorObjectAdapter(new GameRowPresenter());
+
+ Cursor games;
+ if (platform == Game.PLATFORM_ALL)
+ {
+ // Get all games.
+ games = getContentResolver().query(
+ GameProvider.URI_GAME, // URI of table to query
+ null, // Return all columns
+ null, // Return all games
+ null, // Return all games
+ GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
+ );
+ }
+ else
+ {
+ // Get games for this particular platform.
+ games = getContentResolver().query(
+ GameProvider.URI_GAME, // URI of table to query
+ null, // Return all columns
+ GameDatabase.KEY_GAME_PLATFORM + " = ?", // Select by platform
+ new String[]{Integer.toString(platform)}, // Platform id
+ GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
+ );
+ }
+
+ // If cursor is empty, don't return a Row.
+ if (!games.moveToFirst())
+ {
+ return null;
+ }
+
+ row.changeCursor(games);
+ row.setMapper(new CursorMapper()
+ {
+ @Override
+ protected void bindColumns(Cursor cursor)
+ {
+ // No-op? Not sure what this does.
+ }
+
+ @Override
+ protected Object bind(Cursor cursor)
+ {
+ return Game.fromCursor(cursor);
+ }
+ });
+
+ String headerName;
+ switch (platform)
+ {
+ case Game.PLATFORM_GC:
+ headerName = "GameCube Games";
+ break;
+
+ case Game.PLATFORM_WII:
+ headerName = "Wii Games";
+ break;
+
+ case Game.PLATFORM_WII_WARE:
+ headerName = "WiiWare";
+ break;
+
+ case Game.PLATFORM_ALL:
+ headerName = "All Games";
+ break;
+
+ default:
+ headerName = "Error";
+ break;
+ }
+
+ // Create a header for this row.
+ HeaderItem header = new HeaderItem(platform, headerName);
+
+ // Create the row, passing it the filled adapter and the header, and give it to the master adapter.
+ return new ListRow(header, row);
+ }
+
+ private ListRow buildSettingsRow()
+ {
+ ArrayObjectAdapter rowItems = new ArrayObjectAdapter(new SettingsRowPresenter());
+
+ rowItems.add(new TvSettingsItem(R.id.menu_refresh,
+ R.drawable.ic_refresh_tv,
+ R.string.grid_menu_refresh));
+
+ rowItems.add(new TvSettingsItem(R.id.menu_settings,
+ R.drawable.ic_settings_tv,
+ R.string.grid_menu_settings));
+
+ rowItems.add(new TvSettingsItem(R.id.button_add_directory,
+ R.drawable.ic_add_tv,
+ R.string.add_directory_title));
+
+ // Create a header for this row.
+ HeaderItem header = new HeaderItem(R.string.settings, getString(R.string.settings));
+
+ return new ListRow(header, rowItems);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java
new file mode 100644
index 0000000000..3b0b8095ce
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java
@@ -0,0 +1,118 @@
+package org.dolphinemu.dolphinemu.adapters;
+
+import android.graphics.Bitmap;
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.squareup.picasso.Picasso;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.model.Game;
+import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
+
+/**
+ * The Leanback library / docs call this a Presenter, but it works very
+ * similarly to a RecyclerView.ViewHolder.
+ */
+public final class GameRowPresenter extends Presenter
+{
+ public ViewHolder onCreateViewHolder(ViewGroup parent)
+ {
+ // Create a new view.
+ ImageCardView gameCard = new ImageCardView(parent.getContext())
+ {
+ @Override
+ public void setSelected(boolean selected)
+ {
+ setCardBackground(this, selected);
+ super.setSelected(selected);
+ }
+ };
+
+ gameCard.setMainImageAdjustViewBounds(true);
+ gameCard.setMainImageDimensions(480, 320);
+ gameCard.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP);
+
+ gameCard.setFocusable(true);
+ gameCard.setFocusableInTouchMode(true);
+
+ setCardBackground(gameCard, false);
+
+ // Use that view to create a ViewHolder.
+ return new TvGameViewHolder(gameCard);
+ }
+
+ public void onBindViewHolder(ViewHolder viewHolder, Object item)
+ {
+ TvGameViewHolder holder = (TvGameViewHolder) viewHolder;
+ Game game = (Game) item;
+
+ String screenPath = game.getScreenshotPath();
+
+ // Fill in the view contents.
+ Picasso.with(holder.imageScreenshot.getContext())
+ .load(screenPath)
+ .fit()
+ .centerCrop()
+ .noFade()
+ .noPlaceholder()
+ .config(Bitmap.Config.RGB_565)
+ .error(R.drawable.no_banner)
+ .into(holder.imageScreenshot);
+
+ holder.cardParent.setTitleText(game.getTitle());
+ holder.cardParent.setContentText(game.getCompany());
+
+ // TODO These shouldn't be necessary once the move to a DB-based model is complete.
+ holder.gameId = game.getGameId();
+ holder.path = game.getPath();
+ holder.title = game.getTitle();
+ holder.description = game.getDescription();
+ holder.country = game.getCountry();
+ holder.company = game.getCompany();
+ holder.screenshotPath = game.getScreenshotPath();
+
+ switch (game.getPlatform())
+ {
+ case Game.PLATFORM_GC:
+ holder.cardParent.setTag(R.color.dolphin_accent_gamecube);
+ break;
+
+ case Game.PLATFORM_WII:
+ holder.cardParent.setTag(R.color.dolphin_accent_wii);
+ break;
+
+ case Game.PLATFORM_WII_WARE:
+ holder.cardParent.setTag(R.color.dolphin_accent_wiiware);
+ break;
+
+ default:
+ holder.cardParent.setTag(android.R.color.holo_red_dark);
+ break;
+ }
+ }
+
+ public void onUnbindViewHolder(ViewHolder viewHolder)
+ {
+ // no op
+ }
+
+ public void setCardBackground(ImageCardView view, boolean selected)
+ {
+ int backgroundColor;
+
+ if (selected)
+ {
+ // TODO: 7/20/15 Try using view tag to set color
+ backgroundColor = (int) view.getTag();
+ }
+ else
+ {
+ backgroundColor = R.color.tv_card_unselected;
+ }
+
+ view.setInfoAreaBackgroundColor(view.getResources().getColor(backgroundColor));
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java
new file mode 100644
index 0000000000..beef06a218
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.java
@@ -0,0 +1,47 @@
+package org.dolphinemu.dolphinemu.adapters;
+
+
+import android.content.res.Resources;
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.ViewGroup;
+
+import org.dolphinemu.dolphinemu.model.TvSettingsItem;
+import org.dolphinemu.dolphinemu.viewholders.TvSettingsViewHolder;
+
+public final class SettingsRowPresenter extends Presenter
+{
+ public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent)
+ {
+ // Create a new view.
+ ImageCardView settingsCard = new ImageCardView(parent.getContext());
+
+ settingsCard.setMainImageAdjustViewBounds(true);
+ settingsCard.setMainImageDimensions(192, 160);
+
+
+ settingsCard.setFocusable(true);
+ settingsCard.setFocusableInTouchMode(true);
+
+ // Use that view to create a ViewHolder.
+ return new TvSettingsViewHolder(settingsCard);
+ }
+
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item)
+ {
+ TvSettingsViewHolder holder = (TvSettingsViewHolder) viewHolder;
+ TvSettingsItem settingsItem = (TvSettingsItem) item;
+
+ Resources resources = holder.cardParent.getResources();
+
+ holder.itemId = settingsItem.getItemId();
+
+ holder.cardParent.setTitleText(resources.getString(settingsItem.getLabelId()));
+ holder.cardParent.setMainImage(resources.getDrawable(settingsItem.getIconId(), null));
+ }
+
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder)
+ {
+ // no op
+ }
+}
\ No newline at end of file
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java
index f3ef6f7514..33a9f27c54 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java
@@ -3,13 +3,12 @@ package org.dolphinemu.dolphinemu.model;
import android.content.ContentValues;
import android.database.Cursor;
-import java.io.File;
-
public final class Game
{
public static final int PLATFORM_GC = 0;
public static final int PLATFORM_WII = 1;
public static final int PLATFORM_WII_WARE = 2;
+ public static final int PLATFORM_ALL = 3;
// Copied from IVolume::ECountry. Update these if that is ever modified.
public static final int COUNTRY_EUROPE = 0;
@@ -33,13 +32,13 @@ public final class Game
private String mDescription;
private String mPath;
private String mGameId;
- private String mScreenshotFolderPath;
+ private String mScreenshotPath;
private String mCompany;
private int mPlatform;
private int mCountry;
- public Game(int platform, String title, String description, int country, String path, String gameId, String company)
+ public Game(int platform, String title, String description, int country, String path, String gameId, String company, String screenshotPath)
{
mPlatform = platform;
mTitle = title;
@@ -48,7 +47,7 @@ public final class Game
mPath = path;
mGameId = gameId;
mCompany = company;
- mScreenshotFolderPath = PATH_SCREENSHOT_FOLDER + getGameId() + "/";
+ mScreenshotPath = screenshotPath;
}
public int getPlatform()
@@ -86,27 +85,9 @@ public final class Game
return mGameId;
}
- public String getScreenshotFolderPath()
+ public String getScreenshotPath()
{
- return mScreenshotFolderPath;
- }
-
- public String getScreenPath()
- {
- // Count how many screenshots are available, so we can use the most recent one.
- File screenshotFolder = new File(mScreenshotFolderPath.substring(mScreenshotFolderPath.indexOf('s') - 1));
- int screenCount = 0;
-
- if (screenshotFolder.isDirectory())
- {
- screenCount = screenshotFolder.list().length;
- }
-
- String screenPath = mScreenshotFolderPath
- + getGameId() + "-"
- + screenCount + ".png";
-
- return screenPath;
+ return mScreenshotPath;
}
public static ContentValues asContentValues(int platform, String title, String description, int country, String path, String gameId, String company)
@@ -135,6 +116,7 @@ public final class Game
cursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY),
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
- cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
+ cursor.getString(GameDatabase.GAME_COLUMN_COMPANY),
+ cursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH));
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TvSettingsItem.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TvSettingsItem.java
new file mode 100644
index 0000000000..ccd87bfa4c
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TvSettingsItem.java
@@ -0,0 +1,31 @@
+package org.dolphinemu.dolphinemu.model;
+
+
+public final class TvSettingsItem
+{
+ private final int mItemId;
+ private final int mIconId;
+ private final int mLabelId;
+
+ public TvSettingsItem(int itemId, int iconId, int labelId)
+ {
+ mItemId = itemId;
+ mIconId = iconId;
+ mLabelId = labelId;
+ }
+
+ public int getItemId()
+ {
+ return mItemId;
+ }
+
+ public int getIconId()
+ {
+ return mIconId;
+ }
+
+ public int getLabelId()
+ {
+ return mLabelId;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java
new file mode 100644
index 0000000000..d27a671c2f
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java
@@ -0,0 +1,39 @@
+package org.dolphinemu.dolphinemu.viewholders;
+
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.View;
+import android.widget.ImageView;
+
+/**
+ * A simple class that stores references to views so that the GameAdapter doesn't need to
+ * keep calling findViewById(), which is expensive.
+ */
+public final class TvGameViewHolder extends Presenter.ViewHolder
+{
+ public ImageCardView cardParent;
+
+ public ImageView imageScreenshot;
+
+ public String gameId;
+
+ // TODO Not need any of this stuff. Currently only the properties dialog needs it.
+ public String path;
+ public String title;
+ public String description;
+ public int country;
+ public String company;
+ public String screenshotPath;
+
+ public int backgroundColor;
+
+ public TvGameViewHolder(View itemView)
+ {
+ super(itemView);
+
+ itemView.setTag(this);
+
+ cardParent = (ImageCardView) itemView;
+ imageScreenshot = cardParent.getMainImageView();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvSettingsViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvSettingsViewHolder.java
new file mode 100644
index 0000000000..3264e93f5b
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvSettingsViewHolder.java
@@ -0,0 +1,23 @@
+package org.dolphinemu.dolphinemu.viewholders;
+
+
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.View;
+
+public final class TvSettingsViewHolder extends Presenter.ViewHolder
+{
+ public ImageCardView cardParent;
+
+ // Determines what action to take when this item is clicked.
+ public int itemId;
+
+ public TvSettingsViewHolder(View itemView)
+ {
+ super(itemView);
+
+ itemView.setTag(this);
+
+ cardParent = (ImageCardView) itemView;
+ }
+}
diff --git a/Source/Android/app/src/main/res/drawable/ic_add_tv.png b/Source/Android/app/src/main/res/drawable/ic_add_tv.png
new file mode 100644
index 0000000000..7332c75727
Binary files /dev/null and b/Source/Android/app/src/main/res/drawable/ic_add_tv.png differ
diff --git a/Source/Android/app/src/main/res/drawable/ic_refresh_tv.png b/Source/Android/app/src/main/res/drawable/ic_refresh_tv.png
new file mode 100644
index 0000000000..8bae5d34ab
Binary files /dev/null and b/Source/Android/app/src/main/res/drawable/ic_refresh_tv.png differ
diff --git a/Source/Android/app/src/main/res/drawable/ic_settings_tv.png b/Source/Android/app/src/main/res/drawable/ic_settings_tv.png
new file mode 100644
index 0000000000..9e242e7748
Binary files /dev/null and b/Source/Android/app/src/main/res/drawable/ic_settings_tv.png differ
diff --git a/Source/Android/app/src/main/res/layout/activity_tv_main.xml b/Source/Android/app/src/main/res/layout/activity_tv_main.xml
new file mode 100644
index 0000000000..922d874b02
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/activity_tv_main.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/Android/app/src/main/res/layout/card_game.xml b/Source/Android/app/src/main/res/layout/card_game.xml
index 9b172b6871..542969ebd6 100644
--- a/Source/Android/app/src/main/res/layout/card_game.xml
+++ b/Source/Android/app/src/main/res/layout/card_game.xml
@@ -33,7 +33,6 @@
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
- android:layout_toStartOf="@+id/button_details"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
diff --git a/Source/Android/app/src/main/res/values/colors.xml b/Source/Android/app/src/main/res/values/colors.xml
index d7b7847125..9052de99d5 100644
--- a/Source/Android/app/src/main/res/values/colors.xml
+++ b/Source/Android/app/src/main/res/values/colors.xml
@@ -8,4 +8,6 @@
#651fff
#bdbdbd
+
+ #444444
\ No newline at end of file
diff --git a/Source/Android/app/src/main/res/values/styles.xml b/Source/Android/app/src/main/res/values/styles.xml
index 76ee1f8f55..dba853f492 100644
--- a/Source/Android/app/src/main/res/values/styles.xml
+++ b/Source/Android/app/src/main/res/values/styles.xml
@@ -126,6 +126,21 @@
- false
+
+
+
+
+