diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java
index b99e5840..9d3b9f31 100644
--- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java
+++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java
@@ -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 {
diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java
index 1d5fef39..49a25b49 100644
--- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java
+++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java
@@ -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 ...
diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java
index d90e9844..aa5e052f 100644
--- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java
+++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java
@@ -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();
diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java
index 4bd0ab95..afad4c94 100644
--- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java
+++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java
@@ -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+
diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Utils.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Utils.java
index 449598f9..8db699b6 100644
--- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Utils.java
+++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Utils.java
@@ -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];
diff --git a/Android/app/src/main/res/values-de/strings.xml b/Android/app/src/main/res/values-de/strings.xml
index 752ce92e..e42b11bd 100644
--- a/Android/app/src/main/res/values-de/strings.xml
+++ b/Android/app/src/main/res/values-de/strings.xml
@@ -32,8 +32,6 @@
Laufwerk 1
Laufwerk 2
Auswerfen
- Die eingelegte Diskette ist schreibgeschützt
- Entschuldigung, das Diskettenabbild konnte nicht gelesen werden!
Schreibgeschützt
Lesen/Schreiben
Zeige Disk ][ Aktivität
diff --git a/Android/app/src/main/res/values-es/strings.xml b/Android/app/src/main/res/values-es/strings.xml
index 9bfe6c62..75241d5d 100644
--- a/Android/app/src/main/res/values-es/strings.xml
+++ b/Android/app/src/main/res/values-es/strings.xml
@@ -32,8 +32,6 @@
Disquetera 1
Disquetera 2
Eyectar
- Disco insertado en la disquetera de sólo lectura
- Lo sentimos, no se puede leer la imagen de disquete!
Sólo leer
Leer y escribir
Mostrar las operaciones de "Disk ]["
diff --git a/Android/app/src/main/res/values-fr/strings.xml b/Android/app/src/main/res/values-fr/strings.xml
index b8bf3094..cf0a23be 100644
--- a/Android/app/src/main/res/values-fr/strings.xml
+++ b/Android/app/src/main/res/values-fr/strings.xml
@@ -32,8 +32,6 @@
Lecteur 1
Lecteur 2
Ejecter
- Insérer la disquette dans le drive en lecture seulement
- Désolé, impossible de lire l\'image disque!
Lecture seulement
Lecture/Ecriture
Afficher les opérations (disque) ][
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
index 0f6c6520..cbac6fec 100644
--- a/Android/app/src/main/res/values/strings.xml
+++ b/Android/app/src/main/res/values/strings.xml
@@ -36,8 +36,7 @@
Drive 1
Drive 2
Eject
- Inserted disk in drive read-only
- Sorry, could not read the disk image!
+ Cannot insert (not a disk image or state file)
Read only
Read/write
Use system file chooser
@@ -130,8 +129,8 @@
←,→, tap spacebar
W,A,D,X, tap spacebar
Key repeat threshold in secs
- Load disk image…
- Insert a Disk ][ image file
+ Load image or state file…
+ Insert Disk ][ image or state file
Emulator settings…
General settings, joystick, keyboard
Mockingboard disabled
@@ -165,7 +164,7 @@
Quick save
Save & restore…
Save current state or restore previous?
- Quick save and restore
+ Save and restore emulator state
Skip→
Speaker volume
Set the speaker volume
diff --git a/src/test/testui.c b/src/test/testui.c
index 1bad15ae..1248d529 100644
--- a/src/test/testui.c
+++ b/src/test/testui.c
@@ -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;