Android: Multiple changes

- Fix game list display of NTSC-J region
 - Hook up quick load/save/reset options in emulation view.
 - Add speed limiter toggle to emulation view.
 - Add game list scanning options to main menu.
 - Add resume button (not yet hooked up to save states, it'll start the
   BIOS shell)
This commit is contained in:
Connor McLaughlin 2020-07-27 00:04:14 +10:00
parent 8665a24eee
commit a7e24da7fe
14 changed files with 227 additions and 99 deletions

View file

@ -130,6 +130,7 @@ void AndroidHostInterface::SetUserDirectory()
void AndroidHostInterface::LoadSettings() void AndroidHostInterface::LoadSettings()
{ {
CommonHostInterface::LoadSettings(m_settings_interface); CommonHostInterface::LoadSettings(m_settings_interface);
CommonHostInterface::UpdateInputMap(m_settings_interface);
} }
void AndroidHostInterface::UpdateInputMap() void AndroidHostInterface::UpdateInputMap()
@ -396,6 +397,19 @@ void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code,
false); false);
} }
void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database)
{
m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface);
m_game_list->Refresh(invalidate_cache, invalidate_database);
}
void AndroidHostInterface::ApplySettings()
{
Settings old_settings = std::move(m_settings);
CommonHostInterface::LoadSettings(m_settings_interface);
CheckForSettingsChanges(old_settings);
}
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{ {
Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV); Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV);
@ -530,28 +544,18 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobje
return code.value_or(-1); return code.value_or(-1);
} }
DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_cache_path, jstring j_redump_dat_path, DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, jboolean invalidate_database)
jarray j_search_directories, jboolean search_recursively)
{ {
// const std::string cache_path = AndroidHelpers::JStringToString(env, j_cache_path); AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database);
std::string redump_dat_path = AndroidHelpers::JStringToString(env, j_redump_dat_path); }
// TODO: This should use the base HostInterface. static const char* DiscRegionToString(DiscRegion region) {
GameList gl; static std::array<const char*, 4> names = {{"NTSC_J", "NTSC_U", "PAL", "Other"}};
if (!redump_dat_path.empty()) return names[static_cast<int>(region)];
gl.SetDatabaseFilename(std::move(redump_dat_path)); }
const jsize search_directories_size = env->GetArrayLength(j_search_directories);
for (jsize i = 0; i < search_directories_size; i++)
{
jobject search_dir_obj = env->GetObjectArrayElement(reinterpret_cast<jobjectArray>(j_search_directories), i);
const std::string search_dir = AndroidHelpers::JStringToString(env, reinterpret_cast<jstring>(search_dir_obj));
if (!search_dir.empty())
gl.AddDirectory(search_dir.c_str(), search_recursively);
}
gl.Refresh(false, false, nullptr);
DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj)
{
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry"); jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry");
Assert(entry_class != nullptr); Assert(entry_class != nullptr);
@ -560,11 +564,12 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
Assert(entry_constructor != nullptr); Assert(entry_constructor != nullptr);
jobjectArray entry_array = env->NewObjectArray(gl.GetEntryCount(), entry_class, nullptr); AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), entry_class, nullptr);
Assert(entry_array != nullptr); Assert(entry_array != nullptr);
u32 counter = 0; u32 counter = 0;
for (const GameListEntry& entry : gl.GetEntries()) for (const GameListEntry& entry : hi->GetGameList()->GetEntries())
{ {
const Timestamp modified_ts( const Timestamp modified_ts(
Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time))); Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time)));
@ -572,7 +577,7 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca
jstring path = env->NewStringUTF(entry.path.c_str()); jstring path = env->NewStringUTF(entry.path.c_str());
jstring code = env->NewStringUTF(entry.code.c_str()); jstring code = env->NewStringUTF(entry.code.c_str());
jstring title = env->NewStringUTF(entry.title.c_str()); jstring title = env->NewStringUTF(entry.title.c_str());
jstring region = env->NewStringUTF(Settings::GetDiscRegionName(entry.region)); jstring region = env->NewStringUTF(DiscRegionToString(entry.region));
jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type)); jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type));
jstring compatibility_rating = jstring compatibility_rating =
env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating)); env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating));
@ -587,3 +592,42 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca
return entry_array; return entry_array;
} }
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
if (hi->IsEmulationThreadRunning())
{
hi->RunOnEmulationThread([hi]() {
hi->ApplySettings();
});
}
else
{
hi->ApplySettings();
}
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
hi->RunOnEmulationThread([hi]() {
hi->ResetSystem();
});
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_loadState, jobject obj, jboolean global, jint slot)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
hi->RunOnEmulationThread([hi, global, slot]() {
hi->LoadState(global, slot);
});
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveState, jobject obj, jboolean global, jint slot)
{
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
hi->RunOnEmulationThread([hi, global, slot]() {
hi->SaveState(global, slot);
});
}

View file

@ -44,6 +44,9 @@ public:
void SetControllerType(u32 index, std::string_view type_name); void SetControllerType(u32 index, std::string_view type_name);
void SetControllerButtonState(u32 index, s32 button_code, bool pressed); void SetControllerButtonState(u32 index, s32 button_code, bool pressed);
void RefreshGameList(bool invalidate_cache, bool invalidate_database);
void ApplySettings();
protected: protected:
void SetUserDirectory() override; void SetUserDirectory() override;
void LoadSettings() override; void LoadSettings() override;

View file

@ -141,7 +141,7 @@ void AndroidSettingsInterface::DeleteValue(const char* section, const char* key)
std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* section, const char* key) std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* section, const char* key)
{ {
JNIEnv* env = AndroidHelpers::GetJNIEnv(); JNIEnv* env = AndroidHelpers::GetJNIEnv();
jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, nullptr); jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, env->NewStringUTF(GetSettingKey(section, key)), nullptr);
if (!values_set) if (!values_set)
return {}; return {};

View file

@ -7,10 +7,6 @@ public class AndroidHostInterface
{ {
private long nativePointer; private long nativePointer;
static {
System.loadLibrary("duckstation-native");
}
static public native AndroidHostInterface create(Context context); static public native AndroidHostInterface create(Context context);
public AndroidHostInterface(long nativePointer) public AndroidHostInterface(long nativePointer)
@ -28,4 +24,26 @@ public class AndroidHostInterface
public native void setControllerType(int index, String typeName); public native void setControllerType(int index, String typeName);
public native void setControllerButtonState(int index, int buttonCode, boolean pressed); public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
public static native int getControllerButtonCode(String controllerType, String buttonName); public static native int getControllerButtonCode(String controllerType, String buttonName);
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase);
public native GameListEntry[] getGameListEntries();
public native void resetSystem();
public native void loadState(boolean global, int slot);
public native void saveState(boolean global, int slot);
public native void applySettings();
static {
System.loadLibrary("duckstation-native");
}
static private AndroidHostInterface mInstance;
static public boolean createInstance(Context context) {
mInstance = create(context);
return mInstance != null;
}
static public AndroidHostInterface getInstance() {
return mInstance;
}
} }

View file

@ -0,0 +1,8 @@
package com.github.stenzek.duckstation;
public enum DiscRegion {
NTSC_J,
NTSC_U,
PAL,
Other
}

View file

@ -6,6 +6,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
@ -18,6 +19,7 @@ import android.view.MenuItem;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
/** /**
* An example full-screen activity that shows and hides the system UI (i.e. * An example full-screen activity that shows and hides the system UI (i.e.
@ -25,9 +27,17 @@ import androidx.core.app.NavUtils;
*/ */
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback { public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
/** /**
* Interface to the native emulator core * Settings interfaces.
*/ */
AndroidHostInterface mHostInterface; SharedPreferences mPreferences;
private boolean getBooleanSetting(String key, boolean defaultValue) {
return mPreferences.getBoolean(key, defaultValue);
}
private void setBooleanSetting(String key, boolean value) {
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(key, value);
editor.apply();
}
/** /**
* Touchscreen controller overlay * Touchscreen controller overlay
@ -96,15 +106,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Once we get a surface, we can boot. // Once we get a surface, we can boot.
if (mHostInterface.isEmulationThreadRunning()) { if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
mHostInterface.surfaceChanged(holder.getSurface(), format, width, height); AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height);
return; return;
} }
String bootPath = getIntent().getStringExtra("bootPath"); String bootPath = getIntent().getStringExtra("bootPath");
String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath"); String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath");
boolean resumeState = getIntent().getBooleanExtra("resumeState", false);
if (!mHostInterface if (!AndroidHostInterface.getInstance()
.startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) { .startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) {
Log.e("EmulationActivity", "Failed to start emulation thread"); Log.e("EmulationActivity", "Failed to start emulation thread");
finishActivity(0); finishActivity(0);
@ -114,16 +125,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
if (!mHostInterface.isEmulationThreadRunning()) if (!AndroidHostInterface.getInstance().isEmulationThreadRunning())
return; return;
Log.i("EmulationActivity", "Stopping emulation thread"); Log.i("EmulationActivity", "Stopping emulation thread");
mHostInterface.stopEmulationThread(); AndroidHostInterface.getInstance().stopEmulationThread();
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
setContentView(R.layout.activity_emulation); setContentView(R.layout.activity_emulation);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
@ -142,19 +154,15 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
} }
}); });
mHostInterface = AndroidHostInterface.create(this);
if (mHostInterface == null)
throw new InstantiationError("Failed to create host interface");
// Create touchscreen controller. // Create touchscreen controller.
FrameLayout activityLayout = findViewById(R.id.frameLayout); FrameLayout activityLayout = findViewById(R.id.frameLayout);
mTouchscreenController = new TouchscreenControllerView(this); mTouchscreenController = new TouchscreenControllerView(this);
activityLayout.addView(mTouchscreenController); activityLayout.addView(mTouchscreenController);
mTouchscreenController.init(0, "DigitalController", mHostInterface); mTouchscreenController.init(0, "DigitalController", AndroidHostInterface.getInstance());
setTouchscreenControllerVisibility(true); setTouchscreenControllerVisibility(true);
// Hook up controller input. // Hook up controller input.
mContentView.initControllerKeyMapping(mHostInterface, "DigitalController"); mContentView.initControllerKeyMapping("DigitalController");
} }
@Override @Override
@ -172,6 +180,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
// Inflate the menu; this adds items to the action bar if it is present. // Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_emulation, menu); getMenuInflater().inflate(R.menu.menu_emulation, menu);
menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible); menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible);
menu.findItem(R.id.enable_speed_limiter).setChecked(getBooleanSetting("Main/SpeedLimiterEnabled", true));
return true; return true;
} }
@ -191,6 +200,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
setTouchscreenControllerVisibility(!mTouchscreenControllerVisible); setTouchscreenControllerVisibility(!mTouchscreenControllerVisible);
item.setChecked(mTouchscreenControllerVisible); item.setChecked(mTouchscreenControllerVisible);
return true; return true;
} else if (id == R.id.enable_speed_limiter) {
boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true);
setBooleanSetting("Main/SpeedLimiterEnabled", newSetting);
item.setChecked(newSetting);
AndroidHostInterface.getInstance().applySettings();
return true;
} else if (id == R.id.reset) {
AndroidHostInterface.getInstance().resetSystem();
} else if (id == R.id.quick_load) {
AndroidHostInterface.getInstance().loadState(false, 0);
} else if (id == R.id.quick_save) {
AndroidHostInterface.getInstance().saveState(false, 0);
} else if (id == R.id.quit) { } else if (id == R.id.quit) {
finish(); finish();
return true; return true;

View file

@ -48,7 +48,6 @@ public class EmulationSurfaceView extends SurfaceView {
return super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event);
} }
private AndroidHostInterface mHostInterface;
private ArrayMap<Integer, Integer> mControllerKeyMapping; private ArrayMap<Integer, Integer> mControllerKeyMapping;
private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) { private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) {
@ -59,9 +58,7 @@ public class EmulationSurfaceView extends SurfaceView {
mControllerKeyMapping.put(keyCode, mapping); mControllerKeyMapping.put(keyCode, mapping);
} }
public void initControllerKeyMapping(AndroidHostInterface hostInterface, public void initControllerKeyMapping(String controllerType) {
String controllerType) {
mHostInterface = hostInterface;
mControllerKeyMapping = new ArrayMap<>(); mControllerKeyMapping = new ArrayMap<>();
// TODO: Don't hardcode... // TODO: Don't hardcode...
@ -86,7 +83,7 @@ public class EmulationSurfaceView extends SurfaceView {
return false; return false;
final int mapping = mControllerKeyMapping.get(keyCode); final int mapping = mControllerKeyMapping.get(keyCode);
mHostInterface.setControllerButtonState(0, mapping, pressed); AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed);
return true; return true;
} }
} }

View file

@ -15,44 +15,21 @@ import androidx.preference.PreferenceManager;
import java.util.Set; import java.util.Set;
public class GameList { public class GameList {
static {
System.loadLibrary("duckstation-native");
}
private Context mContext; private Context mContext;
private String mCachePath;
private String mRedumpDatPath;
private String[] mSearchDirectories;
private boolean mSearchRecursively;
private GameListEntry[] mEntries; private GameListEntry[] mEntries;
private ListViewAdapter mAdapter;
static private native GameListEntry[] getEntries(String cachePath, String redumpDatPath,
String[] searchDirectories,
boolean searchRecursively);
public GameList(Context context) { public GameList(Context context) {
mContext = context; mContext = context;
refresh(); mAdapter = new ListViewAdapter();
mEntries = new GameListEntry[0];
} }
public void refresh() { public void refresh(boolean invalidateCache, boolean invalidateDatabase) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
mCachePath = preferences.getString("GameList/CachePath", "");
mRedumpDatPath = preferences.getString("GameList/RedumpDatPath", "");
Set<String> searchDirectories =
preferences.getStringSet("GameList/SearchDirectories", null);
if (searchDirectories != null) {
mSearchDirectories = new String[searchDirectories.size()];
searchDirectories.toArray(mSearchDirectories);
} else {
mSearchDirectories = new String[0];
}
mSearchRecursively = preferences.getBoolean("GameList/SearchRecursively", true);
// Search and get entries from native code // Search and get entries from native code
mEntries = getEntries(mCachePath, mRedumpDatPath, mSearchDirectories, mSearchRecursively); AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase);
mEntries = AndroidHostInterface.getInstance().getGameListEntries();
mAdapter.notifyDataSetChanged();
} }
public int getEntryCount() { public int getEntryCount() {
@ -97,6 +74,6 @@ public class GameList {
} }
public BaseAdapter getListViewAdapter() { public BaseAdapter getListViewAdapter() {
return new ListViewAdapter(); return mAdapter;
} }
} }

View file

@ -28,7 +28,7 @@ public class GameListEntry {
private String mTitle; private String mTitle;
private long mSize; private long mSize;
private String mModifiedTime; private String mModifiedTime;
private ConsoleRegion mRegion; private DiscRegion mRegion;
private EntryType mType; private EntryType mType;
private CompatibilityRating mCompatibilityRating; private CompatibilityRating mCompatibilityRating;
@ -42,9 +42,9 @@ public class GameListEntry {
mModifiedTime = modifiedTime; mModifiedTime = modifiedTime;
try { try {
mRegion = ConsoleRegion.valueOf(region); mRegion = DiscRegion.valueOf(region);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
mRegion = ConsoleRegion.NTSC_U; mRegion = DiscRegion.NTSC_U;
} }
try { try {
@ -74,7 +74,7 @@ public class GameListEntry {
public String getModifiedTime() { return mModifiedTime; } public String getModifiedTime() { return mModifiedTime; }
public ConsoleRegion getRegion() { public DiscRegion getRegion() {
return mRegion; return mRegion;
} }

View file

@ -43,22 +43,26 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (!AndroidHostInterface.createInstance(this)) {
Log.i("MainActivity", "Failed to create host interface");
throw new RuntimeException("Failed to create host interface");
}
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab); findViewById(R.id.fab_add_game_directory).setOnClickListener(new View.OnClickListener() {
fab.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (!checkForExternalStoragePermissions()) startAddGameDirectory();
return; }
});
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() {
i.addCategory(Intent.CATEGORY_DEFAULT); @Override
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); public void onClick(View view) {
startActivityForResult(Intent.createChooser(i, "Choose directory"), startEmulation(null, true);
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
} }
}); });
@ -69,7 +73,7 @@ public class MainActivity extends AppCompatActivity {
mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
startEmulation(mGameList.getEntry(position).getPath()); startEmulation(mGameList.getEntry(position).getPath(), true);
} }
}); });
mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@ -89,6 +93,18 @@ public class MainActivity extends AppCompatActivity {
return true; return true;
} }
}); });
mGameList.refresh(false, false);
}
private void startAddGameDirectory() {
if (!checkForExternalStoragePermissions())
return;
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, "Choose directory"),
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
} }
@Override @Override
@ -106,6 +122,13 @@ public class MainActivity extends AppCompatActivity {
int id = item.getItemId(); int id = item.getItemId();
//noinspection SimplifiableIfStatement //noinspection SimplifiableIfStatement
if (id == R.id.action_add_game_directory) {
startAddGameDirectory();
} else if (id == R.id.action_scan_for_new_games) {
mGameList.refresh(false, false);
} if (id == R.id.action_rescan_all_games) {
mGameList.refresh(true, false);
}
if (id == R.id.action_settings) { if (id == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent); startActivity(intent);
@ -132,16 +155,16 @@ public class MainActivity extends AppCompatActivity {
} }
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Set<String> currentValues = prefs.getStringSet("GameList/SearchDirectories", null); Set<String> currentValues = prefs.getStringSet("GameList/RecursivePaths", null);
if (currentValues == null) if (currentValues == null)
currentValues = new HashSet<String>(); currentValues = new HashSet<String>();
currentValues.add(path); currentValues.add(path);
SharedPreferences.Editor editor = prefs.edit(); SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet("GameList/SearchDirectories", currentValues); editor.putStringSet("GameList/RecursivePaths", currentValues);
editor.apply(); editor.apply();
Log.i("MainActivity", "Added path '" + path + "' to game list search directories"); Log.i("MainActivity", "Added path '" + path + "' to game list search directories");
mGameList.refresh(); mGameList.refresh(false, false);
} }
break; break;
} }
@ -175,13 +198,14 @@ public class MainActivity extends AppCompatActivity {
} }
} }
private boolean startEmulation(String bootPath) { private boolean startEmulation(String bootPath, boolean resumeState) {
if (!checkForExternalStoragePermissions()) { if (!checkForExternalStoragePermissions()) {
return false; return false;
} }
Intent intent = new Intent(this, EmulationActivity.class); Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath); intent.putExtra("bootPath", bootPath);
intent.putExtra("resumeState", resumeState);
startActivity(intent); startActivity(intent);
return true; return true;
} }

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="M8,5v14l11,-7z"/>
</vector>

View file

@ -24,7 +24,17 @@
<include layout="@layout/content_main"/> <include layout="@layout/content_main"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab_resume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/fab_margin"
android:layout_marginRight="96dp"
app:backgroundTint="@android:color/background_light"
app:srcCompat="@drawable/ic_baseline_play_arrow_24" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_game_directory"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"

View file

@ -2,14 +2,22 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/quick_load_save"> <group android:id="@+id/actions">
<item android:title="Quick Load" /> <item android:id="@+id/reset"
<item android:title="Quick Save" /> android:title="Reset" />
<item android:id="@+id/quick_load"
android:title="Quick Load" />
<item android:id="@+id/quick_save"
android:title="Quick Save" />
</group> </group>
<group android:id="@+id/quick_settings"> <group android:id="@+id/quick_settings">
<item <item
android:id="@+id/change_disc" android:id="@+id/change_disc"
android:title="Change Disc" /> android:title="Change Disc" />
<item
android:id="@+id/enable_speed_limiter"
android:title="Enable Speed Limiter"
android:checkable="true" />
<item <item
android:id="@+id/show_controller" android:id="@+id/show_controller"
android:checkable="true" android:checkable="true"

View file

@ -2,6 +2,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.github.stenzek.duckstation.MainActivity" > tools:context="com.github.stenzek.duckstation.MainActivity" >
<group android:id="@+id/game_list">
<item android:id="@+id/action_add_game_directory"
android:title="Add Game Directory" />
<item android:id="@+id/action_scan_for_new_games"
android:title="Scan For New Games" />
<item android:id="@+id/action_rescan_all_games"
android:title="Rescan All Games" />
</group>
<item android:id="@+id/action_settings" <item android:id="@+id/action_settings"
android:title="@string/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" android:orderInCategory="100"