Android: Add game directory list editor

This commit is contained in:
Connor McLaughlin 2021-01-24 16:43:53 +10:00
parent 6a122623fa
commit 59810bf8db
12 changed files with 447 additions and 37 deletions

View File

@ -14,13 +14,17 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".GameDirectoriesActivity"
android:label="@string/title_activity_game_directories"
android:theme="@style/AppTheme.NoActionBar"></activity>
<activity
android:name=".EmulationActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:immersive="true"
android:label="@string/title_activity_emulation"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:immersive="true">
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.github.stenzek.duckstation.MainActivity" />

View File

@ -677,6 +677,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
}
private boolean mSustainedPerformanceModeEnabled = false;
private void updateSustainedPerformanceMode() {
final boolean enabled = getBooleanSetting("Main/SustainedPerformanceMode", false);
if (mSustainedPerformanceModeEnabled == enabled)

View File

@ -0,0 +1,279 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
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.BaseAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.TextView;
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.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.ListFragment;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
public class GameDirectoriesActivity extends AppCompatActivity {
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 1;
private class DirectoryListAdapter extends RecyclerView.Adapter {
private class Entry {
private String mPath;
private boolean mRecursive;
public Entry(String path, boolean recursive) {
mPath = path;
mRecursive = recursive;
}
public String getPath() {
return mPath;
}
public boolean isRecursive() {
return mRecursive;
}
public void toggleRecursive() {
mRecursive = !mRecursive;
}
}
private class EntryComparator implements Comparator<Entry> {
@Override
public int compare(Entry left, Entry right) {
return left.getPath().compareTo(right.getPath());
}
}
private Context mContext;
private Entry[] mEntries;
public DirectoryListAdapter(Context context) {
mContext = context;
reload();
}
public void reload() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
ArrayList<Entry> entries = new ArrayList<>();
try {
Set<String> paths = prefs.getStringSet("GameList/Paths", null);
if (paths != null) {
for (String path : paths)
entries.add(new Entry(path, false));
}
} catch (Exception e) {
}
try {
Set<String> paths = prefs.getStringSet("GameList/RecursivePaths", null);
if (paths != null) {
for (String path : paths)
entries.add(new Entry(path, true));
}
} catch (Exception e) {
}
mEntries = new Entry[entries.size()];
entries.toArray(mEntries);
Arrays.sort(mEntries, new EntryComparator());
notifyDataSetChanged();
}
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private int mPosition;
private Entry mEntry;
private TextView mPathView;
private TextView mRecursiveView;
private ImageButton mToggleRecursiveView;
private ImageButton mRemoveView;
public ViewHolder(View rootView) {
super(rootView);
mPathView = rootView.findViewById(R.id.path);
mRecursiveView = rootView.findViewById(R.id.recursive);
mToggleRecursiveView = rootView.findViewById(R.id.toggle_recursive);
mToggleRecursiveView.setOnClickListener(this);
mRemoveView = rootView.findViewById(R.id.remove);
mRemoveView.setOnClickListener(this);
}
public void bindData(int position, Entry entry) {
mPosition = position;
mEntry = entry;
updateText();
}
private void updateText() {
mPathView.setText(mEntry.getPath());
mRecursiveView.setText(getString(mEntry.isRecursive() ? R.string.game_directories_scanning_subdirectories : R.string.game_directories_not_scanning_subdirectories));
mToggleRecursiveView.setImageDrawable(getDrawable(mEntry.isRecursive() ? R.drawable.ic_baseline_folder_24 : R.drawable.ic_baseline_folder_open_24));
}
@Override
public void onClick(View v) {
if (mToggleRecursiveView == v) {
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
mEntry.toggleRecursive();
addSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
updateText();
} else if (mRemoveView == v) {
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
reload();
}
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
((ViewHolder) holder).bindData(position, mEntries[position]);
}
@Override
public int getItemViewType(int position) {
return R.layout.layout_game_directory_entry;
}
@Override
public long getItemId(int position) {
return mEntries[position].getPath().hashCode();
}
@Override
public int getItemCount() {
return mEntries.length;
}
}
DirectoryListAdapter mDirectoryListAdapter;
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game_directories);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
mDirectoryListAdapter = new DirectoryListAdapter(this);
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(mDirectoryListAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
findViewById(R.id.fab).setOnClickListener((v) -> startAddGameDirectory());
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public static String getPathFromTreeUri(Context context, Uri treeUri) {
String path = FileUtil.getFullPathFromTreeUri(treeUri, context);
if (path.length() < 5) {
new AlertDialog.Builder(context)
.setTitle(R.string.main_activity_error)
.setMessage(R.string.main_activity_get_path_from_directory_error)
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
})
.create()
.show();
return null;
}
return path;
}
public static void addSearchDirectory(Context context, String path, boolean recursive) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
PreferenceHelpers.addToStringList(prefs, key, path);
Log.i("GameDirectoriesActivity", "Added path '" + path + "' to game list search directories");
}
public static void removeSearchDirectory(Context context, String path, boolean recursive) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
PreferenceHelpers.removeFromStringList(prefs, key, path);
Log.i("GameDirectoriesActivity", "Removed path '" + path + "' from game list search directories");
}
private void startAddGameDirectory() {
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
i.addCategory(Intent.CATEGORY_DEFAULT);
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
if (resultCode != RESULT_OK)
return;
String path = getPathFromTreeUri(this, data.getData());
if (path == null)
return;
addSearchDirectory(this, path, true);
mDirectoryListAdapter.reload();
}
break;
}
}
}

View File

@ -43,6 +43,7 @@ public class MainActivity extends AppCompatActivity {
private static final int REQUEST_IMPORT_BIOS_IMAGE = 3;
private static final int REQUEST_START_FILE = 4;
private static final int REQUEST_SETTINGS = 5;
private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6;
private GameList mGameList;
private ListView mGameListView;
@ -209,8 +210,10 @@ public class MainActivity extends AppCompatActivity {
startEmulation(null, false);
} else if (id == R.id.action_start_file) {
startStartFile();
} else if (id == R.id.action_add_game_directory) {
startAddGameDirectory();
} else if (id == R.id.action_edit_game_directories) {
Intent intent = new Intent(this, GameDirectoriesActivity.class);
startActivityForResult(intent, REQUEST_EDIT_GAME_DIRECTORIES);
return true;
} else if (id == R.id.action_scan_for_new_games) {
mGameList.refresh(false, false, this);
} else if (id == R.id.action_rescan_all_games) {
@ -255,22 +258,6 @@ public class MainActivity extends AppCompatActivity {
return path;
}
private String getPathFromTreeUri(Uri treeUri) {
String path = FileUtil.getFullPathFromTreeUri(treeUri, this);
if (path.length() < 5) {
new AlertDialog.Builder(this)
.setTitle(R.string.main_activity_error)
.setMessage(R.string.main_activity_get_path_from_directory_error)
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
})
.create()
.show();
return null;
}
return path;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -280,20 +267,11 @@ public class MainActivity extends AppCompatActivity {
if (resultCode != RESULT_OK)
return;
String path = getPathFromTreeUri(data.getData());
String path = GameDirectoriesActivity.getPathFromTreeUri(this, data.getData());
if (path == null)
return;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Set<String> currentValues = prefs.getStringSet("GameList/RecursivePaths", null);
if (currentValues == null)
currentValues = new HashSet<String>();
currentValues.add(path);
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet("GameList/RecursivePaths", currentValues);
editor.apply();
Log.i("MainActivity", "Added path '" + path + "' to game list search directories");
GameDirectoriesActivity.addSearchDirectory(this, path, true);
mGameList.refresh(false, false, this);
}
break;
@ -322,6 +300,11 @@ public class MainActivity extends AppCompatActivity {
loadSettings();
}
break;
case REQUEST_EDIT_GAME_DIRECTORIES: {
mGameList.refresh(false, false, this);
}
break;
}
}

View File

@ -46,8 +46,14 @@ public class PreferenceHelpers {
public static boolean addToStringList(SharedPreferences prefs, String keyName, String valueToAdd) {
Set<String> values = getStringSet(prefs, keyName);
if (values == null)
if (values == null) {
values = new ArraySet<>();
} else {
// We need to copy it otherwise the put doesn't save.
Set<String> valuesCopy = new ArraySet<>();
valuesCopy.addAll(values);
values = valuesCopy;
}
final boolean result = values.add(valueToAdd);
prefs.edit().putStringSet(keyName, values).commit();
@ -59,6 +65,11 @@ public class PreferenceHelpers {
if (values == null)
return false;
// We need to copy it otherwise the put doesn't save.
Set<String> valuesCopy = new ArraySet<>();
valuesCopy.addAll(values);
values = valuesCopy;
final boolean result = values.remove(valueToRemove);
prefs.edit().putStringSet(keyName, values).commit();
return result;

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
</vector>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".GameDirectoriesActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="@layout/activity_main">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@color/fab_background"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="10dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:text="TextView"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/recursive"
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/path"
android:layout_alignParentStart="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="TextView" />
<ImageButton
android:id="@+id/remove"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginTop="15dp"
android:layout_marginEnd="15dp"
android:background="?android:selectableItemBackground"
app:srcCompat="@drawable/ic_baseline_delete_24" />
<ImageButton
android:id="@+id/toggle_recursive"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentTop="true"
android:layout_marginTop="15dp"
android:layout_marginEnd="15dp"
android:layout_toStartOf="@id/remove"
android:background="?android:selectableItemBackground"
app:srcCompat="@drawable/ic_baseline_folder_24" />
</RelativeLayout>

View File

@ -15,8 +15,8 @@
</group>
<group android:id="@+id/game_list">
<item
android:id="@+id/action_add_game_directory"
android:title="@string/menu_main_add_game_directory" />
android:id="@+id/action_edit_game_directories"
android:title="@string/menu_main_edit_game_directories" />
<item
android:id="@+id/action_scan_for_new_games"
android:title="@string/menu_main_scan_for_new_games" />

View File

@ -112,7 +112,7 @@
<string name="menu_main_resume_last_session">Resume Last Session</string>
<string name="menu_main_start_file">Start File</string>
<string name="menu_main_start_bios">Start BIOS</string>
<string name="menu_main_add_game_directory">Add Game Directory</string>
<string name="menu_main_edit_game_directories">Edit Game Directories</string>
<string name="menu_main_scan_for_new_games">Scan For New Games</string>
<string name="menu_main_rescan_all_games">Rescan All Games</string>
<string name="menu_main_import_bios">Import BIOS</string>
@ -195,4 +195,9 @@
<string name="settings_summary_general_sync_to_host_refresh_rate">Adjusts the emulation speed so the console\'s refresh rate matches the host\'s refresh rate, when VSync and Audio Resampling is enabled. This results in the smoothest animations possible, at the cost of potentially increasing the emulation speed by less than 1%.</string>
<string name="settings_sustained_performance_mode">Sustained Performance Mode</string>
<string name="settings_summary_sustained_performance_mode">Enables Android\'s sustained performance mode. May result in more consistent framerates for long sessions on some devices.</string>
<string name="title_activity_game_directories">Edit Game Directories</string>
<string name="settings_game_directories">Game Directories</string>
<string name="settings_summary_game_directories">Change the list of directories used to search for games.</string>
<string name="game_directories_scanning_subdirectories">Scanning subdirectories.</string>
<string name="game_directories_not_scanning_subdirectories">Not scanning subdirectories.</string>
</resources>

View File

@ -14,8 +14,17 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
app:title="@string/settings_game_directories"
app:summary="@string/settings_summary_game_directories"
app:iconSpaceReserved="false">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.github.stenzek.duckstation.GameDirectoriesActivity"
android:targetPackage="com.github.stenzek.duckstation" />
</Preference>
<ListPreference
app:key="Main/EmulationSpeed"
app:title="@string/settings_emulation_speed"