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()
{
CommonHostInterface::LoadSettings(m_settings_interface);
CommonHostInterface::UpdateInputMap(m_settings_interface);
}
void AndroidHostInterface::UpdateInputMap()
@ -396,6 +397,19 @@ void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code,
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)
{
Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV);
@ -530,28 +544,18 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobje
return code.value_or(-1);
}
DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_cache_path, jstring j_redump_dat_path,
jarray j_search_directories, jboolean search_recursively)
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, jboolean invalidate_database)
{
// const std::string cache_path = AndroidHelpers::JStringToString(env, j_cache_path);
std::string redump_dat_path = AndroidHelpers::JStringToString(env, j_redump_dat_path);
AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database);
}
// TODO: This should use the base HostInterface.
GameList gl;
if (!redump_dat_path.empty())
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);
static const char* DiscRegionToString(DiscRegion region) {
static std::array<const char*, 4> names = {{"NTSC_J", "NTSC_U", "PAL", "Other"}};
return names[static_cast<int>(region)];
}
DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj)
{
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry");
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");
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);
u32 counter = 0;
for (const GameListEntry& entry : gl.GetEntries())
for (const GameListEntry& entry : hi->GetGameList()->GetEntries())
{
const Timestamp modified_ts(
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 code = env->NewStringUTF(entry.code.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 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;
}
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 SetControllerButtonState(u32 index, s32 button_code, bool pressed);
void RefreshGameList(bool invalidate_cache, bool invalidate_database);
void ApplySettings();
protected:
void SetUserDirectory() 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)
{
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)
return {};

View file

@ -7,10 +7,6 @@ public class AndroidHostInterface
{
private long nativePointer;
static {
System.loadLibrary("duckstation-native");
}
static public native AndroidHostInterface create(Context context);
public AndroidHostInterface(long nativePointer)
@ -28,4 +24,26 @@ public class AndroidHostInterface
public native void setControllerType(int index, String typeName);
public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
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 android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@ -18,6 +19,7 @@ import android.view.MenuItem;
import android.widget.FrameLayout;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
/**
* 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 {
/**
* 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
@ -96,15 +106,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Once we get a surface, we can boot.
if (mHostInterface.isEmulationThreadRunning()) {
mHostInterface.surfaceChanged(holder.getSurface(), format, width, height);
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height);
return;
}
String bootPath = getIntent().getStringExtra("bootPath");
String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath");
boolean resumeState = getIntent().getBooleanExtra("resumeState", false);
if (!mHostInterface
if (!AndroidHostInterface.getInstance()
.startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) {
Log.e("EmulationActivity", "Failed to start emulation thread");
finishActivity(0);
@ -114,16 +125,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (!mHostInterface.isEmulationThreadRunning())
if (!AndroidHostInterface.getInstance().isEmulationThreadRunning())
return;
Log.i("EmulationActivity", "Stopping emulation thread");
mHostInterface.stopEmulationThread();
AndroidHostInterface.getInstance().stopEmulationThread();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
setContentView(R.layout.activity_emulation);
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.
FrameLayout activityLayout = findViewById(R.id.frameLayout);
mTouchscreenController = new TouchscreenControllerView(this);
activityLayout.addView(mTouchscreenController);
mTouchscreenController.init(0, "DigitalController", mHostInterface);
mTouchscreenController.init(0, "DigitalController", AndroidHostInterface.getInstance());
setTouchscreenControllerVisibility(true);
// Hook up controller input.
mContentView.initControllerKeyMapping(mHostInterface, "DigitalController");
mContentView.initControllerKeyMapping("DigitalController");
}
@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.
getMenuInflater().inflate(R.menu.menu_emulation, menu);
menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible);
menu.findItem(R.id.enable_speed_limiter).setChecked(getBooleanSetting("Main/SpeedLimiterEnabled", true));
return true;
}
@ -191,6 +200,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
setTouchscreenControllerVisibility(!mTouchscreenControllerVisible);
item.setChecked(mTouchscreenControllerVisible);
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) {
finish();
return true;

View file

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

View file

@ -15,44 +15,21 @@ import androidx.preference.PreferenceManager;
import java.util.Set;
public class GameList {
static {
System.loadLibrary("duckstation-native");
}
private Context mContext;
private String mCachePath;
private String mRedumpDatPath;
private String[] mSearchDirectories;
private boolean mSearchRecursively;
private GameListEntry[] mEntries;
static private native GameListEntry[] getEntries(String cachePath, String redumpDatPath,
String[] searchDirectories,
boolean searchRecursively);
private ListViewAdapter mAdapter;
public GameList(Context context) {
mContext = context;
refresh();
mAdapter = new ListViewAdapter();
mEntries = new GameListEntry[0];
}
public void refresh() {
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);
public void refresh(boolean invalidateCache, boolean invalidateDatabase) {
// 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() {
@ -97,6 +74,6 @@ public class GameList {
}
public BaseAdapter getListViewAdapter() {
return new ListViewAdapter();
return mAdapter;
}
}

View file

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

View file

@ -43,22 +43,26 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle 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);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
findViewById(R.id.fab_add_game_directory).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
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);
startAddGameDirectory();
}
});
findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startEmulation(null, true);
}
});
@ -69,7 +73,7 @@ public class MainActivity extends AppCompatActivity {
mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
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() {
@ -89,6 +93,18 @@ public class MainActivity extends AppCompatActivity {
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
@ -106,6 +122,13 @@ public class MainActivity extends AppCompatActivity {
int id = item.getItemId();
//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) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
@ -132,16 +155,16 @@ public class MainActivity extends AppCompatActivity {
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Set<String> currentValues = prefs.getStringSet("GameList/SearchDirectories", null);
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/SearchDirectories", currentValues);
editor.putStringSet("GameList/RecursivePaths", currentValues);
editor.apply();
Log.i("MainActivity", "Added path '" + path + "' to game list search directories");
mGameList.refresh();
mGameList.refresh(false, false);
}
break;
}
@ -175,13 +198,14 @@ public class MainActivity extends AppCompatActivity {
}
}
private boolean startEmulation(String bootPath) {
private boolean startEmulation(String bootPath, boolean resumeState) {
if (!checkForExternalStoragePermissions()) {
return false;
}
Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath);
intent.putExtra("resumeState", resumeState);
startActivity(intent);
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"/>
<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_height="wrap_content"
android:layout_gravity="bottom|end"

View file

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

View file

@ -2,6 +2,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
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"
android:title="@string/action_settings"
android:orderInCategory="100"