Android: Add per-game settings and properties UI

This commit is contained in:
Connor McLaughlin 2021-01-03 18:14:02 +10:00
parent 7f008ea5c7
commit 4eee5ebdb7
13 changed files with 602 additions and 69 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -46,6 +46,8 @@ static jmethodID s_EmulationActivity_method_onGameTitleChanged;
static jmethodID s_EmulationActivity_method_setVibration;
static jclass s_PatchCode_class;
static jmethodID s_PatchCode_constructor;
static jclass s_GameListEntry_class;
static jmethodID s_GameListEntry_constructor;
namespace AndroidHelpers {
// helper for retrieving the current per-thread jni environment
@ -524,7 +526,6 @@ void AndroidHostInterface::UpdateControllerInterface()
}
}
void AndroidHostInterface::OnSystemPaused(bool paused)
{
CommonHostInterface::OnSystemPaused(paused);
@ -664,12 +665,11 @@ void AndroidHostInterface::HandleControllerButtonEvent(u32 controller_index, u32
if (!IsEmulationThreadRunning())
return;
RunOnEmulationThread(
[this, controller_index, button_index, pressed]() {
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
if (ci)
ci->HandleButtonEvent(controller_index, button_index, pressed);
});
RunOnEmulationThread([this, controller_index, button_index, pressed]() {
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
if (ci)
ci->HandleButtonEvent(controller_index, button_index, pressed);
});
}
void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value)
@ -677,12 +677,11 @@ void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 a
if (!IsEmulationThreadRunning())
return;
RunOnEmulationThread(
[this, controller_index, axis_index, value]() {
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
if (ci)
ci->HandleAxisEvent(controller_index, axis_index, value);
});
RunOnEmulationThread([this, controller_index, axis_index, value]() {
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
if (ci)
ci->HandleAxisEvent(controller_index, axis_index, value);
});
}
void AndroidHostInterface::SetFastForwardEnabled(bool enabled)
@ -811,7 +810,7 @@ jobjectArray AndroidHostInterface::GetInputProfileNames(JNIEnv* env) const
return name_array;
}
bool AndroidHostInterface::ApplyInputProfile(const char *profile_name)
bool AndroidHostInterface::ApplyInputProfile(const char* profile_name)
{
const std::string path(GetInputProfilePath(profile_name));
if (path.empty())
@ -822,7 +821,7 @@ bool AndroidHostInterface::ApplyInputProfile(const char *profile_name)
return true;
}
bool AndroidHostInterface::SaveInputProfile(const char *profile_name)
bool AndroidHostInterface::SaveInputProfile(const char* profile_name)
{
const std::string path(GetSavePathForInputProfile(profile_name));
if (path.empty())
@ -838,13 +837,15 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
// Create global reference so it doesn't get cleaned up.
JNIEnv* env = AndroidHelpers::GetJNIEnv();
jclass string_class, host_interface_class, patch_code_class;
jclass string_class, host_interface_class, patch_code_class, game_list_entry_class;
if ((string_class = env->FindClass("java/lang/String")) == nullptr ||
(s_String_class = static_cast<jclass>(env->NewGlobalRef(string_class))) == nullptr ||
(host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr ||
(s_AndroidHostInterface_class = static_cast<jclass>(env->NewGlobalRef(host_interface_class))) == nullptr ||
(patch_code_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr ||
(s_PatchCode_class = static_cast<jclass>(env->NewGlobalRef(patch_code_class))) == nullptr)
(s_PatchCode_class = static_cast<jclass>(env->NewGlobalRef(patch_code_class))) == nullptr ||
(game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr ||
(s_GameListEntry_class = static_cast<jclass>(env->NewGlobalRef(game_list_entry_class))) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface class lookup failed");
return -1;
@ -853,6 +854,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->DeleteLocalRef(string_class);
env->DeleteLocalRef(host_interface_class);
env->DeleteLocalRef(patch_code_class);
env->DeleteLocalRef(game_list_entry_class);
jclass emulation_activity_class;
if ((s_AndroidHostInterface_constructor =
@ -879,7 +881,11 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->GetMethodID(s_EmulationActivity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr ||
(s_EmulationActivity_method_setVibration = env->GetMethodID(emulation_activity_class, "setVibration", "(Z)V")) ==
nullptr ||
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr)
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr ||
(s_GameListEntry_constructor = env->GetMethodID(
s_GameListEntry_class, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/"
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface lookups failed");
return -1;
@ -1026,10 +1032,11 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject
return code.value_or(-1);
}
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused, jstring controller_type)
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused,
jstring controller_type)
{
std::optional<ControllerType> type =
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
if (!type)
return nullptr;
@ -1050,10 +1057,11 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNam
return name_array;
}
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused, jstring controller_type)
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused,
jstring controller_type)
{
std::optional<ControllerType> type =
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
if (!type)
return nullptr;
@ -1074,12 +1082,14 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames
return name_array;
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index, jint button_index, jboolean pressed)
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index,
jint button_index, jboolean pressed)
{
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(controller_index, button_index, pressed);
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index, jint axis_index, jfloat value)
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index,
jint axis_index, jfloat value)
{
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(controller_index, axis_index, value);
}
@ -1115,69 +1125,116 @@ static const char* DiscRegionToString(DiscRegion region)
return names[static_cast<int>(region)];
}
static jobject CreateGameListEntry(JNIEnv* env, AndroidHostInterface* hi, const GameListEntry& entry)
{
const Timestamp modified_ts(
Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time)));
const std::string file_title_str(System::GetTitleForPath(entry.path.c_str()));
const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry));
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 file_title = env->NewStringUTF(file_title_str.c_str());
jstring region = env->NewStringUTF(DiscRegionToString(entry.region));
jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type));
jstring compatibility_rating =
env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating));
jstring cover_path = (cover_path_str.empty()) ? nullptr : env->NewStringUTF(cover_path_str.c_str());
jstring modified_time = env->NewStringUTF(modified_ts.ToString("%Y/%m/%d, %H:%M:%S"));
jlong size = entry.total_size;
jobject entry_jobject =
env->NewObject(s_GameListEntry_class, s_GameListEntry_constructor, path, code, title, file_title, size,
modified_time, region, type, compatibility_rating, cover_path);
env->DeleteLocalRef(modified_time);
if (cover_path)
env->DeleteLocalRef(cover_path);
env->DeleteLocalRef(compatibility_rating);
env->DeleteLocalRef(type);
env->DeleteLocalRef(region);
env->DeleteLocalRef(file_title);
env->DeleteLocalRef(title);
env->DeleteLocalRef(code);
env->DeleteLocalRef(path);
return entry_jobject;
}
DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj)
{
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry");
Assert(entry_class != nullptr);
jmethodID entry_constructor =
env->GetMethodID(entry_class, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/"
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
Assert(entry_constructor != nullptr);
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), entry_class, nullptr);
jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), s_GameListEntry_class, nullptr);
Assert(entry_array != nullptr);
u32 counter = 0;
for (const GameListEntry& entry : hi->GetGameList()->GetEntries())
{
const Timestamp modified_ts(
Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time)));
const std::string file_title_str(System::GetTitleForPath(entry.path.c_str()));
const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry));
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 file_title = env->NewStringUTF(file_title_str.c_str());
jstring region = env->NewStringUTF(DiscRegionToString(entry.region));
jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type));
jstring compatibility_rating =
env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating));
jstring cover_path = (cover_path_str.empty()) ? nullptr : env->NewStringUTF(cover_path_str.c_str());
jstring modified_time = env->NewStringUTF(modified_ts.ToString("%Y/%m/%d, %H:%M:%S"));
jlong size = entry.total_size;
jobject entry_jobject = env->NewObject(entry_class, entry_constructor, path, code, title, file_title, size,
modified_time, region, type, compatibility_rating, cover_path);
jobject entry_jobject = CreateGameListEntry(env, hi, entry);
env->SetObjectArrayElement(entry_array, counter++, entry_jobject);
env->DeleteLocalRef(entry_jobject);
env->DeleteLocalRef(modified_time);
if (cover_path)
env->DeleteLocalRef(cover_path);
env->DeleteLocalRef(compatibility_rating);
env->DeleteLocalRef(type);
env->DeleteLocalRef(region);
env->DeleteLocalRef(file_title);
env->DeleteLocalRef(title);
env->DeleteLocalRef(code);
env->DeleteLocalRef(path);
}
return entry_array;
}
DEFINE_JNI_ARGS_METHOD(jobjectArray , AndroidHostInterface_getHotkeyInfoList, jobject obj)
DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getGameListEntry, jobject obj, jstring path)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
const std::string path_str(AndroidHelpers::JStringToString(env, path));
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
if (!entry)
return nullptr;
return CreateGameListEntry(env, hi, *entry);
}
DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getGameSettingValue, jobject obj, jstring path, jstring key)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
const std::string path_str(AndroidHelpers::JStringToString(env, path));
const std::string key_str(AndroidHelpers::JStringToString(env, key));
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
if (!entry)
return nullptr;
std::optional<std::string> value = entry->settings.GetValueForKey(key_str);
if (!value.has_value())
return nullptr;
else
return env->NewStringUTF(value->c_str());
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setGameSettingValue, jobject obj, jstring path, jstring key,
jstring value)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
const std::string path_str(AndroidHelpers::JStringToString(env, path));
const std::string key_str(AndroidHelpers::JStringToString(env, key));
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
if (!entry)
return;
GameSettings::Entry new_entry(entry->settings);
std::optional<std::string> value_str;
if (value)
value_str = AndroidHelpers::JStringToString(env, value);
new_entry.SetValueForKey(key_str, value_str);
hi->GetGameList()->UpdateGameSettings(path_str, entry->code, entry->title, new_entry, true);
}
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getHotkeyInfoList, jobject obj)
{
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/HotkeyInfo");
Assert(entry_class != nullptr);
jmethodID entry_constructor =
env->GetMethodID(entry_class, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
env->GetMethodID(entry_class, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
Assert(entry_constructor != nullptr);
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);

View File

@ -43,6 +43,15 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="com.github.stenzek.duckstation.MainActivity" />
</activity>
<activity
android:name=".GamePropertiesActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/activity_game_properties"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.github.stenzek.duckstation.MainActivity" />
</activity>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize"

View File

@ -87,6 +87,11 @@ public class AndroidHostInterface {
public native GameListEntry[] getGameListEntries();
public native GameListEntry getGameListEntry(String path);
public native String getGameSettingValue(String path, String key);
public native void setGameSettingValue(String path, String key, String value);
public native void resetSystem();
public native void loadState(boolean global, int slot);

View File

@ -0,0 +1,245 @@
package com.github.stenzek.duckstation;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Property;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.ListFragment;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
public class GamePropertiesActivity extends AppCompatActivity {
PropertyListAdapter mPropertiesListAdapter;
GameListEntry mGameListEntry;
public ListAdapter getPropertyListAdapter() {
if (mPropertiesListAdapter != null)
return mPropertiesListAdapter;
mPropertiesListAdapter = new PropertyListAdapter(this);
mPropertiesListAdapter.addItem("title", "Title", mGameListEntry.getTitle());
mPropertiesListAdapter.addItem("filetitle", "File Title", mGameListEntry.getFileTitle());
mPropertiesListAdapter.addItem("serial", "Serial", mGameListEntry.getCode());
mPropertiesListAdapter.addItem("type", "Type", mGameListEntry.getType().toString());
mPropertiesListAdapter.addItem("path", "Path", mGameListEntry.getPath());
mPropertiesListAdapter.addItem("region", "Region", mGameListEntry.getRegion().toString());
mPropertiesListAdapter.addItem("compatibility", "Compatibility Rating", mGameListEntry.getCompatibilityRating().toString());
return mPropertiesListAdapter;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String path = getIntent().getStringExtra("path");
if (path == null || path.isEmpty()) {
finish();
return;
}
mGameListEntry = AndroidHostInterface.getInstance().getGameListEntry(path);
if (mGameListEntry == null) {
finish();
return;
}
setContentView(R.layout.settings_activity);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsCollectionFragment(this))
.commit();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
setTitle(mGameListEntry.getTitle());
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void displayError(String text) {
new AlertDialog.Builder(this)
.setTitle(R.string.emulation_activity_error)
.setMessage(text)
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
.create()
.show();
}
private void createBooleanGameSetting(PreferenceScreen ps, String key, int titleId) {
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId);
ps.addPreference(pref);
}
private void createListGameSetting(PreferenceScreen ps, String key, int titleId, int entryId, int entryValuesId) {
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId, entryId, entryValuesId);
ps.addPreference(pref);
}
public static class GameSettingsFragment extends PreferenceFragmentCompat {
private GamePropertiesActivity activity;
public GameSettingsFragment(GamePropertiesActivity activity) {
this.activity = activity;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
activity.createListGameSetting(ps, "CPUOverclock", R.string.settings_cpu_overclocking, R.array.settings_advanced_cpu_overclock_entries, R.array.settings_advanced_cpu_overclock_values);
activity.createListGameSetting(ps,"CDROMReadSpeedup", R.string.settings_cdrom_read_speedup, R.array.settings_cdrom_read_speedup_entries, R.array.settings_cdrom_read_speedup_values);
activity.createListGameSetting(ps, "DisplayAspectRatio", R.string.settings_aspect_ratio, R.array.settings_display_aspect_ratio_names, R.array.settings_display_aspect_ratio_values);
activity.createListGameSetting(ps, "DisplayCropMode", R.string.settings_crop_mode,R.array.settings_display_crop_mode_entries,R.array.settings_display_crop_mode_values);
activity.createListGameSetting(ps,"GPUDownsampleMode", R.string.settings_downsample_mode, R.array.settings_downsample_mode_entries, R.array.settings_downsample_mode_values);
activity.createBooleanGameSetting(ps, "DisplayLinearUpscaling", R.string.settings_linear_upscaling);
activity.createBooleanGameSetting(ps,"DisplayIntegerUpscaling",R.string.settings_integer_upscaling);
activity.createBooleanGameSetting(ps,"DisplayForce4_3For24Bit",R.string.settings_force_4_3_for_24bit);
activity.createListGameSetting(ps, "GPUResolutionScale", R.string.settings_gpu_resolution_scale, R.array.settings_gpu_resolution_scale_entries, R.array.settings_gpu_resolution_scale_values);
activity.createListGameSetting(ps, "GPUMSAA", R.string.settings_msaa, R.array.settings_gpu_msaa_entries, R.array.settings_gpu_msaa_values);
activity.createBooleanGameSetting(ps, "GPUTrueColor", R.string.settings_true_color);
activity.createBooleanGameSetting(ps,"GPUScaledDithering",R.string.settings_scaled_dithering);
activity.createListGameSetting(ps, "GPUTextureFilter", R.string.settings_texture_filtering, R.array.settings_gpu_texture_filter_names, R.array.settings_gpu_texture_filter_values);
activity.createBooleanGameSetting(ps,"GPUForceNTSCTimings",R.string.settings_force_ntsc_timings);
activity.createBooleanGameSetting(ps, "GPUWidescreenHack", R.string.settings_widescreen_hack);
activity.createBooleanGameSetting(ps, "GPUPGXP", R.string.settings_pgxp_geometry_correction);
activity.createBooleanGameSetting(ps, "GPUPGXPDepthBuffer", R.string.settings_pgxp_depth_buffer);
setPreferenceScreen(ps);
}
}
public static class ControllerSettingsFragment extends PreferenceFragmentCompat {
private GamePropertiesActivity activity;
public ControllerSettingsFragment(GamePropertiesActivity activity) {
this.activity = activity;
}
private void createInputProfileSetting(PreferenceScreen ps) {
final GameSettingPreference pref = new GameSettingPreference(ps.getContext(), activity.mGameListEntry.getPath(), "InputProfileName", R.string.settings_input_profile);
final String[] inputProfileNames = AndroidHostInterface.getInstance().getInputProfileNames();
pref.setEntries(inputProfileNames);
pref.setEntryValues(inputProfileNames);
ps.addPreference(pref);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
activity.createListGameSetting(ps, "Controller1Type", R.string.settings_controller_type, R.array.settings_controller_type_entries, R.array.settings_controller_type_values);
activity.createListGameSetting(ps, "MemoryCard1Type", R.string.settings_memory_card_1_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
activity.createListGameSetting(ps, "MemoryCard2Type", R.string.settings_memory_card_2_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
createInputProfileSetting(ps);
setPreferenceScreen(ps);
}
}
public static class SettingsCollectionFragment extends Fragment {
private GamePropertiesActivity activity;
private SettingsCollectionAdapter adapter;
private ViewPager2 viewPager;
public SettingsCollectionFragment(GamePropertiesActivity activity) {
this.activity = activity;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
adapter = new SettingsCollectionAdapter(activity, this);
viewPager = view.findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
switch (position)
{
case 0: tab.setText("Summary"); break;
case 1: tab.setText("Game Settings"); break;
case 2: tab.setText("Controller Settings"); break;
}
}).attach();
}
}
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
private GamePropertiesActivity activity;
public SettingsCollectionAdapter(@NonNull GamePropertiesActivity activity, @NonNull Fragment fragment) {
super(fragment);
this.activity = activity;
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position)
{
case 0: { // Summary
ListFragment lf = new ListFragment();
lf.setListAdapter(activity.getPropertyListAdapter());
return lf;
}
case 1: { // Game Settings
return new GameSettingsFragment(activity);
}
case 2: { // Controller Settings
return new ControllerSettingsFragment(activity);
}
// TODO: Memory Card Editor
default:
return null;
}
}
@Override
public int getItemCount() {
return 3;
}
}
}

View File

@ -0,0 +1,83 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.ListPreference;
public class GameSettingPreference extends ListPreference {
private String mGamePath;
/**
* Creates a boolean game property preference.
*/
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId) {
super(context);
mGamePath = gamePath;
setPersistent(false);
setTitle(titleId);
setKey(settingKey);
setIconSpaceReserved(false);
setSummaryProvider(SimpleSummaryProvider.getInstance());
setEntries(R.array.settings_boolean_entries);
setEntryValues(R.array.settings_boolean_values);
updateValue();
}
/**
* Creates a list game property preference.
*/
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId, int entryArray, int entryValuesArray) {
super(context);
mGamePath = gamePath;
setPersistent(false);
setTitle(titleId);
setKey(settingKey);
setIconSpaceReserved(false);
setSummaryProvider(SimpleSummaryProvider.getInstance());
setEntries(entryArray);
setEntryValues(entryValuesArray);
updateValue();
}
private void updateValue() {
final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey());
if (value == null)
super.setValue("null");
else
super.setValue(value);
}
@Override
public void setValue(String value) {
super.setValue(value);
if (value.equals("null"))
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), null);
else
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), value);
}
@Override
public void setEntries(CharSequence[] entries) {
final int length = (entries != null) ? entries.length : 0;
CharSequence[] newEntries = new CharSequence[length + 1];
newEntries[0] = getContext().getString(R.string.game_properties_preference_use_global_setting);
if (entries != null)
System.arraycopy(entries, 0, newEntries, 1, entries.length);
super.setEntries(newEntries);
}
@Override
public void setEntryValues(CharSequence[] entryValues) {
final int length = (entryValues != null) ? entryValues.length : 0;
CharSequence[] newEntryValues = new CharSequence[length + 1];
newEntryValues[0] = "null";
if (entryValues != null)
System.arraycopy(entryValues, 0, newEntryValues, 1, length);
super.setEntryValues(newEntryValues);
}
}

View File

@ -151,6 +151,9 @@ public class MainActivity extends AppCompatActivity {
} else if (id == R.id.game_list_entry_menu_resume_game) {
startEmulation(mGameList.getEntry(position).getPath(), true);
return true;
} else if (id == R.id.game_list_entry_menu_properties) {
openGameProperties(mGameList.getEntry(position).getPath());
return true;
}
return false;
}
@ -356,6 +359,13 @@ public class MainActivity extends AppCompatActivity {
}
}
private boolean openGameProperties(String path) {
Intent intent = new Intent(this, GamePropertiesActivity.class);
intent.putExtra("path", path);
startActivity(intent);
return true;
}
private boolean startEmulation(String bootPath, boolean resumeState) {
if (!doBIOSCheck())
return false;

View File

@ -0,0 +1,84 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
public class PropertyListAdapter extends BaseAdapter {
private class Item {
public String key;
public String title;
public String value;
Item(String key, String title, String value) {
this.key = key;
this.title = title;
this.value = value;
}
}
private Context mContext;
private ArrayList<Item> mItems = new ArrayList<>();
public PropertyListAdapter(Context context) {
mContext = context;
}
public Item getItemByKey(String key) {
for (Item it : mItems) {
if (it.key.equals(key))
return it;
}
return null;
}
public int addItem(String key, String title, String value) {
if (getItemByKey(key) != null)
return -1;
Item it = new Item(key, title, value);
int position = mItems.size();
mItems.add(it);
return position;
}
public boolean removeItem(Item item) {
return mItems.remove(item);
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public Object getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.layout_game_property_entry, parent, false);
}
TextView titleView = (TextView)convertView.findViewById(R.id.property_title);
TextView valueView = (TextView)convertView.findViewById(R.id.property_value);
Item prop = mItems.get(position);
titleView.setText(prop.title);
valueView.setText(prop.value);
return convertView;
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/property_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="10dp"
android:layout_alignParentTop="true"
android:text="TextView"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/property_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_below="@id/property_title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="TextView" />
</RelativeLayout>

View File

@ -7,4 +7,7 @@
<item
android:id="@+id/game_list_entry_menu_resume_game"
android:title="@string/menu_game_list_entry_resume_game" />
<item
android:id="@+id/game_list_entry_menu_properties"
android:title="Game Properties" />
</menu>

View File

@ -460,4 +460,12 @@
<item>Box</item>
<item>Adaptive</item>
</string-array>
<string-array name="settings_boolean_entries">
<item>Disabled</item>
<item>Enabled</item>
</string-array>
<string-array name="settings_boolean_values">
<item>false</item>
<item>true</item>
</string-array>
</resources>

View File

@ -183,4 +183,7 @@
<string name="settings_disable_all_enhancements">Disable All Enhancements</string>
<string name="settings_summary_disable_all_enhancements">Temporarily disables all enhancements, which can be useful when debugging issues.</string>
<string name="settings_downsample_mode">Downsampling</string>
<string name="activity_game_properties">Game Properties</string>
<string name="game_properties_preference_use_global_setting">Use Global Setting</string>
<string name="settings_input_profile">Input Profile</string>
</resources>

View File

@ -86,7 +86,7 @@ public:
virtual void DestroySystem() override;
/// Returns the game list.
ALWAYS_INLINE const GameList* GetGameList() const { return m_game_list.get(); }
ALWAYS_INLINE GameList* GetGameList() const { return m_game_list.get(); }
/// Returns a list of all available hotkeys.
ALWAYS_INLINE const HotkeyInfoList& GetHotkeyInfoList() const { return m_hotkeys; }