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;
}
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="60dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8 0,-1.85 0.63,-3.55 1.69,-4.9L16.9,18.31C15.55,19.37 13.85,20 12,20zM18.31,16.9L7.1,5.69C8.45,4.63 10.15,4 12,4c4.42,0 8,3.58 8,8 0,1.85 -0.63,3.55 -1.69,4.9z"/>
</vector>

View File

@ -0,0 +1,66 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="4dp"
android:layout_marginLeft="15dp"
android:layout_alignParentTop="true"
android:scaleType="fitXY"
tools:srcCompat="@drawable/ic_media_cdrom" />
<TextView
android:id="@+id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="8dp"
android:text="Game Slot 1"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/image" />
<TextView
android:id="@+id/game"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="0dp"
android:text="SCES-0000 - Game Name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_below="@id/summary"
android:layout_toRightOf="@id/image" />
<TextView
android:id="@+id/path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="0dp"
android:text="Dump Name.chd"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_below="@id/game"
android:layout_toRightOf="@id/image" />
<TextView
android:id="@+id/timestamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="14dp"
android:text="Saved at Timestamp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_below="@id/path"
android:layout_toRightOf="@id/image" />
</RelativeLayout>

View File

@ -81,7 +81,6 @@
<string-array name="emulation_menu">
<item>Cargar Estado</item>
<item>Guardar Estado</item>
<item>Guardar Estado en Ranura</item>
<item>Activar Avance Rápido</item>
<item>Más Opciones</item>
<item>Salir</item>
@ -94,19 +93,6 @@
<item>Cambiar Control de Pantalla Tactil</item>
<item>Editar Diseño del Control de Pantalla Tactil</item>
</string-array>
<string-array name="emulation_save_state_slot_menu">
<item>Ranura Rápida</item>
<item>Ranura 1</item>
<item>Ranura 2</item>
<item>Ranura 3</item>
<item>Ranura 4</item>
<item>Ranura 5</item>
<item>Ranura 6</item>
<item>Ranura 7</item>
<item>Ranura 8</item>
<item>Ranura 9</item>
<item>Ranura 10</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Ninguno (Velocidad Doble)</item>
<item>2x (Velocidad Cuádruple)</item>

View File

@ -81,7 +81,6 @@
<string-array name="emulation_menu">
<item>Carica Stato</item>
<item>Salva Stato</item>
<item>Slot Salvataggio Stato</item>
<item>Abilita/Disabilita Avanti Veloce</item>
<item>Altre Opzioni</item>
<item>Esci</item>
@ -94,19 +93,6 @@
<item>Cambia Controller Touchscreen</item>
<item>Edit Touchscreen Controller Layout</item>
</string-array>
<string-array name="emulation_save_state_slot_menu">
<item>Slot Veloce</item>
<item>Slot Gioco 1</item>
<item>Slot Gioco 2</item>
<item>Slot Gioco 3</item>
<item>Slot Gioco 4</item>
<item>Slot Gioco 5</item>
<item>Slot Gioco 6</item>
<item>Slot Gioco 7</item>
<item>Slot Gioco 8</item>
<item>Slot Gioco 9</item>
<item>Slot Gioco 10</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Nessuna Velocità Doppia)</item>
<item>2x (Velocità Quadrupla</item>

View File

@ -81,7 +81,6 @@
<string-array name="emulation_menu">
<item>Staat Laden</item>
<item>Staat Opslaan</item>
<item>Staat Nummer</item>
<item>Doorspoelen aan/uitzetten</item>
<item>Meer Opties</item>
<item>Afsluiten</item>
@ -94,19 +93,6 @@
<item>Touchscreen Controller Aanpassen</item>
<item>Edit Touchscreen Controller Layout</item>
</string-array>
<string-array name="emulation_save_state_slot_menu">
<item>Snel Slot</item>
<item>Game Slot 1</item>
<item>Game Slot 2</item>
<item>Game Slot 3</item>
<item>Game Slot 4</item>
<item>Game Slot 5</item>
<item>Game Slot 6</item>
<item>Game Slot 7</item>
<item>Game Slot 8</item>
<item>Game Slot 9</item>
<item>Game Slot 10</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Geen (Dubbele Snelheid)</item>
<item>2x (Vierdubbele Snelheid)</item>

View File

@ -81,7 +81,6 @@
<string-array name="emulation_menu">
<item>Carregar Estado</item>
<item>Salvar Estado</item>
<item>Salvar para Compartimento</item>
<item>Avanço (Fixo)</item>
<item>Mais Opções</item>
<item>Sair</item>
@ -94,19 +93,6 @@
<item>Mudar controle em Tela</item>
<item>Editar Posição dos Controles (Tela)</item>
</string-array>
<string-array name="emulation_save_state_slot_menu">
<item>Armazenamento Rápido</item>
<item>Armazenamento 1</item>
<item>Armazenamento 2</item>
<item>Armazenamento 3</item>
<item>Armazenamento 4</item>
<item>Armazenamento 5</item>
<item>Armazenamento 6</item>
<item>Armazenamento 7</item>
<item>Armazenamento 8</item>
<item>Armazenamento 9</item>
<item>Armazenamento 10</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Nenhum</item>
<item>2x (4x Veloz)</item>

View File

@ -81,7 +81,6 @@
<string-array name="emulation_menu">
<item>Загрузить состояние</item>
<item>Сохранить состояние</item>
<item>Слот сохранения</item>
<item>Включить ускорение</item>
<item>Другие опции</item>
<item>Выход</item>

View File

@ -160,7 +160,6 @@
<string-array name="emulation_menu">
<item>Load State</item>
<item>Save State</item>
<item>Save State Slot</item>
<item>Toggle Fast Forward</item>
<item>More Options</item>
<item>Quit</item>
@ -173,19 +172,6 @@
<item>Change Touchscreen Controller</item>
<item>Edit Touchscreen Controller Layout</item>
</string-array>
<string-array name="emulation_save_state_slot_menu">
<item>Quick Slot</item>
<item>Game Slot 1</item>
<item>Game Slot 2</item>
<item>Game Slot 3</item>
<item>Game Slot 4</item>
<item>Game Slot 5</item>
<item>Game Slot 6</item>
<item>Game Slot 7</item>
<item>Game Slot 8</item>
<item>Game Slot 9</item>
<item>Game Slot 10</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>None (Double Speed)</item>
<item>2x (Quad Speed)</item>

View File

@ -206,4 +206,8 @@
<string name="menu_edit_game_directories_add_path">Add Path</string>
<string name="edit_game_directories_add_path">Add Path</string>
<string name="edit_game_directories_add_path_summary">Enter the full path to the directory with games.\n\nYou can get this from a file manager app.\n\nExample: /storage/emulated/0/games</string>
<string name="save_state_info_slot_is_empty">Slot Is Empty</string>
<string name="save_state_info_game_save_n">Game Save %d</string>
<string name="save_state_info_global_save_n">Global Save %d</string>
<string name="save_state_info_quick_save">Quick Save</string>
</resources>