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;