Rename emulator.state to emulator.a2state and handle migration

This commit is contained in:
Aaron Culliney 2017-06-28 21:39:43 -07:00
parent dacf0de80e
commit d98c4afa84
10 changed files with 249 additions and 125 deletions

View File

@ -23,12 +23,14 @@ import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import org.deadc0de.apple2ix.basic.BuildConfig;
import org.deadc0de.apple2ix.basic.R;
public class Apple2Activity extends Activity implements Apple2DiskChooserActivity.Callback {
@ -190,7 +192,20 @@ public class Apple2Activity extends Activity implements Apple2DiskChooserActivit
@Override
public void onDisksChosen(DiskArgs args) {
sDisksChosen = args;
if (Apple2DisksMenu.hasDiskExtension(args.name)) {
sDisksChosen = args;
} else {
if (args.name.equals("")) {
return;
}
if (Apple2DisksMenu.hasStateExtension(args.name)) {
////mMainMenu.restoreEmulatorState(args); FIXME TODO ...
return;
}
Toast.makeText(this, R.string.disk_insert_toast_cannot, Toast.LENGTH_SHORT).show();
}
}
@Override
@ -205,7 +220,8 @@ public class Apple2Activity extends Activity implements Apple2DiskChooserActivit
}
}
if (grantedPermissions) {
// this will force copying APK files (now that we have permission
// perform migration(s) and assets exposure now
Apple2Utils.migrateToExternalStorage(Apple2Activity.this);
Apple2Utils.exposeAPKAssetsToExternal(Apple2Activity.this);
} // else ... we keep nagging on app startup ...
} else {

View File

@ -263,7 +263,11 @@ public class Apple2DisksMenu implements Apple2MenuView {
}
public void dismiss() {
String path = popPathStack();
String path = null;
if (!(boolean) Apple2Preferences.getJSONPref(SETTINGS.USE_NEWSCHOOL_DISK_SELECTION)) {
path = popPathStack();
}
if (path == null) {
mActivity.popApple2View(this);
} else {
@ -498,6 +502,20 @@ public class Apple2DisksMenu implements Apple2MenuView {
return (suffix.equalsIgnoreCase(".dsk.gz") || suffix.equalsIgnoreCase(".nib.gz"));
}
public static boolean hasStateExtension(String name) {
// check file extensions ... sigh ... no String.endsWithIgnoreCase() ?
final int extLen = Apple2MainMenu.SAVE_FILE_EXTENSION.length();
final int len = name.length();
if (len <= extLen) {
return false;
}
final String suffix = name.substring(len - extLen, len);
return suffix.equalsIgnoreCase(Apple2MainMenu.SAVE_FILE_EXTENSION);
}
// ------------------------------------------------------------------------
// internals ...

View File

@ -40,7 +40,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class Apple2MainMenu {
public final static String SAVE_FILE = "emulator.state";
public final static String OLD_SAVE_FILE = "emulator.state";
public final static String SAVE_FILE_EXTENSION = ".a2state";
public final static String SAVE_FILE = "emulator" + SAVE_FILE_EXTENSION;
private final static String TAG = "Apple2MainMenu";
private Apple2Activity mActivity = null;
@ -296,8 +298,86 @@ public class Apple2MainMenu {
mActivity.registerAndShowDialog(rebootQuitDialog);
}
public void restoreEmulatorState(String quickSavePath) {
public void maybeSaveRestore() {
Apple2DisksMenu.ejectDisk(/*isDriveA:*/true);
Apple2DisksMenu.ejectDisk(/*isDriveA:*/false);
// First we extract and open the emulator.a2state disk paths (which could be in a restricted location)
String jsonString = mActivity.stateExtractDiskPaths(quickSavePath);
try {
JSONObject map = new JSONObject(jsonString);
map.put("stateFile", quickSavePath);
final String[] diskPathKeys = new String[]{"diskA", "diskB"};
final String[] readOnlyKeys = new String[]{"readOnlyA", "readOnlyB"};
final String[] fdKeys = new String[]{"fdA", "fdB"};
ParcelFileDescriptor[] pfds = {null, null};
for (int i = 0; i < 2; i++) {
String diskPath = map.getString(diskPathKeys[i]);
boolean readOnly = map.getBoolean(readOnlyKeys[i]);
Apple2Preferences.setJSONPref(i == 0 ? Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A : Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B, diskPath);
Apple2Preferences.setJSONPref(i == 0 ? Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_RO : Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_RO, readOnly);
if (diskPath.equals("")) {
continue;
}
if (diskPath.startsWith(Apple2DisksMenu.EXTERNAL_CHOOSER_SENTINEL)) {
String uriString = diskPath.substring(Apple2DisksMenu.EXTERNAL_CHOOSER_SENTINEL.length());
Uri uri = Uri.parse(uriString);
pfds[i] = Apple2DiskChooserActivity.openFileDescriptorFromUri(mActivity, uri);
if (pfds[i] == null) {
Log.e(TAG, "Did not find URI for drive #" + i + " specified in " + SAVE_FILE + " file : " + diskPath);
} else {
int fd = pfds[i].getFd();
map.put(fdKeys[i], fd);
}
} else {
boolean exists = new File(diskPath).exists();
if (!exists) {
Log.e(TAG, "Did not find path for drive #" + i + " specified in " + SAVE_FILE + " file : " + diskPath);
}
}
}
jsonString = mActivity.loadState(map.toString());
for (int i = 0; i < 2; i++) {
try {
if (pfds[i] != null) {
pfds[i].close();
}
} catch (IOException ioe) {
Log.e(TAG, "Error attempting to close PFD #" + i + " : " + ioe);
}
}
map = new JSONObject(jsonString);
{
boolean wasGzippedA = map.getBoolean("wasGzippedA");
Apple2Preferences.setJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_GZ, wasGzippedA);
}
{
boolean wasGzippedB = map.getBoolean("wasGzippedB");
Apple2Preferences.setJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_GZ, wasGzippedB);
}
// FIXME TODO : what to do if state load failed?
} catch (Throwable t) {
Log.v(TAG, "OOPS : " + t);
}
}
private void maybeSaveRestore() {
mActivity.pauseEmulation();
final String quickSavePath;
@ -324,86 +404,13 @@ public class Apple2MainMenu {
}).setNeutralButton(R.string.restore, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!selectionAlreadyHandled.compareAndSet(false, true)) {
Log.v(TAG, "OMG, avoiding nasty UI race in sync/restore onClick()");
return;
}
Apple2DisksMenu.ejectDisk(/*isDriveA:*/true);
Apple2DisksMenu.ejectDisk(/*isDriveA:*/false);
// First we extract and open the emulator.state disk paths (which could be in a restricted location)
String jsonString = mActivity.stateExtractDiskPaths(quickSavePath);
try {
JSONObject map = new JSONObject(jsonString);
map.put("stateFile", quickSavePath);
final String[] diskPathKeys = new String[]{"diskA", "diskB"};
final String[] readOnlyKeys = new String[]{"readOnlyA", "readOnlyB"};
final String[] fdKeys = new String[]{"fdA", "fdB"};
ParcelFileDescriptor[] pfds = {null, null};
for (int i = 0; i < 2; i++) {
String diskPath = map.getString(diskPathKeys[i]);
boolean readOnly = map.getBoolean(readOnlyKeys[i]);
Apple2Preferences.setJSONPref(i == 0 ? Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A : Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B, diskPath);
Apple2Preferences.setJSONPref(i == 0 ? Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_RO : Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_RO, readOnly);
if (diskPath.equals("")) {
continue;
}
if (diskPath.startsWith(Apple2DisksMenu.EXTERNAL_CHOOSER_SENTINEL)) {
String uriString = diskPath.substring(Apple2DisksMenu.EXTERNAL_CHOOSER_SENTINEL.length());
Uri uri = Uri.parse(uriString);
pfds[i] = Apple2DiskChooserActivity.openFileDescriptorFromUri(mActivity, uri);
if (pfds[i] == null) {
Log.e(TAG, "Did not find URI for drive #" + i + " specified in emulator.state file : " + diskPath);
} else {
int fd = pfds[i].getFd();
map.put(fdKeys[i], fd);
}
} else {
boolean exists = new File(diskPath).exists();
if (!exists) {
Log.e(TAG, "Did not find path for drive #" + i + " specified in emulator.state file : " + diskPath);
}
}
}
jsonString = mActivity.loadState(map.toString());
for (int i = 0; i < 2; i++) {
try {
if (pfds[i] != null) {
pfds[i].close();
}
} catch (IOException ioe) {
Log.e(TAG, "Error attempting to close PFD #" + i + " : " + ioe);
}
}
map = new JSONObject(jsonString);
{
boolean wasGzippedA = map.getBoolean("wasGzippedA");
Apple2Preferences.setJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_GZ, wasGzippedA);
}
{
boolean wasGzippedB = map.getBoolean("wasGzippedB");
Apple2Preferences.setJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_GZ, wasGzippedB);
}
// FIXME TODO : what to do if state load failed?
} catch (Throwable t) {
Log.v(TAG, "OOPS : " + t);
}
restoreEmulatorState(quickSavePath);
Apple2MainMenu.this.dismiss();
}
}).setNegativeButton(R.string.cancel, null).create();

View File

@ -175,7 +175,7 @@ public class Apple2Preferences {
Log.v(TAG, "Triggering migration to Apple2ix version : " + BuildConfig.VERSION_NAME);
setJSONPref(PREF_DOMAIN_INTERFACE, PREF_EMULATOR_VERSION, BuildConfig.VERSION_CODE);
Apple2Utils.migrate(activity);
Apple2Utils.migrateToExternalStorage(activity);
if (BuildConfig.VERSION_CODE >= 17) {
// FIXME TODO : remove this after most/all app users are on 18+

View File

@ -29,6 +29,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@ -102,49 +103,59 @@ public class Apple2Utils {
return attempts < maxAttempts;
}
public static void migrate(Apple2Activity activity) {
public static void migrateToExternalStorage(Apple2Activity activity) {
do {
if (BuildConfig.VERSION_CODE >= 18) {
// Migrate emulator.state file from internal path to external storage to allow user manipulation
// Rename old emulator state file
// TODO FIXME : Remove this migration code when all/most users are on version >= 18
final File extStorage = Apple2Utils.getExternalStorageDirectory(activity);
if (extStorage == null) {
break;
}
final String srcPath = getDataDir(activity) + File.separator + Apple2MainMenu.SAVE_FILE;
final File srcFile = new File(srcPath);
final File srcFile = new File(getDataDir(activity) + File.separator + Apple2MainMenu.OLD_SAVE_FILE);
if (!srcFile.exists()) {
break;
}
final String dstPath = extStorage + File.separator + Apple2MainMenu.SAVE_FILE;
final File dstFile = new File(getDataDir(activity) + File.separator + Apple2MainMenu.SAVE_FILE);
final boolean success = copyFile(srcFile, dstFile);
if (success) {
srcFile.delete();
}
}
} while (false);
final int maxAttempts = 5;
int attempts = 0;
do {
try {
FileInputStream is = new FileInputStream(srcFile);
FileOutputStream os = new FileOutputStream(dstPath);
copyFile(is, os);
break;
} catch (InterruptedIOException e) {
// EINTR, EAGAIN ...
} catch (IOException e) {
Log.d(TAG, "OOPS exception attempting to copy emulator.state file : " + e);
}
try {
Thread.sleep(100, 0);
} catch (InterruptedException ie) {
// ...
}
++attempts;
} while (attempts < maxAttempts);
final File extStorage = Apple2Utils.getExternalStorageDirectory(activity);
if (extStorage == null) {
return;
}
srcFile.delete();
do {
if (BuildConfig.VERSION_CODE >= 18) {
// Migrate old emulator state file from internal path to external storage to allow user manipulation
// TODO FIXME : Remove this migration code when all/most users are on version >= 18
final File srcFile = new File(getDataDir(activity) + File.separator + Apple2MainMenu.SAVE_FILE);
if (!srcFile.exists()) {
break;
}
final File dstFile = new File(extStorage + File.separator + Apple2MainMenu.SAVE_FILE);
final boolean success = copyFile(srcFile, dstFile);
if (success) {
srcFile.delete();
}
}
} while (false);
do {
if (BuildConfig.VERSION_CODE >= 20) {
// Recursively rename all *.state files found in /sdcard/apple2ix
// TODO FIXME : Remove this migration code when all/most users are on version >= 20
recursivelyRenameEmulatorStateFiles(extStorage);
}
} while (false);
}
@ -402,6 +413,85 @@ public class Apple2Utils {
} while (attempts < maxAttempts);
}
private static void recursivelyRenameEmulatorStateFiles(File directory) {
try {
if (!directory.isDirectory()) {
return;
}
final int oldSuffixLen = 6;
File[] files = directory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.equals(".") || name.equals("..")) {
return false;
}
final File file = new File(dir, name);
if (file.isDirectory()) {
return true;
}
final int len = name.length();
if (len < oldSuffixLen) {
return false;
}
final String suffix = name.substring(len - oldSuffixLen, len);
return suffix.equalsIgnoreCase(".state");
}
});
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
recursivelyRenameEmulatorStateFiles(file);
} else {
final File srcFile = file;
final String oldName = file.getName();
final String newName = oldName.substring(0, oldName.length() - oldSuffixLen) + Apple2MainMenu.SAVE_FILE_EXTENSION;
boolean success = file.renameTo(new File(file.getParentFile(), newName));
if (success) {
srcFile.delete();
}
}
}
} catch (Exception e) {
Log.e(TAG, "OOPS : {e}");
}
}
private static boolean copyFile(final File srcFile, final File dstFile) {
final int maxAttempts = 5;
int attempts = 0;
do {
try {
FileInputStream is = new FileInputStream(srcFile);
FileOutputStream os = new FileOutputStream(dstFile);
copyFile(is, os);
break;
} catch (InterruptedIOException e) {
// EINTR, EAGAIN ...
} catch (IOException e) {
Log.d(TAG, "OOPS exception attempting to copy emulator state file : " + e);
}
try {
Thread.sleep(100, 0);
} catch (InterruptedException ie) {
// ...
}
++attempts;
} while (attempts < maxAttempts);
return attempts < maxAttempts;
}
private static void copyFile(InputStream is, OutputStream os) throws IOException {
final int BUF_SZ = 4096;
byte[] buf = new byte[BUF_SZ];

View File

@ -32,8 +32,6 @@
<string name="diskA">Laufwerk 1</string>
<string name="diskB">Laufwerk 2</string>
<string name="disk_eject">Auswerfen</string>
<string name="disk_insert_toast">Die eingelegte Diskette ist schreibgeschützt</string>
<string name="disk_insert_could_not_read">Entschuldigung, das Diskettenabbild konnte nicht gelesen werden!</string>
<string name="disk_read_only">Schreibgeschützt</string>
<string name="disk_read_write">Lesen/Schreiben</string>
<string name="disk_show_operation">Zeige Disk ][ Aktivität</string>

View File

@ -32,8 +32,6 @@
<string name="diskA">Disquetera 1</string>
<string name="diskB">Disquetera 2</string>
<string name="disk_eject">Eyectar</string>
<string name="disk_insert_toast">Disco insertado en la disquetera de sólo lectura</string>
<string name="disk_insert_could_not_read">Lo sentimos, no se puede leer la imagen de disquete!</string>
<string name="disk_read_only">Sólo leer</string>
<string name="disk_read_write">Leer y escribir</string>
<string name="disk_show_operation">Mostrar las operaciones de "Disk ]["</string>

View File

@ -32,8 +32,6 @@
<string name="diskA">Lecteur 1</string>
<string name="diskB">Lecteur 2</string>
<string name="disk_eject">Ejecter</string>
<string name="disk_insert_toast">Insérer la disquette dans le drive en lecture seulement</string>
<string name="disk_insert_could_not_read">Désolé, impossible de lire l\'image disque!</string>
<string name="disk_read_only">Lecture seulement</string>
<string name="disk_read_write">Lecture/Ecriture</string>
<string name="disk_show_operation">Afficher les opérations (disque) ][</string>

View File

@ -36,8 +36,7 @@
<string name="diskA">Drive 1</string>
<string name="diskB">Drive 2</string>
<string name="disk_eject">Eject</string>
<string name="disk_insert_toast">Inserted disk in drive read-only</string>
<string name="disk_insert_could_not_read">Sorry, could not read the disk image!</string>
<string name="disk_insert_toast_cannot">Cannot insert (not a disk image or state file)</string>
<string name="disk_read_only">Read only</string>
<string name="disk_read_write">Read/write</string>
<string name="disk_selection_newschoool">Use system file chooser</string>
@ -130,8 +129,8 @@
<string name="keypad_preset_left_right_space">&#8592;,&#8594;, tap spacebar</string>
<string name="keypad_preset_wadx_space">W,A,D,X, tap spacebar</string>
<string name="keypad_repeat_summary">Key repeat threshold in secs</string>
<string name="menu_disks">Load disk image…</string>
<string name="menu_disks_summary">Insert a Disk ][ image file</string>
<string name="menu_disks">Load image or state file…</string>
<string name="menu_disks_summary">Insert Disk ][ image or state file</string>
<string name="menu_settings">Emulator settings…</string>
<string name="menu_settings_summary">General settings, joystick, keyboard</string>
<string name="mockingboard_disabled_title">Mockingboard disabled</string>
@ -165,7 +164,7 @@
<string name="save">Quick save</string>
<string name="saverestore">Save &amp; restore…</string>
<string name="saverestore_choice">Save current state or restore previous?</string>
<string name="saverestore_summary">Quick save and restore</string>
<string name="saverestore_summary">Save and restore emulator state</string>
<string name="skip">Skip&#8594;</string>
<string name="speaker_volume">Speaker volume</string>
<string name="speaker_volume_summary">Set the speaker volume</string>

View File

@ -136,7 +136,7 @@ TEST test_save_state_1() {
_assert_blank_boot();
char *savData = NULL;
ASPRINTF(&savData, "%s/emulator-test.state", HOMEDIR);
ASPRINTF(&savData, "%s/emulator-test.a2state", HOMEDIR);
bool ret = emulator_saveState(savData);
ASSERT(ret);
@ -155,7 +155,7 @@ TEST test_load_state_1() {
c_debugger_set_timeout(0);
char *savData = NULL;
ASPRINTF(&savData, "%s/emulator-test.state", HOMEDIR);
ASPRINTF(&savData, "%s/emulator-test.a2state", HOMEDIR);
bool ret = false;
int fdA = -1;