diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml index a6980293..f194158a 100644 --- a/Android/app/src/main/AndroidManifest.xml +++ b/Android/app/src/main/AndroidManifest.xml @@ -50,6 +50,18 @@ + + + + + + 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 d58da816..ce6616ca 100644 --- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java @@ -14,9 +14,7 @@ package org.deadc0de.apple2ix; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; -import android.content.Intent; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; @@ -25,19 +23,14 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.Toast; -import java.io.File; import java.util.ArrayList; import java.util.Collections; -import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import org.deadc0de.apple2ix.basic.BuildConfig; -import org.deadc0de.apple2ix.basic.R; -public class Apple2Activity extends Activity /*implements Apple2DiskChooserActivity.Callback*/ { +public class Apple2Activity extends Activity implements Apple2DiskChooserActivity.Callback { private final static String TAG = "Apple2Activity"; @@ -55,7 +48,7 @@ public class Apple2Activity extends Activity /*implements Apple2DiskChooserActiv private AtomicBoolean mPausing = new AtomicBoolean(false); private AtomicBoolean mSwitchingToPortrait = new AtomicBoolean(false); - private static String sDiskPathChosen = null; + private static DiskArgs sDisksChosen = null; // non-null if we failed to load/link the native code ... likely we are running on some bizarre 'droid variant private static Throwable sNativeBarfedThrowable = null; @@ -143,8 +136,8 @@ public class Apple2Activity extends Activity /*implements Apple2DiskChooserActiv boolean switchingToPortrait = Apple2VideoSettingsMenu.SETTINGS.applyLandscapeMode(this); Apple2Preferences.sync(this, null); - Apple2DisksMenu.insertDisk((String) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A), /*driveA:*/true, /*isReadOnly:*/(boolean) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_RO), /*onLaunch:*/true); - Apple2DisksMenu.insertDisk((String) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B), /*driveA:*/false, /*isReadOnly:*/(boolean) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_RO), /*onLaunch:*/true); + Apple2DisksMenu.insertDisk(this, new DiskArgs((String) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A)), /*driveA:*/true, /*isReadOnly:*/(boolean) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_A_RO), /*onLaunch:*/true); + Apple2DisksMenu.insertDisk(this, new DiskArgs((String) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B)), /*driveA:*/false, /*isReadOnly:*/(boolean) Apple2Preferences.getJSONPref(Apple2DisksMenu.SETTINGS.CURRENT_DISK_PATH_B_RO), /*onLaunch:*/true); showSplashScreen(!firstTime); @@ -193,24 +186,12 @@ public class Apple2Activity extends Activity /*implements Apple2DiskChooserActiv mSettingsMenu = new Apple2SettingsMenu(this); mDisksMenu = new Apple2DisksMenu(this); - - Intent intent = getIntent(); - String path = null; - if (intent != null) { - Uri data = intent.getData(); - if (data != null) { - path = data.getPath(); - } - } - if (path != null && Apple2DisksMenu.hasDiskExtension(path)) { - sDiskPathChosen = path; - } } - /*@Override - public void onDisksChosen(String path) { - sDiskPathChosen = path; - }*/ + @Override + public void onDisksChosen(DiskArgs args) { + sDisksChosen = args; + } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { @@ -246,9 +227,9 @@ public class Apple2Activity extends Activity /*implements Apple2DiskChooserActiv Apple2CrashHandler.getInstance().checkForCrashes(this); // NOTE : needs to be called again to clean-up } - if (sDiskPathChosen != null && mDisksMenu != null) { - mDisksMenu.showDiskInsertionAlertDialog("title", sDiskPathChosen); - sDiskPathChosen = null; + if (sDisksChosen != null && mDisksMenu != null) { + mDisksMenu.showDiskInsertionAlertDialog("title", sDisksChosen); + sDisksChosen = null; } } diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DiskChooserActivity.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DiskChooserActivity.java new file mode 100644 index 00000000..0b2b633a --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DiskChooserActivity.java @@ -0,0 +1,186 @@ +/* + * Apple // emulator for *nix + * + * This software package is subject to the GNU General Public License + * version 3 or later (your choice) as published by the Free Software + * Foundation. + * + * Copyright 2017 Aaron Culliney + * + */ + +package org.deadc0de.apple2ix; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class Apple2DiskChooserActivity extends Activity { + + public static final AtomicBoolean sDiskChooserIsChoosing = new AtomicBoolean(false); + public static Callback sDisksCallback; + + public interface Callback { + void onDisksChosen(DiskArgs args); + } + + @Nullable + public static ParcelFileDescriptor openFileDescriptorFromUri(Context ctx, Uri uri) { + + ParcelFileDescriptor pfd = null; + + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + throw new RuntimeException("SDK Version not allowed access"); + } + + if (!DocumentsContract.isDocumentUri(ctx, uri)) { + throw new RuntimeException("Not a Document URI for " + uri); + } + + ContentResolver resolver = ctx.getContentResolver(); + resolver.takePersistableUriPermission(uri, (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); + pfd = resolver.openFileDescriptor(uri, "rw"); + } catch (Throwable t) { + Log.e(TAG, "OOPS, could not get appropriate access to URI ( " + uri + " ) : " + t); + } + + return pfd; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putBoolean("ran", true); + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + Bundle b; + { + Intent intent = getIntent(); + Bundle extras = null; + if (intent != null) { + extras = intent.getExtras(); + } + + if (savedState != null) { + b = savedState; + } else if (extras != null) { + b = extras; + } else { + b = new Bundle(); + } + } + + boolean ran = b.getBoolean("ran"); + if (ran) { + Log.e(TAG, "OOPS, already ran..."); + finish(); + return; + } + + ////Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT); + Intent pickIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + + try { + pickIntent.setType("*/*"); + pickIntent.addCategory(Intent.CATEGORY_OPENABLE); + + /* FIXME TODO : currently we don't have decent UI/UX for multi-select ... + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + }*/ + + if (!ran) { + startActivityForResult(pickIntent, EDIT_REQUEST_CODE); + } + } catch (Throwable t) { + Log.e(TAG, "OOPS : " + t); + setResult(RESULT_CANCELED); + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode != RESULT_CANCELED) { + + /* FIXME TODO : currently we don't have decent UI/UX for multi-select ... + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + + ClipData clipData = null; + if (data != null) { + clipData = data.getClipData(); + } + + if (clipData != null && clipData.getItemCount() > 1) { + uris = new Uri[2]; + uris[0] = clipData.getItemAt(0).getUri(); + uris[1] = clipData.getItemAt(1).getUri(); + } + }*/ + + if (chosenUri == null) { + if (data != null) { + chosenUri = data.getData(); + } + } + + if (chosenUri != null) { + chosenPfd = openFileDescriptorFromUri(this, chosenUri); + } + } + + setResult(RESULT_OK); + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + public void finish() { + sDiskChooserIsChoosing.set(false); + sDisksCallback.onDisksChosen(new DiskArgs(chosenUri, chosenPfd)); + super.finish(); + } + + private Uri chosenUri; + + private ParcelFileDescriptor chosenPfd; + + private static final String TAG = "A2DiskChooserActivity"; + + private static final int EDIT_REQUEST_CODE = 44; +} + +class DiskArgs { + public String path; + public Uri uri; + public ParcelFileDescriptor pfd; + + public DiskArgs(Uri uri, ParcelFileDescriptor pfd) { + this.uri = uri; + this.pfd = pfd; + } + + public DiskArgs(String path) { + this.path = path; + } +} 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 c50197aa..a2ff5fbd 100644 --- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java @@ -16,6 +16,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Build; import android.util.Log; import android.view.LayoutInflater; @@ -35,9 +36,9 @@ import org.json.JSONException; import java.io.File; import java.io.FilenameFilter; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; import org.deadc0de.apple2ix.basic.R; import org.json.JSONObject; @@ -46,7 +47,7 @@ public class Apple2DisksMenu implements Apple2MenuView { private final static String TAG = "Apple2DisksMenu"; - private final static String EXTERNAL_CHOOSER_SENTINEL = "apple2ix://"; + public final static String EXTERNAL_CHOOSER_SENTINEL = "apple2ix://"; private Apple2Activity mActivity = null; private View mDisksView = null; @@ -282,15 +283,29 @@ public class Apple2DisksMenu implements Apple2MenuView { return path; } - public static void insertDisk(String imageName, boolean isDriveA, boolean isReadOnly, boolean onLaunch) { + public static void insertDisk(Apple2Activity activity, DiskArgs diskArgs, boolean isDriveA, boolean isReadOnly, boolean onLaunch) { try { JSONObject map = new JSONObject(); ejectDisk(isDriveA); - if (imageName.startsWith("file://")) { - ////int fd = Apple2DiskChooserActivity.openFileDescriptor(imageName, isReadOnly); - ////map.put("fd", fd); + String imageName = diskArgs.path; + + if (imageName == null) { + imageName = EXTERNAL_CHOOSER_SENTINEL + diskArgs.uri.toString(); + } + + if (imageName.startsWith(EXTERNAL_CHOOSER_SENTINEL)) { + if (diskArgs.pfd == null) { + if (diskArgs.uri == null) { + String uriString = imageName.substring(EXTERNAL_CHOOSER_SENTINEL.length()); + diskArgs.uri = Uri.parse(uriString); + } + diskArgs.pfd = Apple2DiskChooserActivity.openFileDescriptorFromUri(activity, diskArgs.uri); + } + + int fd = diskArgs.pfd.getFd(); + map.put("fd", fd); } else { File file = new File(imageName); if (!file.exists()) { @@ -316,6 +331,15 @@ public class Apple2DisksMenu implements Apple2MenuView { String jsonString = nativeChooseDisk(map.toString()); + if (diskArgs.pfd != null) { + try { + diskArgs.pfd.close(); + } catch (IOException ioe) { + Log.e(TAG, "Error attempting to close PFD : " + ioe); + } + diskArgs.pfd = null; + } + map = new JSONObject(jsonString); boolean inserted = map.getBoolean("inserted"); if (inserted) { @@ -339,7 +363,7 @@ public class Apple2DisksMenu implements Apple2MenuView { nativeEjectDisk(isDriveA); } - public void showDiskInsertionAlertDialog(String title, final String imagePath) { + public void showDiskInsertionAlertDialog(String title, final DiskArgs diskArgs) { title = mActivity.getResources().getString(R.string.header_disks) + " " + title; @@ -385,7 +409,7 @@ public class Apple2DisksMenu implements Apple2MenuView { boolean isDriveA = driveA.isChecked(); boolean diskReadOnly = readOnly.isChecked(); - insertDisk(imagePath, isDriveA, diskReadOnly, /*onLaunch:*/false); + insertDisk(mActivity, diskArgs, isDriveA, diskReadOnly, /*onLaunch:*/false); dialog.dismiss(); mActivity.dismissAllMenus(); @@ -613,43 +637,18 @@ public class Apple2DisksMenu implements Apple2MenuView { public void onItemClick(AdapterView parent, View view, final int position, long id) { if (isDirectory[position]) { if (filePaths[position].equals(EXTERNAL_CHOOSER_SENTINEL)) { - /* final boolean alreadyChoosing = Apple2DiskChooserActivity.sDiskChooserIsChoosing.getAndSet(true); if (alreadyChoosing) { return; } - - ///* - final String packageName = "org.deadc0de.apple2ix.basic"; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mActivity.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - mActivity.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } - //* / - Intent chooserIntent = new Intent(mActivity, Apple2DiskChooserActivity.class); + chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION/* | Intent.FLAG_ACTIVITY_CLEAR_TOP */); - - - ///* - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } - //* / - - //chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION/* | Intent.FLAG_ACTIVITY_CLEAR_TOP * /); - - chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION/* | Intent.FLAG_ACTIVITY_CLEAR_TOP * /); - - //chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION/* | Intent.FLAG_ACTIVITY_CLEAR_TOP * /); Apple2DiskChooserActivity.sDisksCallback = mActivity; mActivity.startActivity(chooserIntent); + return; - */ } Log.d(TAG, "Descending to path : " + filePaths[position]); @@ -676,7 +675,7 @@ public class Apple2DisksMenu implements Apple2MenuView { return; } - showDiskInsertionAlertDialog(/*title:*/fileNames[position], /*diskPath:*/imageName); + showDiskInsertionAlertDialog(/*title:*/fileNames[position], /*diskPath:*/new DiskArgs(imageName)); } }); } 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 4652f32d..4489f9af 100644 --- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java @@ -25,7 +25,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupWindow; @@ -33,15 +32,10 @@ import android.widget.RadioButton; import android.widget.TextView; import org.deadc0de.apple2ix.basic.R; -import org.json.JSONException; import org.json.JSONObject; import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.net.URI; -import java.net.URL; +import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; public class Apple2MainMenu { @@ -349,6 +343,8 @@ public class Apple2MainMenu { 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]); @@ -360,9 +356,15 @@ public class Apple2MainMenu { continue; } - if (diskPath.startsWith("file://")) { - ////int fd = Apple2DiskChooserActivity.openFileDescriptor(diskPath, /*isReadOnly:*/readOnly); - ////map.put(fdKeys[i], fd); + 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); + int fd = pfds[i].getFd(); + + map.put(fdKeys[i], fd); } else { boolean exists = new File(diskPath).exists(); if (!exists) { @@ -372,6 +374,16 @@ public class Apple2MainMenu { } 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); {