Android: Add graphical save/load state selector

This commit is contained in:
Connor McLaughlin
2021-02-07 02:47:19 +10:00
parent b560142015
commit 6ad2b72c2e
14 changed files with 370 additions and 99 deletions

View File

@ -136,6 +136,8 @@ public class AndroidHostInterface {
public native boolean setMediaFilename(String filename);
public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty);
static {
System.loadLibrary("duckstation-native");
}

View File

@ -19,6 +19,7 @@ import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -41,7 +42,6 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
private boolean mApplySettingsOnSurfaceRestored = false;
private String mGameTitle = null;
private EmulationSurfaceView mContentView;
private int mSaveStateSlot = 0;
private boolean getBooleanSetting(String key, boolean defaultValue) {
return mPreferences.getBoolean(key, defaultValue);
@ -398,42 +398,36 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if (mGameTitle != null && !mGameTitle.isEmpty())
builder.setTitle(mGameTitle);
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
switch (i) {
case 0: // Quick Load
case 0: // Load State
{
AndroidHostInterface.getInstance().loadState(false, mSaveStateSlot);
onMenuClosed();
showSaveStateMenu(false);
return;
}
case 1: // Quick Save
case 1: // Save State
{
AndroidHostInterface.getInstance().saveState(false, mSaveStateSlot);
onMenuClosed();
showSaveStateMenu(true);
return;
}
case 2: // Save State Slot
{
showSaveStateSlotMenu();
return;
}
case 3: // Toggle Fast Forward
case 2: // Toggle Fast Forward
{
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
onMenuClosed();
return;
}
case 4: // More Options
case 3: // More Options
{
showMoreMenu();
return;
}
case 5: // Quit
case 4: // Quit
{
mStopRequested = true;
finish();
@ -445,15 +439,34 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
builder.create().show();
}
private void showSaveStateSlotMenu() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setSingleChoiceItems(R.array.emulation_save_state_slot_menu, mSaveStateSlot, (dialogInterface, i) -> {
mSaveStateSlot = i;
dialogInterface.dismiss();
private void showSaveStateMenu(boolean saving) {
final SaveStateInfo[] infos = AndroidHostInterface.getInstance().getSaveStateInfo(true);
if (infos == null) {
onMenuClosed();
return;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
final ListView listView = new ListView(this);
listView.setAdapter(new SaveStateInfo.ListAdapter(this, infos));
builder.setView(listView);
builder.setOnDismissListener((dialog) -> {
onMenuClosed();
});
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
builder.create().show();
final AlertDialog dialog = builder.create();
listView.setOnItemClickListener((parent, view, position, id) -> {
SaveStateInfo info = infos[position];
if (saving) {
AndroidHostInterface.getInstance().saveState(info.isGlobal(), info.getSlot());
} else {
AndroidHostInterface.getInstance().loadState(info.isGlobal(), info.getSlot());
}
dialog.dismiss();
});
dialog.show();
}
private void showMoreMenu() {

View File

@ -298,7 +298,7 @@ public class GameDirectoriesActivity extends AppCompatActivity {
.setTitle(R.string.edit_game_directories_add_path)
.setMessage(R.string.edit_game_directories_add_path_summary)
.setView(text)
.setPositiveButton("Add", (dialog, which) -> {
.setPositiveButton("Add", (dialog, which) -> {
final String path = text.getText().toString();
if (!path.isEmpty()) {
addSearchDirectory(GameDirectoriesActivity.this, path, true);

View File

@ -0,0 +1,151 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.nio.ByteBuffer;
public class SaveStateInfo {
private String mPath;
private String mGameTitle;
private String mGameCode;
private String mMediaPath;
private String mTimestamp;
private int mSlot;
private boolean mGlobal;
private Bitmap mScreenshot;
public SaveStateInfo(String path, String gameTitle, String gameCode, String mediaPath, String timestamp, int slot, boolean global,
int screenshotWidth, int screenshotHeight, byte[] screenshotData) {
mPath = path;
mGameTitle = gameTitle;
mGameCode = gameCode;
mMediaPath = mediaPath;
mTimestamp = timestamp;
mSlot = slot;
mGlobal = global;
if (screenshotData != null) {
try {
mScreenshot = Bitmap.createBitmap(screenshotWidth, screenshotHeight, Bitmap.Config.ARGB_8888);
mScreenshot.copyPixelsFromBuffer(ByteBuffer.wrap(screenshotData));
} catch (Exception e) {
mScreenshot = null;
}
}
}
public boolean exists() {
return mPath != null;
}
public String getPath() {
return mPath;
}
public String getGameTitle() {
return mGameTitle;
}
public String getGameCode() {
return mGameCode;
}
public String getMediaPath() {
return mMediaPath;
}
public String getTimestamp() {
return mTimestamp;
}
public int getSlot() {
return mSlot;
}
public boolean isGlobal() {
return mGlobal;
}
public Bitmap getScreenshot() {
return mScreenshot;
}
private void fillView(Context context, View view) {
ImageView imageView = (ImageView) view.findViewById(R.id.image);
TextView summaryView = (TextView) view.findViewById(R.id.summary);
TextView gameView = (TextView) view.findViewById(R.id.game);
TextView pathView = (TextView) view.findViewById(R.id.path);
TextView timestampView = (TextView) view.findViewById(R.id.timestamp);
if (mScreenshot != null)
imageView.setImageBitmap(mScreenshot);
else
imageView.setImageDrawable(context.getDrawable(R.drawable.ic_baseline_not_interested_60));
String summaryText;
if (mGlobal)
summaryView.setText(String.format(context.getString(R.string.save_state_info_global_save_n), mSlot));
else if (mSlot == 0)
summaryView.setText(R.string.save_state_info_quick_save);
else
summaryView.setText(String.format(context.getString(R.string.save_state_info_game_save_n), mSlot));
if (exists()) {
gameView.setText(String.format("%s - %s", mGameCode, mGameTitle));
int lastSlashPosition = mMediaPath.lastIndexOf('/');
if (lastSlashPosition >= 0)
pathView.setText(mMediaPath.substring(lastSlashPosition + 1));
else
pathView.setText(mMediaPath);
timestampView.setText(mTimestamp);
} else {
gameView.setText(R.string.save_state_info_slot_is_empty);
pathView.setText("");
timestampView.setText("");
}
}
public static class ListAdapter extends BaseAdapter {
private final Context mContext;
private final SaveStateInfo[] mInfos;
public ListAdapter(Context context, SaveStateInfo[] infos) {
mContext = context;
mInfos = infos;
}
@Override
public int getCount() {
return mInfos.length;
}
@Override
public Object getItem(int position) {
return mInfos[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.save_state_view_entry, parent, false);
}
mInfos[position].fillView(mContext, convertView);
return convertView;
}
}
}