591 lines
22 KiB
Java
591 lines
22 KiB
Java
/*
|
|
* 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 2015 Aaron Culliney
|
|
*
|
|
*/
|
|
|
|
package org.deadc0de.apple2ix;
|
|
|
|
import android.app.AlertDialog;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.AssetManager;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Environment;
|
|
import android.util.Log;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.AdapterView;
|
|
import android.widget.ArrayAdapter;
|
|
import android.widget.Button;
|
|
import android.widget.CompoundButton;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ListView;
|
|
import android.widget.RadioButton;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InterruptedIOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
|
|
import org.deadc0de.apple2ix.basic.R;
|
|
|
|
public class Apple2DisksMenu implements Apple2MenuView {
|
|
|
|
private final static String TAG = "Apple2DisksMenu";
|
|
private static String sDataDir = null;
|
|
|
|
private Apple2Activity mActivity = null;
|
|
private View mDisksView = null;
|
|
|
|
private final ArrayList<String> mPathStack = new ArrayList<String>();
|
|
|
|
private static File sExternalFilesDir = null;
|
|
private static boolean sInitializedPath = false;
|
|
|
|
public Apple2DisksMenu(Apple2Activity activity) {
|
|
mActivity = activity;
|
|
|
|
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
mDisksView = inflater.inflate(R.layout.activity_disks, null, false);
|
|
|
|
final Button cancelButton = (Button) mDisksView.findViewById(R.id.cancelButton);
|
|
cancelButton.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
Apple2DisksMenu.this.mActivity.dismissAllMenus();
|
|
}
|
|
});
|
|
|
|
getExternalStorageDirectory();
|
|
}
|
|
|
|
public static File getExternalStorageDirectory() {
|
|
if (sExternalFilesDir == null) {
|
|
String storageState = Environment.getExternalStorageState();
|
|
File externalDir = new File(Environment.getExternalStorageDirectory(), "apple2ix"); // /sdcard/apple2ix
|
|
sExternalFilesDir = null;
|
|
boolean externalStorageAvailable = storageState.equals(Environment.MEDIA_MOUNTED);
|
|
if (externalStorageAvailable) {
|
|
sExternalFilesDir = externalDir;
|
|
boolean made = sExternalFilesDir.mkdirs();
|
|
if (!made) {
|
|
Log.d(TAG, "WARNING: could not make directory : " + sExternalFilesDir);
|
|
}
|
|
} else {
|
|
sExternalFilesDir = externalDir;
|
|
}
|
|
}
|
|
return sExternalFilesDir;
|
|
}
|
|
|
|
// HACK NOTE 2015/02/22 : Apparently native code cannot easily access stuff in the APK ... so copy various resources
|
|
// out of the APK and into the /data/data/... for ease of access. Because this is FOSS software we don't care about
|
|
// security or DRM for these assets =)
|
|
public static String getDataDir(Apple2Activity activity) {
|
|
|
|
if (sDataDir != null) {
|
|
return sDataDir;
|
|
}
|
|
|
|
try {
|
|
PackageManager pm = activity.getPackageManager();
|
|
PackageInfo pi = pm.getPackageInfo(activity.getPackageName(), 0);
|
|
sDataDir = pi.applicationInfo.dataDir;
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
Log.e(TAG, "" + e);
|
|
if (sDataDir == null) {
|
|
sDataDir = "/data/local/tmp";
|
|
}
|
|
}
|
|
|
|
return sDataDir;
|
|
}
|
|
|
|
public static void firstTime(Apple2Activity activity) {
|
|
getDataDir(activity);
|
|
|
|
Log.d(TAG, "First time copying stuff-n-things out of APK for ease-of-NDK access...");
|
|
|
|
getExternalStorageDirectory();
|
|
|
|
recursivelyCopyAPKAssets(activity, /*from APK directory:*/"disks", /*to location:*/new File(sDataDir, "disks").getAbsolutePath());
|
|
recursivelyCopyAPKAssets(activity, /*from APK directory:*/"keyboards", /*to location:*/new File(sDataDir, "keyboards").getAbsolutePath());
|
|
recursivelyCopyAPKAssets(activity, /*from APK directory:*/"shaders", /*to location:*/new File(sDataDir, "shaders").getAbsolutePath());
|
|
|
|
// expose keyboards to modding
|
|
recursivelyCopyAPKAssets(activity, /*from APK directory:*/"keyboards", /*to location:*/sExternalFilesDir.getAbsolutePath());
|
|
}
|
|
|
|
public static void exposeSymbols(Apple2Activity activity) {
|
|
recursivelyCopyAPKAssets(activity, /*from APK directory:*/"symbols", /*to location:*/new File(sDataDir, "symbols").getAbsolutePath());
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Apple2MenuView interface methods
|
|
|
|
public final boolean isCalibrating() {
|
|
return false;
|
|
}
|
|
|
|
public void onKeyTapCalibrationEvent(char ascii, int scancode) {
|
|
/* ... */
|
|
}
|
|
|
|
public void show() {
|
|
if (isShowing()) {
|
|
return;
|
|
}
|
|
if (!sInitializedPath) {
|
|
sInitializedPath = true;
|
|
Apple2Preferences.CURRENT_DISK_PATH.load(mActivity);
|
|
}
|
|
dynamicSetup();
|
|
mActivity.pushApple2View(this);
|
|
}
|
|
|
|
public void dismiss() {
|
|
String path = popPathStack();
|
|
if (path == null) {
|
|
mActivity.popApple2View(this);
|
|
} else {
|
|
dynamicSetup();
|
|
ListView disksList = (ListView) mDisksView.findViewById(R.id.listView_settings);
|
|
disksList.postInvalidate();
|
|
}
|
|
}
|
|
|
|
public void dismissAll() {
|
|
mActivity.popApple2View(this);
|
|
}
|
|
|
|
public boolean isShowing() {
|
|
return mDisksView.isShown();
|
|
}
|
|
|
|
public View getView() {
|
|
return mDisksView;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// path stack methods
|
|
|
|
public String getPathStackJSON() {
|
|
JSONArray jsonArray = new JSONArray(Arrays.asList(mPathStack.toArray()));
|
|
return jsonArray.toString();
|
|
}
|
|
|
|
public void setPathStackJSON(String pathStackJSON) {
|
|
mPathStack.clear();
|
|
try {
|
|
JSONArray jsonArray = new JSONArray(pathStackJSON);
|
|
for (int i = 0, count = jsonArray.length(); i < count; i++) {
|
|
String pathComponent = jsonArray.getString(i);
|
|
mPathStack.add(pathComponent);
|
|
}
|
|
} catch (JSONException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
public void pushPathStack(String path) {
|
|
mPathStack.add(path);
|
|
Apple2Preferences.CURRENT_DISK_PATH.saveString(mActivity, getPathStackJSON());
|
|
}
|
|
|
|
public String popPathStack() {
|
|
if (mPathStack.size() == 0) {
|
|
return null;
|
|
}
|
|
String path = mPathStack.remove(mPathStack.size() - 1);
|
|
Apple2Preferences.CURRENT_DISK_PATH.saveString(mActivity, getPathStackJSON());
|
|
return path;
|
|
}
|
|
|
|
public static boolean hasDiskExtension(String name) {
|
|
|
|
// check file extensions ... sigh ... no String.endsWithIgnoreCase() ?
|
|
|
|
final int len = name.length();
|
|
if (len <= 3) {
|
|
return false;
|
|
}
|
|
|
|
String suffix;
|
|
suffix = name.substring(len - 3, len);
|
|
if (suffix.equalsIgnoreCase(".do") || suffix.equalsIgnoreCase(".po")) {
|
|
return true;
|
|
}
|
|
|
|
if (len <= 4) {
|
|
return false;
|
|
}
|
|
|
|
suffix = name.substring(len - 4, len);
|
|
if (suffix.equalsIgnoreCase(".dsk") || suffix.equalsIgnoreCase(".nib")) {
|
|
return true;
|
|
}
|
|
|
|
if (len <= 6) {
|
|
return false;
|
|
}
|
|
|
|
suffix = name.substring(len - 6, len);
|
|
if (suffix.equalsIgnoreCase(".do.gz") || suffix.equalsIgnoreCase(".po.gz")) {
|
|
return true;
|
|
}
|
|
|
|
if (len <= 7) {
|
|
return false;
|
|
}
|
|
|
|
suffix = name.substring(len - 7, len);
|
|
return (suffix.equalsIgnoreCase(".dsk.gz") || suffix.equalsIgnoreCase(".nib.gz"));
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// internals ...
|
|
|
|
private String pathStackAsDirectory() {
|
|
if (mPathStack.size() == 0) {
|
|
return null;
|
|
}
|
|
StringBuilder pathBuffer = new StringBuilder();
|
|
for (String component : mPathStack) {
|
|
pathBuffer.append(component);
|
|
pathBuffer.append(File.separator);
|
|
}
|
|
return pathBuffer.toString();
|
|
}
|
|
|
|
private static void recursivelyCopyAPKAssets(Apple2Activity activity, String srcFileOrDir, String dstFileOrDir) {
|
|
AssetManager assetManager = activity.getAssets();
|
|
|
|
final int maxAttempts = 5;
|
|
String[] files = null;
|
|
int attempts = 0;
|
|
do {
|
|
try {
|
|
files = assetManager.list(srcFileOrDir);
|
|
break;
|
|
} catch (InterruptedIOException e) {
|
|
/* EINTR, EAGAIN ... */
|
|
} catch (IOException e) {
|
|
Log.d(TAG, "OOPS exception attempting to list APK files at : " + srcFileOrDir + " : " + e);
|
|
}
|
|
|
|
try {
|
|
Thread.sleep(100, 0);
|
|
} catch (InterruptedException ie) {
|
|
/* ... */
|
|
}
|
|
++attempts;
|
|
} while (attempts < maxAttempts);
|
|
|
|
if (files == null) {
|
|
Log.d(TAG, "OOPS, could not list APK assets at : " + srcFileOrDir);
|
|
return;
|
|
}
|
|
|
|
if (files.length > 0) {
|
|
// ensure destination directory exists
|
|
File dstPath = new File(dstFileOrDir);
|
|
if (!dstPath.mkdirs()) {
|
|
if (!dstPath.exists()) {
|
|
Log.d(TAG, "OOPS, could not mkdirs on " + dstPath);
|
|
return;
|
|
}
|
|
}
|
|
for (String filename : files) {
|
|
// iterate on files and subdirectories
|
|
recursivelyCopyAPKAssets(activity, srcFileOrDir + File.separator + filename, dstFileOrDir + File.separator + filename);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// presumably this is a file, not a subdirectory
|
|
InputStream is = null;
|
|
FileOutputStream os = null;
|
|
attempts = 0;
|
|
do {
|
|
try {
|
|
is = assetManager.open(srcFileOrDir);
|
|
os = new FileOutputStream(dstFileOrDir);
|
|
copyFile(is, os);
|
|
break;
|
|
} catch (InterruptedIOException e) {
|
|
/* EINTR, EAGAIN */
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "Failed to copy asset file: " + srcFileOrDir, e);
|
|
} finally {
|
|
if (is != null) {
|
|
try {
|
|
is.close();
|
|
} catch (IOException e) {
|
|
// NOOP
|
|
}
|
|
}
|
|
if (os != null) {
|
|
try {
|
|
os.close();
|
|
} catch (IOException e) {
|
|
// NOOP
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
Thread.sleep(100, 0);
|
|
} catch (InterruptedException ie) {
|
|
/* ... */
|
|
}
|
|
++attempts;
|
|
} while (attempts < maxAttempts);
|
|
}
|
|
|
|
private static void copyFile(InputStream is, FileOutputStream os) throws IOException {
|
|
final int BUF_SZ = 4096;
|
|
byte[] buf = new byte[BUF_SZ];
|
|
while (true) {
|
|
int len = is.read(buf, 0, BUF_SZ);
|
|
if (len < 0) {
|
|
break;
|
|
}
|
|
os.write(buf, 0, len);
|
|
}
|
|
os.flush();
|
|
}
|
|
|
|
private void dynamicSetup() {
|
|
|
|
final ListView disksList = (ListView) mDisksView.findViewById(R.id.listView_settings);
|
|
disksList.setEnabled(true);
|
|
|
|
String disksDir = pathStackAsDirectory();
|
|
boolean isRootPath = false;
|
|
if (disksDir == null) {
|
|
isRootPath = true;
|
|
disksDir = sDataDir + File.separator + "disks"; // default path
|
|
}
|
|
|
|
File dir = new File(disksDir);
|
|
|
|
final File[] files = dir.listFiles(new FilenameFilter() {
|
|
public boolean accept(File dir, String name) {
|
|
name = name.toLowerCase();
|
|
if (name.equals(".")) {
|
|
return false;
|
|
}
|
|
if (name.equals("..")) {
|
|
return false;
|
|
}
|
|
File file = new File(dir, name);
|
|
return file.isDirectory() || hasDiskExtension(name);
|
|
}
|
|
});
|
|
|
|
// This appears to happen in cases where the external files directory String is valid, but is not actually mounted
|
|
// We could probably check for more media "states" in the setup above ... but this defensive coding probably should
|
|
// remain here after any refactoring =)
|
|
if (files == null) {
|
|
dismiss();
|
|
return;
|
|
}
|
|
|
|
Arrays.sort(files);
|
|
|
|
getExternalStorageDirectory();
|
|
final boolean includeExternalStoragePath = (sExternalFilesDir != null && isRootPath);
|
|
final int offset = includeExternalStoragePath ? 1 : 0;
|
|
final String[] fileNames = new String[files.length + offset];
|
|
final boolean[] isDirectory = new boolean[files.length + offset];
|
|
|
|
if (includeExternalStoragePath) {
|
|
fileNames[0] = sExternalFilesDir.getPath();
|
|
isDirectory[0] = true;
|
|
}
|
|
|
|
int idx = offset;
|
|
for (File file : files) {
|
|
isDirectory[idx] = file.isDirectory();
|
|
fileNames[idx] = file.getName();
|
|
if (isDirectory[idx]) {
|
|
fileNames[idx] += File.separator;
|
|
}
|
|
++idx;
|
|
}
|
|
|
|
ArrayAdapter<?> adapter = new ArrayAdapter<String>(mActivity, R.layout.a2disk, R.id.a2disk_title, fileNames) {
|
|
@Override
|
|
public boolean areAllItemsEnabled() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public View getView(int position, View convertView, ViewGroup parent) {
|
|
View view = super.getView(position, convertView, parent);
|
|
|
|
LinearLayout layout = (LinearLayout) view.findViewById(R.id.a2disk_widget_frame);
|
|
if (layout.getChildCount() > 0) {
|
|
// layout cells appear to be reused when scrolling into view ... make sure we start with clear hierarchy
|
|
layout.removeAllViews();
|
|
}
|
|
|
|
if (isDirectory[position]) {
|
|
ImageView imageView = new ImageView(mActivity);
|
|
Drawable drawable = mActivity.getResources().getDrawable(android.R.drawable.ic_menu_more);
|
|
imageView.setImageDrawable(drawable);
|
|
layout.addView(imageView);
|
|
} else {
|
|
|
|
String imageName = files[position - offset].getAbsolutePath();
|
|
final int len = imageName.length();
|
|
final String suffix = imageName.substring(len - 3, len);
|
|
if (suffix.equalsIgnoreCase(".gz")) {
|
|
imageName = files[position - offset].getAbsolutePath().substring(0, len - 3);
|
|
}
|
|
|
|
String eject = mActivity.getResources().getString(R.string.disk_eject);
|
|
if (imageName.equals(Apple2Preferences.CURRENT_DISK_A.stringValue(mActivity))) {
|
|
Button ejectButton = new Button(mActivity);
|
|
ejectButton.setText(eject + " 1");
|
|
ejectButton.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
mActivity.nativeEjectDisk(/*driveA:*/true);
|
|
Apple2Preferences.CURRENT_DISK_A.saveString(mActivity, "");
|
|
dynamicSetup();
|
|
}
|
|
});
|
|
layout.addView(ejectButton);
|
|
} else if (imageName.equals(Apple2Preferences.CURRENT_DISK_B.stringValue(mActivity))) {
|
|
Button ejectButton = new Button(mActivity);
|
|
ejectButton.setText(eject + " 2");
|
|
ejectButton.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
mActivity.nativeEjectDisk(/*driveA:*/false);
|
|
Apple2Preferences.CURRENT_DISK_B.saveString(mActivity, "");
|
|
dynamicSetup();
|
|
}
|
|
});
|
|
layout.addView(ejectButton);
|
|
}
|
|
|
|
}
|
|
return view;
|
|
}
|
|
};
|
|
|
|
disksList.setAdapter(adapter);
|
|
disksList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
|
@Override
|
|
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
|
|
if (isDirectory[position]) {
|
|
Log.d(TAG, "Descending to path : " + fileNames[position]);
|
|
pushPathStack(fileNames[position]);
|
|
dynamicSetup();
|
|
ListView disksList = (ListView) mDisksView.findViewById(R.id.listView_settings);
|
|
disksList.postInvalidate();
|
|
return;
|
|
}
|
|
|
|
String str = files[position - offset].getAbsolutePath();
|
|
final int len = str.length();
|
|
final String suffix = str.substring(len - 3, len);
|
|
if (suffix.equalsIgnoreCase(".gz")) {
|
|
str = files[position - offset].getAbsolutePath().substring(0, len - 3);
|
|
}
|
|
final String imageName = str;
|
|
|
|
if (imageName.equals(Apple2Preferences.CURRENT_DISK_A.stringValue(mActivity))) {
|
|
mActivity.nativeEjectDisk(/*driveA:*/true);
|
|
Apple2Preferences.CURRENT_DISK_A.saveString(mActivity, "");
|
|
dynamicSetup();
|
|
return;
|
|
}
|
|
if (imageName.equals(Apple2Preferences.CURRENT_DISK_B.stringValue(mActivity))) {
|
|
mActivity.nativeEjectDisk(/*driveA:*/false);
|
|
Apple2Preferences.CURRENT_DISK_B.saveString(mActivity, "");
|
|
dynamicSetup();
|
|
return;
|
|
|
|
}
|
|
|
|
String title = mActivity.getResources().getString(R.string.header_disks);
|
|
title = title + " " + fileNames[position];
|
|
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity).setCancelable(true).setMessage(title);
|
|
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
final View diskConfirmationView = inflater.inflate(R.layout.a2disk_confirmation, null, false);
|
|
builder.setView(diskConfirmationView);
|
|
|
|
final RadioButton diskA = (RadioButton) diskConfirmationView.findViewById(R.id.radioButton_diskA);
|
|
diskA.setChecked(Apple2Preferences.CURRENT_DRIVE_A_BUTTON.booleanValue(mActivity));
|
|
diskA.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
|
@Override
|
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
Apple2Preferences.CURRENT_DRIVE_A_BUTTON.saveBoolean(mActivity, isChecked);
|
|
}
|
|
});
|
|
final RadioButton diskB = (RadioButton) diskConfirmationView.findViewById(R.id.radioButton_diskB);
|
|
diskB.setChecked(!Apple2Preferences.CURRENT_DRIVE_A_BUTTON.booleanValue(mActivity));
|
|
|
|
|
|
final RadioButton readOnly = (RadioButton) diskConfirmationView.findViewById(R.id.radioButton_readOnly);
|
|
readOnly.setChecked(Apple2Preferences.CURRENT_DISK_RO_BUTTON.booleanValue(mActivity));
|
|
readOnly.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
|
@Override
|
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
Apple2Preferences.CURRENT_DISK_RO_BUTTON.saveBoolean(mActivity, isChecked);
|
|
}
|
|
});
|
|
|
|
final RadioButton readWrite = (RadioButton) diskConfirmationView.findViewById(R.id.radioButton_readWrite);
|
|
readWrite.setChecked(!Apple2Preferences.CURRENT_DISK_RO_BUTTON.booleanValue(mActivity));
|
|
|
|
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
dialog.dismiss();
|
|
}
|
|
});
|
|
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
dialog.dismiss();
|
|
mActivity.dismissAllMenus();
|
|
boolean isDriveA = diskA.isChecked();
|
|
boolean diskReadOnly = readOnly.isChecked();
|
|
if (isDriveA) {
|
|
Apple2Preferences.CURRENT_DISK_A_RO.saveBoolean(mActivity, diskReadOnly);
|
|
Apple2Preferences.CURRENT_DISK_A.saveString(mActivity, imageName);
|
|
} else {
|
|
Apple2Preferences.CURRENT_DISK_B_RO.saveBoolean(mActivity, diskReadOnly);
|
|
Apple2Preferences.CURRENT_DISK_B.saveString(mActivity, imageName);
|
|
}
|
|
}
|
|
});
|
|
|
|
AlertDialog dialog = builder.create();
|
|
mActivity.registerAndShowDialog(dialog);
|
|
}
|
|
});
|
|
}
|
|
}
|