diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6239b5e30..16910b175 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,13 +14,17 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
+
+ android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
index d84cc3d49..53e983205 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
@@ -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)
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java
new file mode 100644
index 000000000..ee975a09f
--- /dev/null
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java
@@ -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 {
+ @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 entries = new ArrayList<>();
+
+ try {
+ Set paths = prefs.getStringSet("GameList/Paths", null);
+ if (paths != null) {
+ for (String path : paths)
+ entries.add(new Entry(path, false));
+ }
+ } catch (Exception e) {
+ }
+
+ try {
+ Set 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
index 335045e12..3e763ce57 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java
@@ -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 currentValues = prefs.getStringSet("GameList/RecursivePaths", null);
- if (currentValues == null)
- currentValues = new HashSet();
-
- 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;
}
}
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java b/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java
index a76b984b0..99e9ad815 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/PreferenceHelpers.java
@@ -46,8 +46,14 @@ public class PreferenceHelpers {
public static boolean addToStringList(SharedPreferences prefs, String keyName, String valueToAdd) {
Set 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 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 valuesCopy = new ArraySet<>();
+ valuesCopy.addAll(values);
+ values = valuesCopy;
+
final boolean result = values.remove(valueToRemove);
prefs.edit().putStringSet(keyName, values).commit();
return result;
diff --git a/android/app/src/main/res/drawable/ic_baseline_delete_24.xml b/android/app/src/main/res/drawable/ic_baseline_delete_24.xml
new file mode 100644
index 000000000..2c0afcc56
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_baseline_delete_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_baseline_folder_24.xml b/android/app/src/main/res/drawable/ic_baseline_folder_24.xml
new file mode 100644
index 000000000..bbfe9c931
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_baseline_folder_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/layout/activity_game_directories.xml b/android/app/src/main/res/layout/activity_game_directories.xml
new file mode 100644
index 000000000..54bfc2181
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_game_directories.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/layout_game_directory_entry.xml b/android/app/src/main/res/layout/layout_game_directory_entry.xml
new file mode 100644
index 000000000..e09713887
--- /dev/null
+++ b/android/app/src/main/res/layout/layout_game_directory_entry.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml
index bb9bc871b..5ea2f13b9 100644
--- a/android/app/src/main/res/menu/menu_main.xml
+++ b/android/app/src/main/res/menu/menu_main.xml
@@ -15,8 +15,8 @@
+ android:id="@+id/action_edit_game_directories"
+ android:title="@string/menu_main_edit_game_directories" />
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 8f18a66fc..ea00af480 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -112,7 +112,7 @@
Resume Last Session
Start File
Start BIOS
- Add Game Directory
+ Edit Game Directories
Scan For New Games
Rescan All Games
Import BIOS
@@ -195,4 +195,9 @@
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%.
Sustained Performance Mode
Enables Android\'s sustained performance mode. May result in more consistent framerates for long sessions on some devices.
+ Edit Game Directories
+ Game Directories
+ Change the list of directories used to search for games.
+ Scanning subdirectories.
+ Not scanning subdirectories.
diff --git a/android/app/src/main/res/xml/general_preferences.xml b/android/app/src/main/res/xml/general_preferences.xml
index da9a051a7..9f4fdd919 100644
--- a/android/app/src/main/res/xml/general_preferences.xml
+++ b/android/app/src/main/res/xml/general_preferences.xml
@@ -14,8 +14,17 @@
~ limitations under the License.
-->
-
-
+
+
+
+