Added option to show graphic packs for installed games only

This commit is contained in:
SSimco
2024-09-01 22:34:44 +03:00
parent f0effa0f31
commit 3678a0e474
13 changed files with 190 additions and 36 deletions
@@ -3,7 +3,8 @@
#include "GameIconLoader.h"
#include "JNIUtils.h"
class AndroidGameIconLoadedCallback : public GameIconLoadedCallback {
class AndroidGameIconLoadedCallback : public GameIconLoadedCallback
{
jmethodID m_onGameIconLoadedMID;
JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj;
@@ -18,7 +19,7 @@ class AndroidGameIconLoadedCallback : public GameIconLoadedCallback {
JNIUtils::ScopedJNIENV env;
jintArray jIconData = env->NewIntArray(width * height);
env->SetIntArrayRegion(jIconData, 0, width * height, reinterpret_cast<const jint*>(colors));
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, *reinterpret_cast<const jlong*>(&titleId), jIconData, width, height);
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, static_cast<jlong>(titleId), jIconData, width, height);
env->DeleteLocalRef(jIconData);
}
};
@@ -15,7 +15,7 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback {
{
JNIUtils::ScopedJNIENV env;
jstring name = env->NewStringUTF(game.name.c_str());
jlong titleId = *reinterpret_cast<const jlong*>(&game.titleId);
jlong titleId = static_cast<const jlong>(game.titleId);
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, titleId, name);
env->DeleteLocalRef(name);
}
+11
View File
@@ -53,6 +53,17 @@ jobject JNIUtils::createArrayList(JNIEnv* env, const std::vector<jobject>& objec
return arrayList;
}
jobject JNIUtils::createJavaLongArrayList(JNIEnv* env, const std::vector<uint64_t>& values)
{
jclass longClass = env->FindClass("java/lang/Long");
jmethodID valueOf = env->GetStaticMethodID(longClass, "valueOf", "(J)Ljava/lang/Long;");
std::vector<jobject> valuesJava;
for (auto&& value : values)
valuesJava.push_back(env->CallStaticObjectMethod(longClass, valueOf, static_cast<jlong>(value)));
env->DeleteLocalRef(longClass);
return JNIUtils::createArrayList(env, valuesJava);
}
jobject JNIUtils::createJavaStringArrayList(JNIEnv* env, const std::vector<std::wstring>& strings)
{
jclass arrayListClass = env->FindClass("java/util/ArrayList");
+7 -3
View File
@@ -13,7 +13,8 @@ namespace JNIUtils
jobject createJavaStringArrayList(JNIEnv* env, const std::vector<std::wstring>& stringList);
class ScopedJNIENV {
class ScopedJNIENV
{
public:
ScopedJNIENV()
{
@@ -51,7 +52,8 @@ namespace JNIUtils
bool m_threadWasAttached = false;
};
class Scopedjobject {
class Scopedjobject
{
public:
Scopedjobject() = default;
@@ -103,7 +105,8 @@ namespace JNIUtils
jobject m_jobject = nullptr;
};
class Scopedjclass {
class Scopedjclass
{
public:
Scopedjclass() = default;
@@ -155,4 +158,5 @@ namespace JNIUtils
Scopedjobject getEnumValue(JNIEnv* env, const std::string& enumClassName, const std::string& enumName);
jobject createArrayList(JNIEnv* env, const std::vector<jobject>& objects);
jobject createJavaLongArrayList(JNIEnv* env, const std::vector<uint64_t>& values);
} // namespace JNIUtils
+15 -6
View File
@@ -6,6 +6,8 @@
EmulationState s_emulationState;
static_assert(sizeof(TitleId) == sizeof(jlong));
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, [[maybe_unused]] void* reserved)
{
JNIUtils::g_jvm = vm;
@@ -27,7 +29,7 @@ Java_info_cemu_Cemu_NativeLibrary_setSurfaceSize([[maybe_unused]] JNIEnv* env, [
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong title_id)
{
s_emulationState.startGame(*reinterpret_cast<TitleId*>(&title_id));
s_emulationState.startGame(static_cast<TitleId>(title_id));
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
@@ -50,7 +52,7 @@ Java_info_cemu_Cemu_NativeLibrary_setGameIconLoadedCallback(JNIEnv* env, [[maybe
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_NativeLibrary_requestGameIcon([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong title_id)
{
s_emulationState.requestGameIcon(*reinterpret_cast<TitleId*>(&title_id));
s_emulationState.requestGameIcon(static_cast<TitleId>(title_id));
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
@@ -297,10 +299,10 @@ Java_info_cemu_Cemu_NativeLibrary_refreshGraphicPacks([[maybe_unused]] JNIEnv* e
}
extern "C" JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_NativeLibrary_getGraphicPackIdsAndVirtualPaths(JNIEnv* env, [[maybe_unused]] jclass clazz)
Java_info_cemu_Cemu_NativeLibrary_getGraphicPackBasicInfos(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
auto graphicPackInfoClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackIdAndVirtualPath");
auto graphicPackInfoCtorId = env->GetMethodID(graphicPackInfoClass, "<init>", "(JLjava/lang/String;)V");
auto graphicPackInfoClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackBasicInfo");
auto graphicPackInfoCtorId = env->GetMethodID(graphicPackInfoClass, "<init>", "(JLjava/lang/String;Ljava/util/ArrayList;)V");
auto graphicPacks = s_emulationState.getGraphicPacks();
std::vector<jobject> graphicPackInfoJObjects;
@@ -308,7 +310,8 @@ Java_info_cemu_Cemu_NativeLibrary_getGraphicPackIdsAndVirtualPaths(JNIEnv* env,
{
jstring virtualPath = env->NewStringUTF(graphicPack.second->GetVirtualPath().c_str());
jlong id = graphicPack.first;
jobject jGraphicPack = env->NewObject(graphicPackInfoClass, graphicPackInfoCtorId, id, virtualPath);
jobject titleIds = JNIUtils::createJavaLongArrayList(env, graphicPack.second->GetTitleIds());
jobject jGraphicPack = env->NewObject(graphicPackInfoClass, graphicPackInfoCtorId, id, virtualPath, titleIds);
graphicPackInfoJObjects.push_back(jGraphicPack);
}
return JNIUtils::createArrayList(env, graphicPackInfoJObjects);
@@ -589,3 +592,9 @@ Java_info_cemu_Cemu_NativeLibrary_onTouchMove([[maybe_unused]] JNIEnv* env, [[ma
{
s_emulationState.onTouchMove(x, y, isTV);
}
extern "C" JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_NativeLibrary_getInstalledGamesTitleIds(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return JNIUtils::createJavaLongArrayList(env, CafeTitleList::GetAllTitleIds());
}
@@ -241,10 +241,12 @@ public class NativeLibrary {
public static native int getAudioDeviceVolume(boolean tv);
public record GraphicPackIdAndVirtualPath(long id, String virtualPath) {
public record GraphicPackBasicInfo(long id, String virtualPath, ArrayList<Long> titleIds) {
}
public static native ArrayList<GraphicPackIdAndVirtualPath> getGraphicPackIdsAndVirtualPaths();
public static native ArrayList<Long> getInstalledGamesTitleIds();
public static native ArrayList<GraphicPackBasicInfo> getGraphicPackBasicInfos();
public static class GraphicPackPreset {
private final long graphicPackId;
@@ -0,0 +1,64 @@
package info.cemu.Cemu.guibasecomponents;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class FilterableRecyclerViewAdapter<T extends RecyclerViewItem> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Predicate<T> predicate;
private final List<T> recyclerViewItems = new ArrayList<>();
private List<T> filteredRecyclerViewItems = new ArrayList<>();
public void addRecyclerViewItem(T recyclerViewItem) {
recyclerViewItems.add(recyclerViewItem);
if (predicate == null || predicate.test(recyclerViewItem)) {
filteredRecyclerViewItems.add(recyclerViewItem);
notifyItemInserted(filteredRecyclerViewItems.size() - 1);
}
}
public void setPredicate(@NonNull Predicate<T> predicate) {
this.predicate = predicate;
int oldSize = filteredRecyclerViewItems.size();
filteredRecyclerViewItems = recyclerViewItems.stream().filter(predicate).collect(Collectors.toList());
if (oldSize != filteredRecyclerViewItems.size())
notifyDataSetChanged();
}
public void clearPredicate() {
int oldSize = filteredRecyclerViewItems.size();
filteredRecyclerViewItems = new ArrayList<>(recyclerViewItems);
if (oldSize != filteredRecyclerViewItems.size())
notifyDataSetChanged();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
return filteredRecyclerViewItems.get(position).onCreateViewHolder(parent);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
filteredRecyclerViewItems.get(position).onBindViewHolder(holder, this);
}
@Override
public int getItemCount() {
return filteredRecyclerViewItems.size();
}
public void clearRecyclerViewItems() {
int size = filteredRecyclerViewItems.size();
filteredRecyclerViewItems.clear();
recyclerViewItems.clear();
notifyItemRangeRemoved(0, size);
}
}
@@ -34,11 +34,13 @@ public class GraphicPackItemRecyclerViewItem implements RecyclerViewItem {
}
private final String text;
public final boolean hasInstalledTitleId;
private final GraphicPackItemType graphicPackItemType;
private final OnClickCallback onClickCallback;
public GraphicPackItemRecyclerViewItem(String text, GraphicPackItemType graphicPackItemType, OnClickCallback onClickCallback) {
public GraphicPackItemRecyclerViewItem(String text, boolean hasInstalledTitleId, GraphicPackItemType graphicPackItemType, OnClickCallback onClickCallback) {
this.text = text;
this.hasInstalledTitleId = hasInstalledTitleId;
this.graphicPackItemType = graphicPackItemType;
this.onClickCallback = onClickCallback;
}
@@ -63,6 +63,7 @@ public class GraphicPacksFragment extends Fragment {
? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION
: GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK;
GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name,
node.hasTitleIdInstalled(),
graphicPackItemType,
() -> {
graphicPackCurrentViewModel.setGraphicPackNode(node);
@@ -40,11 +40,13 @@ public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<Recycl
private final String name;
private final String path;
private final boolean hasInstalledTitleId;
private final OnClickCallback onClickCallback;
private GraphicPackSearchItemRecyclerViewItem(String name, String path, OnClickCallback onClickCallback) {
private GraphicPackSearchItemRecyclerViewItem(String name, String path, boolean hasInstalledTitleId, OnClickCallback onClickCallback) {
this.name = name;
this.path = path;
this.hasInstalledTitleId = hasInstalledTitleId;
this.onClickCallback = onClickCallback;
}
@@ -67,6 +69,8 @@ public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<Recycl
}
private final OnItemClickCallback onItemClickCallback;
private boolean showInstalledGamesOnly;
private String filterText = "";
public GraphicPacksRecyclerViewAdapter(OnItemClickCallback onItemClickCallback) {
this.onItemClickCallback = onItemClickCallback;
@@ -75,6 +79,11 @@ public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<Recycl
private List<GraphicPackSearchItemRecyclerViewItem> recyclerViewItems = new ArrayList<>();
private List<GraphicPackSearchItemRecyclerViewItem> filteredViewItems = new ArrayList<>();
public void setShowInstalledOnly(boolean showInstalledGamesOnly) {
this.showInstalledGamesOnly = showInstalledGamesOnly;
filter();
}
public void setItems(List<GraphicPacksRootFragment.GraphicPackDataNode> graphicPackDataNodes) {
clearItems();
recyclerViewItems = graphicPackDataNodes.stream()
@@ -83,28 +92,44 @@ public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<Recycl
new GraphicPackSearchItemRecyclerViewItem(
graphicPackDataNode.getName(),
graphicPackDataNode.getPath(),
graphicPackDataNode.hasTitleIdInstalled(),
() -> GraphicPacksRecyclerViewAdapter.this.onItemClickCallback.onClick(graphicPackDataNode)
)).collect(Collectors.toList());
filteredViewItems = recyclerViewItems;
if (showInstalledGamesOnly)
filteredViewItems = filteredViewItems.stream().filter(g -> g.hasInstalledTitleId).collect(Collectors.toList());
notifyItemRangeInserted(0, filteredViewItems.size());
}
public void filter(String text) {
if (text.isBlank()) {
filteredViewItems = recyclerViewItems;
notifyDataSetChanged();
private void resetFilteredItems() {
filteredViewItems = recyclerViewItems;
if (showInstalledGamesOnly)
filteredViewItems = filteredViewItems.stream().filter(g -> g.hasInstalledTitleId).collect(Collectors.toList());
notifyDataSetChanged();
}
public void setFilterText(String filterText) {
this.filterText = filterText;
filter();
}
public void filter() {
if (filterText.isBlank()) {
resetFilteredItems();
return;
}
var stringBuilder = new StringBuilder();
String pattern = Arrays.stream(text.split(" "))
String pattern = Arrays.stream(filterText.split(" "))
.map(s -> "(?=.*" + Pattern.quote(s) + ")")
.collect(() -> stringBuilder, StringBuilder::append, StringBuilder::append)
.append(".*")
.toString();
var regex = Pattern.compile(pattern, CASE_INSENSITIVE);
filteredViewItems = recyclerViewItems.stream()
.filter(g -> regex.matcher(g.path).matches())
.collect(Collectors.toList());
var filteredData = recyclerViewItems.stream();
if (showInstalledGamesOnly)
filteredData = filteredData.filter(g -> g.hasInstalledTitleId);
filteredData = filteredData.filter(g -> regex.matcher(g.path).matches());
filteredViewItems = filteredData.collect(Collectors.toList());
notifyDataSetChanged();
}
@@ -112,6 +137,7 @@ public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<Recycl
if (filteredViewItems.isEmpty()) return;
int itemsCount = filteredViewItems.size();
filteredViewItems.clear();
recyclerViewItems.clear();
notifyItemRangeRemoved(0, itemsCount);
}
@@ -40,6 +40,7 @@ import java.util.Optional;
import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.GenericRecyclerViewLayoutBinding;
import info.cemu.Cemu.guibasecomponents.FilterableRecyclerViewAdapter;
import info.cemu.Cemu.guibasecomponents.GenericRecyclerViewAdapter;
import info.cemu.Cemu.NativeLibrary;
import info.cemu.Cemu.utils.FileUtil;
@@ -53,8 +54,13 @@ import okhttp3.ResponseBody;
public class GraphicPacksRootFragment extends Fragment {
public abstract static class GraphicPackNode {
protected boolean titleIdInstalled;
protected String name;
public boolean hasTitleIdInstalled() {
return titleIdInstalled;
}
public String getName() {
return name;
}
@@ -64,10 +70,11 @@ public class GraphicPacksRootFragment extends Fragment {
private final long id;
private final String path;
public GraphicPackDataNode(long id, String name, String path) {
public GraphicPackDataNode(long id, String name, String path, boolean hasTitleIdInstalled) {
this.id = id;
this.name = name;
this.path = path;
this.titleIdInstalled = hasTitleIdInstalled;
}
public long getId() {
@@ -82,8 +89,9 @@ public class GraphicPacksRootFragment extends Fragment {
public static class GraphicPackSectionNode extends GraphicPackNode {
ArrayList<GraphicPackNode> children = new ArrayList<>();
public GraphicPackSectionNode(String name) {
public GraphicPackSectionNode(String name, boolean hasTitleIdInstalled) {
this.name = name;
this.titleIdInstalled = hasTitleIdInstalled;
}
public GraphicPackSectionNode() {
@@ -96,17 +104,17 @@ public class GraphicPacksRootFragment extends Fragment {
private void addGraphicPackDataByTokens(List<String> tokens, GraphicPackDataNode dataNode) {
var node = this;
for (var token : tokens) {
node = getOrAddSectionByToken(token, node.children);
node = getOrAddSectionByToken(token, node.children, dataNode.titleIdInstalled);
}
node.children.add(dataNode);
}
private GraphicPackSectionNode getOrAddSectionByToken(String token, ArrayList<GraphicPackNode> graphicPackNodes) {
private GraphicPackSectionNode getOrAddSectionByToken(String token, ArrayList<GraphicPackNode> graphicPackNodes, boolean hasTitleIdInstalled) {
var sectionNodeOptional = graphicPackNodes.stream()
.filter(g -> (g instanceof GraphicPackSectionNode) && g.name.equals(token))
.findFirst();
if (!sectionNodeOptional.isPresent()) {
var sectionNode = new GraphicPackSectionNode(token);
var sectionNode = new GraphicPackSectionNode(token, hasTitleIdInstalled);
graphicPackNodes.add(sectionNode);
return sectionNode;
}
@@ -136,8 +144,9 @@ public class GraphicPacksRootFragment extends Fragment {
}
}
private final ArrayList<Long> installedTitleIds = NativeLibrary.getInstalledGamesTitleIds();
private final GraphicPackSectionNode graphicPackSectionRootNode = new GraphicPackSectionNode();
private final GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
private final FilterableRecyclerViewAdapter<GraphicPackItemRecyclerViewItem> genericRecyclerViewAdapter = new FilterableRecyclerViewAdapter<>();
private GraphicPackViewModel graphicPackViewModel;
private final GraphicPacksRecyclerViewAdapter graphicPacksRecyclerViewAdapter = new GraphicPacksRecyclerViewAdapter(graphicPackDataNode -> {
graphicPackViewModel.setGraphicPackNode(graphicPackDataNode);
@@ -153,6 +162,8 @@ public class GraphicPacksRootFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
genericRecyclerViewAdapter.setPredicate(g -> g.hasInstalledTitleId);
graphicPacksRecyclerViewAdapter.setShowInstalledOnly(true);
fillGraphicPacks();
var navController = NavHostFragment.findNavController(GraphicPacksRootFragment.this);
graphicPackViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getCurrentBackStackEntry()))
@@ -173,13 +184,13 @@ public class GraphicPacksRootFragment extends Fragment {
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
graphicPacksRecyclerViewAdapter.filter(query);
graphicPacksRecyclerViewAdapter.setFilterText(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
graphicPacksRecyclerViewAdapter.filter(newText);
graphicPacksRecyclerViewAdapter.setFilterText(newText);
return true;
}
});
@@ -198,6 +209,14 @@ public class GraphicPacksRootFragment extends Fragment {
});
}
private void showInstalledOnly(boolean installedOnly) {
if (installedOnly) {
genericRecyclerViewAdapter.setPredicate(g -> g.hasInstalledTitleId);
return;
}
genericRecyclerViewAdapter.clearPredicate();
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_graphic_packs_download && !updateInProgress) {
@@ -207,6 +226,13 @@ public class GraphicPacksRootFragment extends Fragment {
if (menuItem.getItemId() == R.id.action_graphic_packs_search) {
return true;
}
if (menuItem.getItemId() == R.id.show_installed_games_only) {
boolean installedOnly = !menuItem.isChecked();
graphicPacksRecyclerViewAdapter.setShowInstalledOnly(installedOnly);
menuItem.setChecked(installedOnly);
showInstalledOnly(installedOnly);
return true;
}
return false;
}
}, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
@@ -214,12 +240,13 @@ public class GraphicPacksRootFragment extends Fragment {
}
private void fillGraphicPacks() {
var nativeGraphicPacks = NativeLibrary.getGraphicPackIdsAndVirtualPaths();
var nativeGraphicPacks = NativeLibrary.getGraphicPackBasicInfos();
graphicPackSectionRootNode.clear();
List<GraphicPackDataNode> graphicPackDataNodes = new ArrayList<>();
nativeGraphicPacks.forEach(graphicPack -> {
var tokens = Arrays.asList(graphicPack.virtualPath().split("/"));
var dataNode = new GraphicPackDataNode(graphicPack.id(), tokens.get(tokens.size() - 1), graphicPack.virtualPath());
boolean hasTitleIdInstalled = graphicPack.titleIds().stream().anyMatch(installedTitleIds::contains);
var dataNode = new GraphicPackDataNode(graphicPack.id(), tokens.get(tokens.size() - 1), graphicPack.virtualPath(), hasTitleIdInstalled);
graphicPackDataNodes.add(dataNode);
graphicPackSectionRootNode.addGraphicPackDataByTokens(tokens.subList(0, tokens.size() - 1), dataNode);
});
@@ -232,6 +259,7 @@ public class GraphicPacksRootFragment extends Fragment {
? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION
: GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK;
GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name,
node.hasTitleIdInstalled(),
graphicPackItemType,
() -> {
graphicPackViewModel.setGraphicPackNode(node);
@@ -1,6 +1,5 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_graphic_packs_search"
@@ -15,4 +14,10 @@
android:icon="@drawable/ic_download"
android:title="@string/download_graphic_packs_title"
app:showAsAction="ifRoom" />
<item
android:id="@+id/show_installed_games_only"
android:checkable="true"
android:checked="true"
android:title="@string/installed_games_title" />
</menu>
@@ -163,4 +163,5 @@
<string name="edit_inputs">Edit inputs</string>
<string name="show_pad">Show pad</string>
<string name="replace_tv_with_pad">Replace TV with PAD</string>
<string name="installed_games_title">Installed games</string>
</resources>