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

@ -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>