diff --git a/.gitignore b/.gitignore index 2e284b82..d544ae4a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,16 +43,19 @@ ylwrap apple2ix*.tar.gz test-driver +# GDB +.gdb_history + # generated sources src/rom.c src/x86/glue.S -src/meta/debug.c +src/arm/glue.S # sub{tree,module} src/rom # generated binaries -apple2ix +/apple2ix genfont genrom @@ -64,3 +67,22 @@ man6 xcuserdata .DS_Store xcshareddata + +# Android CLI builds +Android/bin +Android/gen +Android/libs +# Android.mk is tEh dynamicz! +Android/jni/Android.mk +*.apk + +# Android Studio +.gradle +Android/local.properties +Android/.idea/workspace.xml +Android/.idea/libraries +Android/.idea/dictionaries +Android/build +Android/jni/obj +Android/obj + diff --git a/Android/.classpath b/Android/.classpath new file mode 100644 index 00000000..7bc01d9a --- /dev/null +++ b/Android/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Android/.idea/.name b/Android/.idea/.name new file mode 100644 index 00000000..1429c13e --- /dev/null +++ b/Android/.idea/.name @@ -0,0 +1 @@ +Android \ No newline at end of file diff --git a/Android/.idea/compiler.xml b/Android/.idea/compiler.xml new file mode 100644 index 00000000..96cc43ef --- /dev/null +++ b/Android/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/.idea/copyright/deadc0de_org.xml b/Android/.idea/copyright/deadc0de_org.xml new file mode 100644 index 00000000..f70821ba --- /dev/null +++ b/Android/.idea/copyright/deadc0de_org.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/Android/.idea/copyright/profiles_settings.xml b/Android/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..e7bedf33 --- /dev/null +++ b/Android/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Android/.idea/gradle.xml b/Android/.idea/gradle.xml new file mode 100644 index 00000000..0833b17c --- /dev/null +++ b/Android/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/Android/.idea/misc.xml b/Android/.idea/misc.xml new file mode 100644 index 00000000..1a3eaffb --- /dev/null +++ b/Android/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/.idea/modules.xml b/Android/.idea/modules.xml new file mode 100644 index 00000000..983383f3 --- /dev/null +++ b/Android/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Android/.idea/runConfigurations.xml b/Android/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/Android/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/Android/.idea/vcs.xml b/Android/.idea/vcs.xml new file mode 100644 index 00000000..6564d52d --- /dev/null +++ b/Android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/Android.iml b/Android/Android.iml new file mode 100644 index 00000000..3a188e20 --- /dev/null +++ b/Android/Android.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/AndroidManifest.xml b/Android/AndroidManifest.xml new file mode 120000 index 00000000..9b975be3 --- /dev/null +++ b/Android/AndroidManifest.xml @@ -0,0 +1 @@ +app/build/intermediates/manifests/full/debug/AndroidManifest.xml \ No newline at end of file diff --git a/Android/app/.gitignore b/Android/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/Android/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Android/app/app.iml b/Android/app/app.iml new file mode 100644 index 00000000..693d732d --- /dev/null +++ b/Android/app/app.iml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/build.gradle b/Android/app/build.gradle new file mode 100644 index 00000000..4693d283 --- /dev/null +++ b/Android/app/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "21.1.2" + signingConfigs { + release { + storeFile file("release2.keystore") + storePassword System.getenv("GOOGSTOREPWD") + keyPassword System.getenv("GOOGKEYPWD") + keyAlias "release" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + debug { + debuggable true + jniDebuggable true + } + } + + defaultConfig { + applicationId "org.deadc0de.apple2ix.basic" + minSdkVersion 10 + targetSdkVersion 23 + versionCode 7 + versionName "1.0.4" + ndk { + moduleName "apple2ix" + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.1.0' +} diff --git a/Android/app/proguard-rules.pro b/Android/app/proguard-rules.pro new file mode 100644 index 00000000..43127a0a --- /dev/null +++ b/Android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/asc/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Android/app/src/androidTest/java/org/deadc0de/apple2ix/ApplicationTest.java b/Android/app/src/androidTest/java/org/deadc0de/apple2ix/ApplicationTest.java new file mode 100644 index 00000000..12bdaa8a --- /dev/null +++ b/Android/app/src/androidTest/java/org/deadc0de/apple2ix/ApplicationTest.java @@ -0,0 +1,13 @@ +package org.deadc0de.apple2ix; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/Android/app/src/debug/AndroidManifest.xml b/Android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..952dbe91 --- /dev/null +++ b/Android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..970a318d --- /dev/null +++ b/Android/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/assets b/Android/app/src/main/assets new file mode 120000 index 00000000..2978ef39 --- /dev/null +++ b/Android/app/src/main/assets @@ -0,0 +1 @@ +../../../assets \ No newline at end of file diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AbstractMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AbstractMenu.java new file mode 100644 index 00000000..1da8b9e1 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AbstractMenu.java @@ -0,0 +1,282 @@ +/* + * 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.graphics.drawable.Drawable; +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.CheckBox; +import android.widget.CheckedTextView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.deadc0de.apple2ix.basic.R; + +public abstract class Apple2AbstractMenu implements Apple2MenuView { + + private final static String TAG = "Apple2AbstractMenu"; + + protected Apple2Activity mActivity = null; + private View mSettingsView = null; + + public Apple2AbstractMenu(Apple2Activity activity) { + mActivity = activity; + setup(); + } + + public void show() { + if (isShowing()) { + return; + } + mActivity.pushApple2View(this); + } + + public void dismiss() { + mActivity.popApple2View(this); + } + + public void dismissAll() { + this.dismiss(); + } + + public boolean isShowing() { + return mSettingsView.getParent() != null; + } + + public View getView() { + return mSettingsView; + } + + public boolean isCalibrating() { + return false; + } + + public void onKeyTapCalibrationEvent(char ascii, int scancode) { + /* ... */ + } + + // ------------------------------------------------------------------------ + // required overrides ... + + public interface IMenuEnum { + public String getTitle(final Apple2Activity activity); + + public String getSummary(final Apple2Activity activity); + + public View getView(final Apple2Activity activity, View convertView); + + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked); + } + + public abstract IMenuEnum[] allValues(); + + public abstract String[] allTitles(); + + public abstract boolean areAllItemsEnabled(); + + public abstract boolean isEnabled(int position); + + // ------------------------------------------------------------------------ + // boilerplate menu view code + + protected static View _basicView(Apple2Activity activity, IMenuEnum setting, View convertView) { + TextView tv = (TextView) convertView.findViewById(R.id.a2preference_title); + if (tv == null) { + // attemping to recycle different layout ... + LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.a2preference, null, false); + tv = (TextView) convertView.findViewById(R.id.a2preference_title); + } + tv.setText(setting.getTitle(activity)); + + tv = (TextView) convertView.findViewById(R.id.a2preference_summary); + tv.setText(setting.getSummary(activity)); + + LinearLayout layout = (LinearLayout) convertView.findViewById(R.id.a2preference_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(); + } + + return convertView; + } + + public interface IPreferenceLoadSave { + public int intValue(); + + public void saveInt(int value); + } + + public interface IPreferenceSlider extends IPreferenceLoadSave { + public void showValue(int value, final TextView seekBarValue); + } + + protected static View _sliderView(final Apple2Activity activity, final IMenuEnum setting, final int numChoices, final IPreferenceSlider iLoadSave) { + + LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.a2preference_slider, null, false); + + TextView tv = (TextView) view.findViewById(R.id.a2preference_slider_summary); + tv.setText(setting.getSummary(activity)); + + final TextView seekBarValue = (TextView) view.findViewById(R.id.a2preference_slider_seekBarValue); + + SeekBar sb = (SeekBar) view.findViewById(R.id.a2preference_slider_seekBar); + sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + return; + } + iLoadSave.showValue(progress, seekBarValue); + iLoadSave.saveInt(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + sb.setMax(0); // http://stackoverflow.com/questions/10278467/seekbar-not-setting-actual-progress-setprogress-not-working-on-early-android + sb.setMax(numChoices); + int progress = iLoadSave.intValue(); + sb.setProgress(progress); + iLoadSave.showValue(progress, seekBarValue); + return view; + } + + protected static void _alertDialogHandleSelection(final Apple2Activity activity, final int titleId, final String[] choices, final IPreferenceLoadSave iLoadSave) { + _alertDialogHandleSelection(activity, activity.getResources().getString(titleId), choices, iLoadSave); + } + + protected static void _alertDialogHandleSelection(final Apple2Activity activity, final String titleId, final String[] choices, final IPreferenceLoadSave iLoadSave) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(titleId); + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + final int checkedPosition = iLoadSave.intValue(); + final ArrayAdapter adapter = new ArrayAdapter(activity, android.R.layout.select_dialog_singlechoice, choices) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + CheckedTextView ctv = (CheckedTextView) view.findViewById(android.R.id.text1); + ctv.setChecked(position == checkedPosition); + return view; + } + }; + + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int value) { + iLoadSave.saveInt(value); + dialog.dismiss(); + } + }); + AlertDialog dialog = builder.create(); + activity.registerAndShowDialog(dialog); + } + + protected static ImageView _addPopupIcon(Apple2Activity activity, IMenuEnum setting, View convertView) { + ImageView imageView = new ImageView(activity); + Drawable drawable = activity.getResources().getDrawable(android.R.drawable.ic_menu_edit); + imageView.setImageDrawable(drawable); + LinearLayout layout = (LinearLayout) convertView.findViewById(R.id.a2preference_widget_frame); + layout.addView(imageView); + return imageView; + } + + protected static CheckBox _addCheckbox(Apple2Activity activity, IMenuEnum setting, View convertView, boolean isChecked) { + CheckBox checkBox = new CheckBox(activity); + checkBox.setChecked(isChecked); + LinearLayout layout = (LinearLayout) convertView.findViewById(R.id.a2preference_widget_frame); + layout.addView(checkBox); + return checkBox; + } + + private void setup() { + + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mSettingsView = inflater.inflate(R.layout.activity_settings, null, false); + + final Button cancelButton = (Button) mSettingsView.findViewById(R.id.cancelButton); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mActivity.dismissAllMenus(); + } + }); + + ListView settingsList = (ListView) mSettingsView.findViewById(R.id.listView_settings); + settingsList.setEnabled(true); + + ArrayAdapter adapter = new ArrayAdapter(mActivity, R.layout.a2preference, R.id.a2preference_title, allTitles()) { + @Override + public boolean areAllItemsEnabled() { + return Apple2AbstractMenu.this.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return Apple2AbstractMenu.this.isEnabled(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + //View view = super.getView(position, convertView, parent); + // ^^^ WHOA ... this is catching an NPE deep in AOSP code on the second time loading ... WTF? + // Methinks it is related to the hack of loading a completely different R.layout.something for certain views... + View view = convertView != null ? convertView : super.getView(position, null, parent); + IMenuEnum setting = allValues()[position]; + return setting.getView(mActivity, view); + } + }; + + settingsList.setAdapter(adapter); + settingsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + IMenuEnum setting = allValues()[position]; + LinearLayout layout = (LinearLayout) view.findViewById(R.id.a2preference_widget_frame); + if (layout == null) { + return; + } + + View childView = layout.getChildAt(0); + boolean selected = false; + if (childView != null && childView instanceof CheckBox) { + CheckBox checkBox = (CheckBox) childView; + checkBox.setChecked(!checkBox.isChecked()); + selected = checkBox.isChecked(); + } + setting.handleSelection(mActivity, Apple2AbstractMenu.this, selected); + } + }); + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java new file mode 100644 index 00000000..cb2106b8 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java @@ -0,0 +1,608 @@ +/* + * 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.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.StrictMode; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +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 org.deadc0de.apple2ix.basic.BuildConfig; +import org.deadc0de.apple2ix.basic.R; + +public class Apple2Activity extends Activity { + + private final static String TAG = "Apple2Activity"; + private final static int MAX_FINGERS = 32;// HACK ... + private static volatile boolean DEBUG_STRICT = false; + + private Apple2View mView = null; + private Runnable mGraphicsInitializedRunnable = null; + private Apple2SplashScreen mSplashScreen = null; + private Apple2MainMenu mMainMenu = null; + private Apple2SettingsMenu mSettingsMenu = null; + private Apple2DisksMenu mDisksMenu = null; + + private ArrayList mMenuStack = new ArrayList(); + private ArrayList mAlertDialogs = new ArrayList(); + + private AtomicBoolean mPausing = new AtomicBoolean(false); + + private float[] mXCoords = new float[MAX_FINGERS]; + private float[] mYCoords = new float[MAX_FINGERS]; + + // 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; + private static boolean sNativeBarfed = false; + + static { + try { + System.loadLibrary("apple2ix"); + } catch (Throwable barf) { + sNativeBarfed = true; + sNativeBarfedThrowable = barf; + } + } + + public final static long NATIVE_TOUCH_HANDLED = (1 << 0); + public final static long NATIVE_TOUCH_REQUEST_SHOW_MENU = (1 << 1); + + public final static long NATIVE_TOUCH_KEY_TAP = (1 << 4); + public final static long NATIVE_TOUCH_KBD = (1 << 5); + public final static long NATIVE_TOUCH_JOY = (1 << 6); + public final static long NATIVE_TOUCH_MENU = (1 << 7); + public final static long NATIVE_TOUCH_JOY_KPAD = (1 << 8); + + public final static long NATIVE_TOUCH_INPUT_DEVICE_CHANGED = (1 << 16); + public final static long NATIVE_TOUCH_CPU_SPEED_DEC = (1 << 17); + public final static long NATIVE_TOUCH_CPU_SPEED_INC = (1 << 18); + + public final static long NATIVE_TOUCH_ASCII_SCANCODE_SHIFT = 32; + public final static long NATIVE_TOUCH_ASCII_SCANCODE_MASK = 0xFFFFL; + public final static long NATIVE_TOUCH_ASCII_MASK = 0xFF00L; + public final static long NATIVE_TOUCH_SCANCODE_MASK = 0x00FFL; + + private native void nativeOnCreate(String dataDir, int sampleRate, int monoBufferSize, int stereoBufferSize); + + private native void nativeOnKeyDown(int keyCode, int metaState); + + private native void nativeOnKeyUp(int keyCode, int metaState); + + public native void nativeEmulationResume(); + + public native void nativeEmulationPause(); + + public native void nativeOnQuit(); + + public native long nativeOnTouch(int action, int pointerCount, int pointerIndex, float[] xCoords, float[] yCoords); + + public native void nativeReboot(); + + public native void nativeChooseDisk(String path, boolean driveA, boolean readOnly); + + public native void nativeEjectDisk(boolean driveA); + + + @Override + public void onCreate(Bundle savedInstanceState) { + if (Apple2Activity.DEBUG_STRICT && BuildConfig.DEBUG) { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectAll() + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + /*.detectLeakedClosableObjects()*/ + .penaltyLog() + .penaltyDeath() + .build()); + } + super.onCreate(savedInstanceState); + + Log.e(TAG, "onCreate()"); + + // placeholder view on initial launch + if (mView == null) { + setContentView(new View(this)); + } + + Apple2CrashHandler.getInstance().initializeAndSetCustomExceptionHandler(this); + if (sNativeBarfed) { + Log.e(TAG, "NATIVE BARFED...", sNativeBarfedThrowable); + return; + } + + int sampleRate = DevicePropertyCalculator.getRecommendedSampleRate(this); + int monoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/false); + int stereoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/true); + Log.d(TAG, "Device sampleRate:" + sampleRate + " mono bufferSize:" + monoBufferSize + " stereo bufferSize:" + stereoBufferSize); + + String dataDir = Apple2DisksMenu.getDataDir(this); + nativeOnCreate(dataDir, sampleRate, monoBufferSize, stereoBufferSize); + + final boolean firstTime = !Apple2Preferences.FIRST_TIME_CONFIGURED.booleanValue(this); + Apple2Preferences.FIRST_TIME_CONFIGURED.saveBoolean(this, true); + + showSplashScreen(!firstTime); + Apple2CrashHandler.getInstance().checkForCrashes(this); + + mGraphicsInitializedRunnable = new Runnable() { + @Override + public void run() { + if (firstTime) { + Apple2Preferences.KeypadPreset.IJKM_SPACE.apply(Apple2Activity.this); + } + Apple2Preferences.loadPreferences(Apple2Activity.this); + } + }; + + // first-time initializations + if (firstTime) { + new Thread(new Runnable() { + @Override + public void run() { + Apple2DisksMenu.firstTime(Apple2Activity.this); + mSplashScreen.setDismissable(true); + Log.d(TAG, "Finished first time copying..."); + } + }).start(); + } + + 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)) { + handleInsertDiskIntent(path); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (sNativeBarfed) { + Apple2CrashHandler.getInstance().abandonAllHope(this, sNativeBarfedThrowable); + return; + } + + Log.d(TAG, "onResume()"); + showSplashScreen(/*dismissable:*/true); + Apple2CrashHandler.getInstance().checkForCrashes(this); // NOTE : needs to be called again to clean-up + } + + @Override + protected void onPause() { + super.onPause(); + if (sNativeBarfed) { + return; + } + + boolean wasPausing = mPausing.getAndSet(true); + if (wasPausing) { + return; + } + + Log.d(TAG, "onPause()"); + if (mView != null) { + mView.onPause(); + } + + // Apparently not good to leave popup/dialog windows showing when backgrounding. + // Dismiss these popups to avoid android.view.WindowLeaked issues + synchronized (this) { + dismissAllMenus(); + nativeEmulationPause(); + } + + mPausing.set(false); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (sNativeBarfed) { + return true; + } + if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) || (keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) || (keyCode == KeyEvent.KEYCODE_VOLUME_UP)) { + return false; + } + nativeOnKeyDown(keyCode, event.getMetaState()); + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (sNativeBarfed) { + return true; + } + if (keyCode == KeyEvent.KEYCODE_BACK) { + Apple2MenuView apple2MenuView = peekApple2View(); + if (apple2MenuView == null) { + showMainMenu(); + } else { + apple2MenuView.dismiss(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MENU) { + showMainMenu(); + return true; + } else if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) || (keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) || (keyCode == KeyEvent.KEYCODE_VOLUME_UP)) { + return false; + } else { + nativeOnKeyUp(keyCode, event.getMetaState()); + return true; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + do { + + if (sNativeBarfed) { + break; + } + if (mMainMenu == null) { + break; + } + + Apple2MenuView apple2MenuView = peekApple2View(); + if ((apple2MenuView != null) && (!apple2MenuView.isCalibrating())) { + break; + } + + //printSamples(event); + int action = event.getActionMasked(); + int pointerIndex = event.getActionIndex(); + int pointerCount = event.getPointerCount(); + for (int i = 0; i < pointerCount/* && i < MAX_FINGERS */; i++) { + mXCoords[i] = event.getX(i); + mYCoords[i] = event.getY(i); + } + + long nativeFlags = nativeOnTouch(action, pointerCount, pointerIndex, mXCoords, mYCoords); + + if ((nativeFlags & NATIVE_TOUCH_HANDLED) == 0) { + break; + } + + if ((nativeFlags & NATIVE_TOUCH_REQUEST_SHOW_MENU) != 0) { + mMainMenu.show(); + } + + if ((nativeFlags & NATIVE_TOUCH_KEY_TAP) != 0) { + if (Apple2Preferences.KEYBOARD_CLICK_ENABLED.booleanValue(this)) { + AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); + if (am != null) { + am.playSoundEffect(AudioManager.FX_KEY_CLICK); + } + } + + if ((apple2MenuView != null) && apple2MenuView.isCalibrating()) { + long asciiScancodeLong = nativeFlags & (NATIVE_TOUCH_ASCII_SCANCODE_MASK << NATIVE_TOUCH_ASCII_SCANCODE_SHIFT); + int asciiInt = (int) (asciiScancodeLong >> (NATIVE_TOUCH_ASCII_SCANCODE_SHIFT + 8)); + int scancode = (int) ((asciiScancodeLong >> NATIVE_TOUCH_ASCII_SCANCODE_SHIFT) & 0xFFL); + char ascii = (char) asciiInt; + apple2MenuView.onKeyTapCalibrationEvent(ascii, scancode); + } + } + + if ((nativeFlags & NATIVE_TOUCH_MENU) == 0) { + break; + } + + // handle menu-specific actions + + if ((nativeFlags & NATIVE_TOUCH_INPUT_DEVICE_CHANGED) != 0) { + Apple2Preferences.TouchDeviceVariant nextVariant; + if ((nativeFlags & NATIVE_TOUCH_KBD) != 0) { + nextVariant = Apple2Preferences.TouchDeviceVariant.KEYBOARD; + } else if ((nativeFlags & NATIVE_TOUCH_JOY) != 0) { + nextVariant = Apple2Preferences.TouchDeviceVariant.JOYSTICK; + } else if ((nativeFlags & NATIVE_TOUCH_JOY_KPAD) != 0) { + nextVariant = Apple2Preferences.TouchDeviceVariant.JOYSTICK_KEYPAD; + } else { + int touchDevice = Apple2Preferences.nativeGetCurrentTouchDevice(); + nextVariant = Apple2Preferences.TouchDeviceVariant.next(touchDevice); + } + Apple2Preferences.CURRENT_TOUCH_DEVICE.saveTouchDevice(this, nextVariant); + } else if ((nativeFlags & NATIVE_TOUCH_CPU_SPEED_DEC) != 0) { + int percentSpeed = Apple2Preferences.nativeGetCPUSpeed(); + if (percentSpeed > 400) { // HACK: max value from native side + percentSpeed = 375; + } else if (percentSpeed > 100) { + percentSpeed -= 25; + } else { + percentSpeed -= 5; + } + Apple2Preferences.CPU_SPEED_PERCENT.saveInt(this, percentSpeed); + } else if ((nativeFlags & NATIVE_TOUCH_CPU_SPEED_INC) != 0) { + int percentSpeed = Apple2Preferences.nativeGetCPUSpeed(); + if (percentSpeed >= 100) { + percentSpeed += 25; + } else { + percentSpeed += 5; + } + Apple2Preferences.CPU_SPEED_PERCENT.saveInt(this, percentSpeed); + } + } while (false); + + return super.onTouchEvent(event); + } + + public void showMainMenu() { + if (mMainMenu != null) { + if (!(mSettingsMenu.isShowing() || mDisksMenu.isShowing())) { + mMainMenu.show(); + } + } + } + + public Apple2MainMenu getMainMenu() { + return mMainMenu; + } + + public synchronized Apple2DisksMenu getDisksMenu() { + return mDisksMenu; + } + + public synchronized Apple2SettingsMenu getSettingsMenu() { + return mSettingsMenu; + } + + private void handleInsertDiskIntent(final String path) { + runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (Apple2Activity.this) { + if (mMainMenu == null) { + return; + } + String diskPath = path; + File diskFile = new File(diskPath); + if (!diskFile.canRead()) { + Toast.makeText(Apple2Activity.this, Apple2Activity.this.getString(R.string.disk_insert_could_not_read), Toast.LENGTH_SHORT).show(); + return; + } + + Apple2Preferences.CURRENT_DISK_A_RO.saveBoolean(Apple2Activity.this, true); + final int len = diskPath.length(); + final String suffix = diskPath.substring(len - 3, len); + if (suffix.equalsIgnoreCase(".gz")) { // HACK FIXME TODO : small amount of code duplication of Apple2DisksMenu + diskPath = diskPath.substring(0, len - 3); + } + Apple2Preferences.CURRENT_DISK_A.saveString(Apple2Activity.this, diskPath); + + while (mDisksMenu.popPathStack() != null) { + /* ... */ + } + + File storageDir = Apple2DisksMenu.getExternalStorageDirectory(); + if (storageDir != null) { + String storagePath = storageDir.getAbsolutePath(); + if (diskPath.contains(storagePath)) { + diskPath = diskPath.replace(storagePath + File.separator, ""); + mDisksMenu.pushPathStack(storagePath); + } + } + StringTokenizer tokenizer = new StringTokenizer(diskPath, File.separator); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (token.equals("")) { + continue; + } + if (Apple2DisksMenu.hasDiskExtension(token)) { + continue; + } + mDisksMenu.pushPathStack(token); + } + + Toast.makeText(Apple2Activity.this, Apple2Activity.this.getString(R.string.disk_insert_toast), Toast.LENGTH_SHORT).show(); + } + } + }); + } + + public Apple2SplashScreen getSplashScreen() { + return mSplashScreen; + } + + private void showSplashScreen(boolean dismissable) { + if (mSplashScreen != null) { + return; + } + mSplashScreen = new Apple2SplashScreen(this, dismissable); + mSplashScreen.show(); + } + + private void setupGLView() { + + boolean glViewFirstTime = false; + if (mView == null) { + glViewFirstTime = true; + mView = new Apple2View(this, mGraphicsInitializedRunnable); + mGraphicsInitializedRunnable = null; + mMainMenu = new Apple2MainMenu(this, mView); + } + + if (glViewFirstTime) { + // HACK NOTE : do not blanket setContentView() ... it appears to wedge Gingerbread + setContentView(mView); + } else { + mView.onResume(); + } + } + + public void registerAndShowDialog(AlertDialog dialog) { + dialog.show(); + mAlertDialogs.add(dialog); + } + + public synchronized void pushApple2View(Apple2MenuView apple2MenuView) { + mMenuStack.add(apple2MenuView); + View menuView = apple2MenuView.getView(); + nativeEmulationPause(); + addContentView(menuView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + public synchronized Apple2MenuView popApple2View() { + int lastIndex = mMenuStack.size() - 1; + if (lastIndex < 0) { + return null; + } + + Apple2MenuView apple2MenuView = mMenuStack.remove(lastIndex); + _disposeApple2View(apple2MenuView); + return apple2MenuView; + } + + public synchronized Apple2MenuView peekApple2View() { + int lastIndex = mMenuStack.size() - 1; + if (lastIndex < 0) { + return null; + } + + return mMenuStack.get(lastIndex); + } + + public synchronized Apple2MenuView peekApple2View(int index) { + int lastIndex = mMenuStack.size() - 1; + if (lastIndex < 0) { + return null; + } + + try { + return mMenuStack.get(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public void dismissAllMenus() { + if (mMainMenu != null) { + mMainMenu.dismiss(); + } + + for (AlertDialog dialog : mAlertDialogs) { + dialog.dismiss(); + } + mAlertDialogs.clear(); + + // Get rid of the menu hierarchy + ArrayList menuHierarchy = new ArrayList(mMenuStack); + Collections.reverse(menuHierarchy); + for (Apple2MenuView view : menuHierarchy) { + view.dismissAll(); + } + } + + public synchronized Apple2MenuView popApple2View(Apple2MenuView apple2MenuView) { + boolean wasRemoved = mMenuStack.remove(apple2MenuView); + _disposeApple2View(apple2MenuView); + return wasRemoved ? apple2MenuView : null; + } + + private void _disposeApple2View(Apple2MenuView apple2MenuView) { + + boolean dismissedSplashScreen = false; + + // Actually remove View from view hierarchy + { + View menuView = apple2MenuView.getView(); + ViewGroup viewGroup = (ViewGroup) menuView.getParent(); + if (viewGroup != null) { + viewGroup.removeView(menuView); + } + if (apple2MenuView instanceof Apple2SplashScreen) { // 20151101 HACK NOTE : use instanceof to avoid edge case where joystick calibration occurred (and thus the splash was already dismissed without proper mView initialization) + mSplashScreen = null; + dismissedSplashScreen = true; + } + } + + // if no more views on menu stack, resume emulation + if (mMenuStack.size() == 0) { + dismissAllMenus(); // NOTE : at this point, this should not be re-entrant into mMenuStack, it should just dismiss lingering popups + if (!mPausing.get()) { + if (dismissedSplashScreen) { + setupGLView(); + } else { + nativeEmulationResume(); + } + } + } + } + + public void maybeResumeCPU() { + if (mMenuStack.size() == 0 && !mPausing.get()) { + nativeEmulationResume(); + } + } + + public void maybeQuitApp() { + nativeEmulationPause(); + AlertDialog quitDialog = new AlertDialog.Builder(this).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(R.string.quit_really).setMessage(R.string.quit_warning).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + nativeOnQuit(); + Apple2Activity.this.finish(); + new Runnable() { + @Override + public void run() { + try { + Thread.sleep(2000); + } catch (InterruptedException ex) { + // ... + } + System.exit(0); + } + }.run(); + } + }).setNegativeButton(R.string.no, null).create(); + registerAndShowDialog(quitDialog); + } + + public void maybeReboot() { + nativeEmulationPause(); + AlertDialog rebootDialog = new AlertDialog.Builder(this).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(R.string.reboot_really).setMessage(R.string.reboot_warning).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + nativeReboot(); + Apple2Activity.this.mMainMenu.dismiss(); + } + }).setNegativeButton(R.string.no, null).create(); + registerAndShowDialog(rebootDialog); + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AudioSettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AudioSettingsMenu.java new file mode 100644 index 00000000..ffb41101 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2AudioSettingsMenu.java @@ -0,0 +1,226 @@ +/* + * 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.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2AudioSettingsMenu extends Apple2AbstractMenu { + + private final static String TAG = "Apple2AudioSettingsMenu"; + + public Apple2AudioSettingsMenu(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return false; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return position == SETTINGS.MOCKINGBOARD_ENABLED.ordinal(); + } + + enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + SPEAKER_ENABLED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.speaker_enable); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.speaker_enable_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, true); + cb.setEnabled(false); + return convertView; + } + }, + SPEAKER_VOLUME { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.speaker_volume); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.speaker_volume_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.Volume.MAX.ordinal() - 1, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.SPEAKER_VOLUME.saveVolume(activity, Apple2Preferences.Volume.values()[progress]); + } + + @Override + public int intValue() { + return Apple2Preferences.SPEAKER_VOLUME.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + progress); + } + }); + } + }, + MOCKINGBOARD_ENABLED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.mockingboard_enable); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.mockingboard_enable_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.MOCKINGBOARD_ENABLED.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.MOCKINGBOARD_ENABLED.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + MOCKINGBOARD_VOLUME { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.mockingboard_volume); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.mockingboard_volume_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.Volume.MAX.ordinal() - 1, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.MOCKINGBOARD_VOLUME.saveVolume(activity, Apple2Preferences.Volume.values()[progress]); + } + + @Override + public int intValue() { + return Apple2Preferences.MOCKINGBOARD_VOLUME.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + progress); + } + }); + } + }, + ADVANCED_SEPARATOR { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced_summary); + } + }, + AUDIO_LATENCY { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.audio_latency); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.audio_latency_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.AUDIO_LATENCY_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + if (progress == 0) { + // disallow 0-length buffer ... + progress = 1; + } + Apple2Preferences.AUDIO_LATENCY.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.AUDIO_LATENCY.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + ((float) progress / Apple2Preferences.AUDIO_LATENCY_NUM_CHOICES)); + } + }); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java new file mode 100644 index 00000000..79d27124 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java @@ -0,0 +1,569 @@ +/* + * 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.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; + +import org.deadc0de.apple2ix.basic.BuildConfig; +import org.deadc0de.apple2ix.basic.R; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Apple2CrashHandler { + + public final static String javaCrashFileName = "jcrash.txt"; + + public static Apple2CrashHandler getInstance() { + return sCrashHandler; + } + + public enum CrashType { + JAVA_CRASH { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.crash_java_npe); + } + }, + NULL_DEREF { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.crash_null); + } + }, + STACKCALL_OVERFLOW { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.crash_stackcall_overflow); + } + }, + STACKBUF_OVERFLOW { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.crash_stackbuf_overflow); + } + }; + + + public static final int size = CrashType.values().length; + + public abstract String getTitle(Apple2Activity activity); + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (CrashType setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } + + public synchronized void initializeAndSetCustomExceptionHandler(Apple2Activity activity) { + synchronized (this) { + if (homeDir == null) { + homeDir = Apple2DisksMenu.getDataDir(activity); + } + } + if (mDefaultExceptionHandler != null) { + return; + } + mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + + final Thread.UncaughtExceptionHandler defaultExceptionHandler = mDefaultExceptionHandler; + + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable t) { + try { + Apple2CrashHandler.onUncaughtException(thread, t); + } catch (Throwable terminator2) { + // Yo dawg, I hear you like exceptions in your exception handler! ... + } + defaultExceptionHandler.uncaughtException(thread, t); + } + }); + } + + public void abandonAllHope(Apple2Activity activity, Throwable nativeBarfed) { + // write out the early link crash and send this through the main crash processing code + onUncaughtException(Thread.currentThread(), nativeBarfed); + checkForCrashes(activity); + } + + public boolean areJavaCrashesPresent(Apple2Activity activity) { + File javaCrash = _javaCrashFile(activity); + return javaCrash.exists(); + } + + public boolean areNativeCrashesPresent(Apple2Activity activity) { + File[] nativeCrashes = _nativeCrashFiles(activity); + return nativeCrashes != null && nativeCrashes.length > 0; + } + + public boolean areCrashesPresent(Apple2Activity activity) { + return areJavaCrashesPresent(activity) || areNativeCrashesPresent(activity); + } + + public void checkForCrashes(final Apple2Activity activity) { + if (!areCrashesPresent(activity)) { + return; + } + + if (!Apple2Preferences.CRASH_CHECK.booleanValue(activity)) { + return; + } + + boolean previouslyRanCrashCheck = mAlreadyRanCrashCheck.getAndSet(true); + + boolean previouslySentReport = mAlreadySentReport.get(); + if (previouslySentReport) { + + // here we assume that the crash data was previously sent via email ... if not then we lost it =P + + Log.d(TAG, "Cleaning up crash data ..."); + int idx = 0; + File[] nativeCrashes = _nativeCrashFiles(activity); + for (File crash : nativeCrashes) { + + if (!crash.delete()) { + Log.d(TAG, "Could not unlink crash : " + crash); + } + + File processed = new File(_dumpPath2ProcessedPath(crash.getAbsolutePath())); + if (!processed.delete()) { + Log.d(TAG, "Could not unlink processed : " + processed); + } + } + + File javaCrashFile = _javaCrashFile(activity); + if (!javaCrashFile.delete()) { + Log.d(TAG, "Could not unlink java crash : " + javaCrashFile); + } + + // remove previous log file + _writeTempLogFile(activity, new StringBuilder()); + return; + } + + if (previouslyRanCrashCheck) { + // don't keep asking on return from backgrounding + return; + } + + final AlertDialog crashDialog = new AlertDialog.Builder(activity).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(R.string.crasher_send).setMessage(R.string.crasher_send_message).setNegativeButton(R.string.no, null).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + + final Apple2SplashScreen splashScreen = activity.getSplashScreen(); + if (splashScreen != null) { + splashScreen.setDismissable(false); + } + final ProgressBar bar = (ProgressBar) activity.findViewById(R.id.crash_progressBar); + try { + bar.setVisibility(View.VISIBLE); + } catch (NullPointerException npe) { + /* could happen on early lifecycle crashes */ + } + + new Thread(new Runnable() { + @Override + public void run() { + + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + + final int sampleRate = DevicePropertyCalculator.getRecommendedSampleRate(activity); + final int monoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(activity, /*isStereo:*/false); + final int stereoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(activity, /*isStereo:*/true); + + StringBuilder summary = new StringBuilder(); + StringBuilder allCrashData = new StringBuilder(); + + // prepend information about this device + summary.append("BRAND: ").append(Build.BRAND).append("\n"); + summary.append("MODEL: ").append(Build.MODEL).append("\n"); + summary.append("MANUFACTURER: ").append(Build.MANUFACTURER).append("\n"); + summary.append("DEVICE: ").append(Build.DEVICE).append("\n"); + summary.append("SAMPLE RATE: ").append(sampleRate).append("\n"); + summary.append("MONO BUFSIZE: ").append(monoBufferSize).append("\n"); + summary.append("STEREO BUFSIZE: ").append(stereoBufferSize).append("\n"); + summary.append("GPU VENDOR: ").append(Apple2Preferences.GL_VENDOR.stringValue(activity)).append("\n"); + summary.append("GPU RENDERER: ").append(Apple2Preferences.GL_RENDERER.stringValue(activity)).append("\n"); + summary.append("GPU VERSION: ").append(Apple2Preferences.GL_VERSION.stringValue(activity)).append("\n"); + + allCrashData.append(summary); + + File[] nativeCrashes = _nativeCrashFiles(activity); + if (nativeCrashes == null) { + nativeCrashes = new File[0]; + } + + final int len = nativeCrashes.length + 1/* maybe Java crash */ + 1/* exposeSymbols */; + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (bar != null) { + bar.setMax(len); + } + } + }); + + if (len > 0) { + Apple2DisksMenu.exposeSymbols(activity); + } + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (bar != null) { + bar.setProgress(1); + } + } + }); + + boolean summarizedHeader = false; + + // iteratively process native crashes + for (File crash : nativeCrashes) { + + String crashPath = crash.getAbsolutePath(); + Log.d(TAG, "Processing crash : " + crashPath); + + String processedPath = _dumpPath2ProcessedPath(crashPath); + try { + nativeProcessCrash(crashPath, processedPath); // Run Breakpad minidump_stackwalk + } catch (UnsatisfiedLinkError ule) { + /* could happen on early lifecycle crashes */ + } + + StringBuilder crashData = new StringBuilder(); + if (!_readFile(new File(processedPath), crashData)) { + Log.e(TAG, "Error processing crash : " + crashPath); + } + allCrashData.append(">>>>>>> NATIVE CRASH [").append(crashPath).append("]\n"); + allCrashData.append(crashData); + summary.append("NATIVE CRASH:\n"); + + // append succinct information about crashing thread + String[] lines = crashData.toString().split("[\\n\\r][\\n\\r]*"); + for (int i = 0, j = 0; i < lines.length; i++) { + + // 2 lines of minidump summary + if (i < 2) { + if (!summarizedHeader) { + summary.append(lines[i]); + summary.append("\n"); + } + continue; + } + + // 1 line of crashing thread and reason + if (i == 2) { + summarizedHeader = true; + summary.append(lines[i]); + summary.append("\n"); + continue; + } + + // whole lotta modules + if (lines[i].startsWith("Module")) { + continue; + } + + // one apparently empty line + if (lines[i].matches("^[ \\t]*$")) { + continue; + } + + // append crashing thread backtrace + + summary.append(lines[i]); + summary.append("\n"); + final int maxSummaryBacktrace = 8; + if (j++ >= maxSummaryBacktrace) { + break; + } + } + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (bar != null) { + bar.setProgress(bar.getProgress() + 1); + } + } + }); + } + + StringBuilder javaCrashData = new StringBuilder(); + File javaCrashFile = _javaCrashFile(activity); + if (javaCrashFile.exists()) { + Log.d(TAG, "Reading java crashes file"); + if (!_readFile(javaCrashFile, javaCrashData)) { + Log.e(TAG, "Error processing java crash : " + javaCrashFileName); + } + } + + allCrashData.append(">>>>>>> JAVA CRASH DATA\n"); + allCrashData.append(javaCrashData); + + summary.append("JAVA CRASH:\n"); + summary.append(javaCrashData); + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (bar != null) { + bar.setProgress(bar.getProgress() + 1); + } + } + }); + + Apple2DisksMenu.unexposeSymbols(activity); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + bar.setVisibility(View.INVISIBLE); + splashScreen.setDismissable(true); + } catch (NullPointerException npe) { + /* could happen on early lifecycle crashes */ + } + } + }); + + // send report with all the data + _sendEmailToDeveloperWithCrashData(activity, summary, allCrashData); + } + }).start(); + } + }).create(); + activity.registerAndShowDialog(crashDialog); + } + + public void performCrash(int crashType) { + if (BuildConfig.DEBUG) { + nativePerformCrash(crashType); + } + } + + // ------------------------------------------------------------------------ + // privates + + private Apple2CrashHandler() { + /* ... */ + } + + private static void onUncaughtException(Thread thread, Throwable t) { + StackTraceElement[] stackTraceElements = t.getStackTrace(); + StringBuffer traceBuffer = new StringBuffer(); + + // append the Java stack trace + traceBuffer.append("NAME: ").append(t.getClass().getName()).append("\n"); + traceBuffer.append("MESSAGE: ").append(t.getMessage()).append("\n"); + final int maxTraceSize = 2048 + 1024 + 512; // probably should keep this less than a standard Linux PAGE_SIZE + for (StackTraceElement elt : stackTraceElements) { + traceBuffer.append(elt.toString()); + traceBuffer.append("\n"); + if (traceBuffer.length() >= maxTraceSize) { + break; + } + } + traceBuffer.append("\n"); + + final int maxAttempts = 5; + int attempts = 0; + do { + try { + BufferedWriter writer = new BufferedWriter(new FileWriter(new File(sCrashHandler.homeDir, javaCrashFileName), /*append:*/true)); + writer.append(traceBuffer); + writer.flush(); + writer.close(); + break; + } catch (InterruptedIOException ie) { + /* EINTR, EAGAIN ... */ + } catch (IOException e) { + Log.e(TAG, "Exception attempting to write data : " + e); + } + + try { + Thread.sleep(100, 0); + } catch (InterruptedException e) { + /* ... */ + } + ++attempts; + } while (attempts < maxAttempts); + } + + private File _javaCrashFile(Apple2Activity activity) { + return new File(homeDir, javaCrashFileName); + } + + private File[] _nativeCrashFiles(Apple2Activity activity) { + FilenameFilter dmpFilter = new FilenameFilter() { + public boolean accept(File dir, String name) { + File file = new File(dir, name); + if (file.isDirectory()) { + return false; + } + + // check file extensions ... sigh ... no String.endsWithIgnoreCase() ? + + final String extension = ".dmp"; + final int nameLen = name.length(); + final int extLen = extension.length(); + if (nameLen <= extLen) { + return false; + } + + String suffix = name.substring(nameLen - extLen, nameLen); + return (suffix.equalsIgnoreCase(extension)); + } + }; + + return new File(homeDir).listFiles(dmpFilter); + } + + private String _dumpPath2ProcessedPath(String crashPath) { + return crashPath.substring(0, crashPath.length() - 4) + ".txt"; + } + + private boolean _readFile(File file, StringBuilder fileData) { + final int maxAttempts = 5; + int attempts = 0; + do { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + char[] buf = new char[1024]; + int numRead = 0; + while ((numRead = reader.read(buf)) != -1) { + String readData = String.valueOf(buf, 0, numRead); + fileData.append(readData); + } + reader.close(); + break; + } catch (InterruptedIOException ie) { + /* EINTR, EAGAIN ... */ + } catch (IOException e) { + Log.d(TAG, "Error reading file at path : " + file.toString()); + } + + try { + Thread.sleep(100, 0); + } catch (InterruptedException e) { + /* ... */ + } + ++attempts; + } while (attempts < maxAttempts); + + return attempts < maxAttempts; + } + + private File _writeTempLogFile(Apple2Activity activity, StringBuilder allCrashData) { + + File allCrashFile = null; + + String storageState = Environment.getExternalStorageState(); + if (storageState.equals(Environment.MEDIA_MOUNTED)) { + allCrashFile = new File(Environment.getExternalStorageDirectory(), "apple2ix_crash.txt"); + } else { + allCrashFile = new File(Apple2DisksMenu.getDataDir(activity), "apple2ix_crash.txt"); + } + + Log.d(TAG, "Writing all crashes to temp file : " + allCrashFile.getAbsolutePath()); + final int maxAttempts = 5; + int attempts = 0; + do { + try { + BufferedWriter writer = new BufferedWriter(new FileWriter(allCrashFile)); + writer.append(allCrashData); + writer.flush(); + writer.close(); + break; + } catch (InterruptedIOException ie) { + /* EINTR, EAGAIN ... */ + } catch (IOException e) { + Log.e(TAG, "Exception attempting to write data : " + e); + } + + try { + Thread.sleep(100, 0); + } catch (InterruptedException e) { + /* ... */ + } + ++attempts; + } while (attempts < maxAttempts); + + if (!allCrashFile.setReadable(true, /*ownerOnly:*/false)) { + Log.d(TAG, "Oops, could not set all crash data readable!"); + } + + return allCrashFile; + } + + private void _sendEmailToDeveloperWithCrashData(Apple2Activity activity, StringBuilder summary, StringBuilder allCrashData) { + mAlreadySentReport.set(true); + + // ... the disaster that is early Android ... there does not appear to be a reliable way to start an + // email Intent to send both text and an attachment, but we make a valiant (if futile) effort to do so here. + // And the reason to send an attachment is that you trigger an android.os.TransactionTooLargeException with too + // much text data in the EXTRA_TEXT ... + + Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "apple2ix_crash@deadcode.org"/*non-zero variant is correct endpoint at the moment*/, null)); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Crasher"); + + final int maxCharsEmail = 4096; + int len = summary.length(); + len = len < maxCharsEmail ? len : maxCharsEmail; + String summaryData = summary.substring(0, len); + emailIntent.putExtra(Intent.EXTRA_TEXT, "The app crashed, please help!\n\n"+summaryData); + + File allCrashFile = _writeTempLogFile(activity, allCrashData); + emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(allCrashFile)); + + Log.d(TAG, "STARTING CHOOSER FOR EMAIL ..."); + activity.startActivity(Intent.createChooser(emailIntent, "Send email")); + Log.d(TAG, "AFTER START ACTIVITY ..."); + } + + + private final static String TAG = "Apple2CrashHandler"; + private final static Apple2CrashHandler sCrashHandler = new Apple2CrashHandler(); + + private String homeDir; + private Thread.UncaughtExceptionHandler mDefaultExceptionHandler; + private AtomicBoolean mAlreadyRanCrashCheck = new AtomicBoolean(false); + private AtomicBoolean mAlreadySentReport = new AtomicBoolean(false); + + private static native void nativePerformCrash(int crashType); // testing + + private static native void nativeProcessCrash(String crashFilePath, String crashProcessedPath); + +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java new file mode 100644 index 00000000..be3d4476 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2DisksMenu.java @@ -0,0 +1,658 @@ +/* + * 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.ProgressBar; +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 mPathStack = new ArrayList(); + + private static File sExternalFilesDir = null; + private static File sDownloadFilesDir = 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() { + + do { + if (sExternalFilesDir != null) { + break; + } + + String storageState = Environment.getExternalStorageState(); + if (!storageState.equals(Environment.MEDIA_MOUNTED)) { + // 2015/10/28 : do not expose sExternalFilesDir/sDownloadFilesDir unless they are writable + break; + } + + File externalStorageDir = Environment.getExternalStorageDirectory(); + if (externalStorageDir == null) { + break; + } + + File externalDir = new File(externalStorageDir, "apple2ix"); // /sdcard/apple2ix + if (!externalDir.exists()) { + boolean made = externalDir.mkdirs(); + if (!made) { + Log.d(TAG, "WARNING: could not make directory : " + sExternalFilesDir); + break; + } + } + + sExternalFilesDir = externalDir; + sDownloadFilesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } while (false); + + 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) { + final ProgressBar bar = (ProgressBar) activity.findViewById(R.id.crash_progressBar); + try { + bar.setVisibility(View.VISIBLE); + bar.setIndeterminate(true); + } catch (NullPointerException npe) { + Log.v(TAG, "Whoa, avoided NPE in first time #1"); + } + + 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 + if (sExternalFilesDir != null) { + recursivelyCopyAPKAssets(activity, /*from APK directory:*/"keyboards", /*to location:*/sExternalFilesDir.getAbsolutePath()); + } + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + bar.setVisibility(View.INVISIBLE); + bar.setIndeterminate(false); + } catch (NullPointerException npe) { + Log.v(TAG, "Whoa, avoided NPE in first time #2"); + } + } + }); + } + + public static void exposeSymbols(Apple2Activity activity) { + recursivelyCopyAPKAssets(activity, /*from APK directory:*/"symbols", /*to location:*/new File(sDataDir, "symbols").getAbsolutePath()); + } + + public static void unexposeSymbols(Apple2Activity activity) { + recursivelyDelete(new File(sDataDir, "symbols")); + } + + // ------------------------------------------------------------------------ + // 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.getParent() != null; + } + + 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 recursivelyDelete(File file) { + if (file.isDirectory()) { + for (File f : file.listFiles()) { + recursivelyDelete(f); + } + } + if (!file.delete()) { + Log.d(TAG, "Failed to delete file: " + file); + } + } + + 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 boolean includeDownloadsPath = (sDownloadFilesDir != null && isRootPath); + final int offset = includeExternalStoragePath ? (includeDownloadsPath ? 2 : 1) : (includeDownloadsPath ? 1 : 0); + final String[] fileNames = new String[files.length + offset]; + final boolean[] isDirectory = new boolean[files.length + offset]; + + int idx = 0; + if (includeExternalStoragePath) { + fileNames[idx] = sExternalFilesDir.getAbsolutePath(); + isDirectory[idx] = true; + ++idx; + } + if (includeDownloadsPath) { + fileNames[idx] = sDownloadFilesDir.getAbsolutePath(); + isDirectory[idx] = true; + ++idx; + } + + for (File file : files) { + isDirectory[idx] = file.isDirectory(); + fileNames[idx] = file.getName(); + if (isDirectory[idx]) { + fileNames[idx] += File.separator; + } + ++idx; + } + + ArrayAdapter adapter = new ArrayAdapter(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; + } + }; + + final String parentDisksDir = disksDir; + final boolean parentIsRootPath = isRootPath; + + 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]); + if (parentIsRootPath && !new File(fileNames[position]).isAbsolute()) { + pushPathStack(parentDisksDir + File.separator + fileNames[position]); + } else { + 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) { + 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); + } + dialog.dismiss(); + mActivity.dismissAllMenus(); + } + }); + + AlertDialog dialog = builder.create(); + mActivity.registerAndShowDialog(dialog); + } + }); + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickCalibration.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickCalibration.java new file mode 100644 index 00000000..5c662dbf --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickCalibration.java @@ -0,0 +1,123 @@ +/* + * 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.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.SeekBar; + +import org.deadc0de.apple2ix.basic.R; + +import java.util.ArrayList; + +public class Apple2JoystickCalibration implements Apple2MenuView { + + private final static String TAG = "Apple2JoystickCalibration"; + + private Apple2Activity mActivity = null; + private View mSettingsView = null; + private ArrayList mViewStack = null; + private boolean mTouchMenuEnabled = false; + private int mSavedTouchDevice = Apple2Preferences.TouchDeviceVariant.NONE.ordinal(); + + public Apple2JoystickCalibration(Apple2Activity activity, ArrayList viewStack, Apple2Preferences.TouchDeviceVariant variant) { + mActivity = activity; + mViewStack = viewStack; + if (!(variant == Apple2Preferences.TouchDeviceVariant.JOYSTICK || variant == Apple2Preferences.TouchDeviceVariant.JOYSTICK_KEYPAD)) { + throw new RuntimeException("You're doing it wrong"); + } + + setup(variant); + } + + private void setup(Apple2Preferences.TouchDeviceVariant variant) { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mSettingsView = inflater.inflate(R.layout.activity_calibrate_joystick, null, false); + + SeekBar sb = (SeekBar) mSettingsView.findViewById(R.id.seekBar); + sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + return; + } + Apple2Preferences.JOYSTICK_DIVIDER.saveInt(mActivity, progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + sb.setMax(0); // http://stackoverflow.com/questions/10278467/seekbar-not-setting-actual-progress-setprogress-not-working-on-early-android + sb.setMax(Apple2Preferences.JOYSTICK_DIVIDER_NUM_CHOICES); + sb.setProgress(Apple2Preferences.JOYSTICK_DIVIDER.intValue(mActivity)); + + mTouchMenuEnabled = Apple2Preferences.TOUCH_MENU_ENABLED.booleanValue(mActivity); + Apple2Preferences.nativeSetTouchMenuEnabled(false); + mSavedTouchDevice = Apple2Preferences.CURRENT_TOUCH_DEVICE.intValue(mActivity); + Apple2Preferences.nativeSetCurrentTouchDevice(variant.ordinal()); + if (variant == Apple2Preferences.TouchDeviceVariant.JOYSTICK) { + Apple2Preferences.loadAllJoystickButtons(mActivity); + } else { + Apple2Preferences.loadAllKeypadKeys(mActivity); + } + + Apple2Preferences.nativeTouchDeviceBeginCalibrationMode(); + } + + public final boolean isCalibrating() { + return true; + } + + public void onKeyTapCalibrationEvent(char ascii, int scancode) { + /* ... */ + } + + public void show() { + if (isShowing()) { + return; + } + mActivity.pushApple2View(this); + } + + public void dismiss() { + for (Apple2MenuView apple2MenuView : mViewStack) { + if (apple2MenuView != this) { + mActivity.pushApple2View(apple2MenuView); + } + } + + Apple2Preferences.nativeTouchDeviceEndCalibrationMode(); + Apple2Preferences.nativeSetTouchMenuEnabled(mTouchMenuEnabled); + Apple2Preferences.nativeSetCurrentTouchDevice(mSavedTouchDevice); + + mActivity.popApple2View(this); + } + + public void dismissAll() { + dismiss(); + } + + public boolean isShowing() { + return mSettingsView.getParent() != null; + } + + public View getView() { + return mSettingsView; + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickSettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickSettingsMenu.java new file mode 100644 index 00000000..c217bf03 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2JoystickSettingsMenu.java @@ -0,0 +1,447 @@ +/* + * 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.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import java.util.ArrayList; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2JoystickSettingsMenu extends Apple2AbstractMenu { + + private final static String TAG = "Apple2JoystickSettingsMenu"; + + public Apple2JoystickSettingsMenu(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return true; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return true; + } + + protected enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + JOYSTICK_TAP_BUTTON { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_tap_button); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_tap_button_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + _alertDialogHandleSelection(activity, R.string.joystick_button_tap_button, new String[]{ + activity.getResources().getString(R.string.joystick_button_button_none), + activity.getResources().getString(R.string.joystick_button_button1), + activity.getResources().getString(R.string.joystick_button_button2), + activity.getResources().getString(R.string.joystick_button_button_both), + }, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_TAP_BUTTON.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.JOYSTICK_TAP_BUTTON.saveTouchJoystickButtons(activity, Apple2Preferences.TouchJoystickButtons.values()[value]); + } + }); + } + }, + JOYSTICK_SWIPEUP_BUTTON { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_swipe_up_button); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_swipe_up_button_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + _alertDialogHandleSelection(activity, R.string.joystick_button_swipe_up_button, new String[]{ + activity.getResources().getString(R.string.joystick_button_button_none), + activity.getResources().getString(R.string.joystick_button_button1), + activity.getResources().getString(R.string.joystick_button_button2), + activity.getResources().getString(R.string.joystick_button_button_both), + }, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_SWIPEUP_BUTTON.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.JOYSTICK_SWIPEUP_BUTTON.saveTouchJoystickButtons(activity, Apple2Preferences.TouchJoystickButtons.values()[value]); + } + }); + } + }, + JOYSTICK_SWIPEDOWN_BUTTON { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_swipe_down_button); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_swipe_down_button_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + _alertDialogHandleSelection(activity, R.string.joystick_button_swipe_down_button, new String[]{ + activity.getResources().getString(R.string.joystick_button_button_none), + activity.getResources().getString(R.string.joystick_button_button1), + activity.getResources().getString(R.string.joystick_button_button2), + activity.getResources().getString(R.string.joystick_button_button_both), + }, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_SWIPEDOWN_BUTTON.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.JOYSTICK_SWIPEDOWN_BUTTON.saveTouchJoystickButtons(activity, Apple2Preferences.TouchJoystickButtons.values()[value]); + } + }); + } + }, + JOYSTICK_CALIBRATE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_calibrate); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_calibrate_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + ArrayList viewStack = new ArrayList(); + { + int idx = 0; + while (true) { + Apple2MenuView apple2MenuView = activity.peekApple2View(idx); + if (apple2MenuView == null) { + break; + } + viewStack.add(apple2MenuView); + ++idx; + } + } + + Apple2JoystickCalibration calibration = new Apple2JoystickCalibration(activity, viewStack, Apple2Preferences.TouchDeviceVariant.JOYSTICK); + + // show this new view... + calibration.show(); + + // ...with nothing else underneath 'cept the emulator OpenGL layer + for (Apple2MenuView apple2MenuView : viewStack) { + activity.popApple2View(apple2MenuView); + } + } + }, + JOYSTICK_TAPDELAY { + @Override + public final String getTitle(Apple2Activity activity) { + return ""; + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_tapdelay_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.TAPDELAY_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.JOYSTICK_TAPDELAY.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_TAPDELAY.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + (((float) progress / Apple2Preferences.TAPDELAY_NUM_CHOICES) * Apple2Preferences.TAPDELAY_SCALE)); + } + }); + } + }, + JOYSTICK_ADVANCED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced_joystick_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2JoystickSettingsMenu.JoystickAdvanced(activity).show(); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } + + public static class JoystickAdvanced extends Apple2AbstractMenu { + + private final static String TAG = "JoystickAdvanced"; + + public JoystickAdvanced(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return false; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return position == SETTINGS.JOYSTICK_AXIS_ON_LEFT.ordinal(); + } + + protected enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + JOYSTICK_VISIBILITY { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_visible); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_visible_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.JOYSTICK_VISIBILITY.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.JOYSTICK_VISIBILITY.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + JOYSTICK_AXIS_ON_LEFT { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_axisleft); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_axisleft_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.JOYSTICK_AXIS_ON_LEFT.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.JOYSTICK_AXIS_ON_LEFT.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + JOYSTICK_AXIS_SENSITIVIY { + @Override + public final String getTitle(Apple2Activity activity) { + return ""; + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_axis_sensitivity_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.JOYSTICK_AXIS_SENSITIVITY_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.JOYSTICK_AXIS_SENSITIVIY.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_AXIS_SENSITIVIY.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + saveInt(progress); + int percent = (int) (Apple2Preferences.JOYSTICK_AXIS_SENSITIVIY.floatValue(activity) * 100.f); + seekBarValue.setText("" + percent + "%"); + } + }); + } + }, + JOYSTICK_BUTTON_THRESHOLD { + @Override + public final String getTitle(Apple2Activity activity) { + return ""; + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_button_threshold_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.JOYSTICK_BUTTON_THRESHOLD_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + if (progress == 0) { + progress = 1; + } + Apple2Preferences.JOYSTICK_BUTTON_THRESHOLD.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.JOYSTICK_BUTTON_THRESHOLD.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + int threshold = progress * Apple2Preferences.JOYSTICK_BUTTON_THRESHOLD_STEP; + seekBarValue.setText("" + threshold + " pts"); + } + }); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeyboardSettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeyboardSettingsMenu.java new file mode 100644 index 00000000..8c6fe7b3 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeyboardSettingsMenu.java @@ -0,0 +1,315 @@ +/* + * 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.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.Arrays; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2KeyboardSettingsMenu extends Apple2AbstractMenu { + + private final static String TAG = "KeyboardSettingsMenu"; + + // These settings must match native side + public final static int MOUSETEXT_BEGIN = 0x80; + public final static int MOUSETEXT_CLOSEDAPPLE = MOUSETEXT_BEGIN/*+0x00*/; + public final static int MOUSETEXT_OPENAPPLE = MOUSETEXT_BEGIN + 0x01; + public final static int MOUSETEXT_LEFT = MOUSETEXT_BEGIN + 0x08; + public final static int MOUSETEXT_UP = MOUSETEXT_BEGIN + 0x0b; + public final static int MOUSETEXT_DOWN = MOUSETEXT_BEGIN + 0x0a; + public final static int MOUSETEXT_RIGHT = MOUSETEXT_BEGIN + 0x15; + + public final static int ICONTEXT_BEGIN = 0xA0; + public final static int ICONTEXT_VISUAL_SPACE = ICONTEXT_BEGIN + 0x11; + public final static int ICONTEXT_KBD_BEGIN = ICONTEXT_BEGIN + 0x13; + public final static int ICONTEXT_CTRL = ICONTEXT_KBD_BEGIN/* + 0x00*/; + public final static int ICONTEXT_ESC = ICONTEXT_KBD_BEGIN + 0x09; + public final static int ICONTEXT_RETURN = ICONTEXT_KBD_BEGIN + 0x0A; + public final static int ICONTEXT_NONACTION = ICONTEXT_KBD_BEGIN + 0x0C; + + public final static int SCANCODE_A = 30; + public final static int SCANCODE_D = 32; + public final static int SCANCODE_F = 33; + public final static int SCANCODE_H = 35; + public final static int SCANCODE_I = 23; + public final static int SCANCODE_J = 36; + public final static int SCANCODE_K = 37; + public final static int SCANCODE_L = 38; + public final static int SCANCODE_M = 50; + public final static int SCANCODE_N = 49; + public final static int SCANCODE_O = 24; + public final static int SCANCODE_U = 22; + public final static int SCANCODE_W = 17; + public final static int SCANCODE_X = 45; + public final static int SCANCODE_Y = 21; + public final static int SCANCODE_Z = 44; + public final static int SCANCODE_SPACE = 57; + public final static int SCANCODE_UP = 103; + public final static int SCANCODE_LEFT = 105; + public final static int SCANCODE_RIGHT = 106; + public final static int SCANCODE_DOWN = 108; + public final static int SCANCODE_COMMA = 51; + + public Apple2KeyboardSettingsMenu(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return true; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return true; + } + + protected enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + KEYBOARD_VISIBILITY_INACTIVE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_visibility_inactive); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_visibility_inactive_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.KEYBOARD_VISIBILITY_INACTIVE.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.KEYBOARD_VISIBILITY_INACTIVE.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + ((float) progress / Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES)); + } + }); + } + }, + KEYBOARD_VISIBILITY_ACTIVE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_visibility_active); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_visibility_active_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.KEYBOARD_VISIBILITY_ACTIVE.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.KEYBOARD_VISIBILITY_ACTIVE.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + ((float) progress / Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES)); + } + }); + } + }, + KEYBOARD_ENABLE_CLICK { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_click_enabled); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_click_enabled_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.KEYBOARD_CLICK_ENABLED.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.KEYBOARD_CLICK_ENABLED.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + KEYBOARD_ENABLE_LOWERCASE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_lowercase_enabled); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_lowercase_enabled_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.KEYBOARD_LOWERCASE_ENABLED.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.KEYBOARD_LOWERCASE_ENABLED.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + KEYBOARD_CHOOSE_ALT { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_choose_alt); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_choose_alt_summary); + } + + @Override + public final View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + + File extKeyboardDir = Apple2DisksMenu.getExternalStorageDirectory(); + + FilenameFilter kbdJsonFilter = new FilenameFilter() { + public boolean accept(File dir, String name) { + File file = new File(dir, name); + if (file.isDirectory()) { + return false; + } + + // check file extensions ... sigh ... no String.endsWithIgnoreCase() ? + + final String extension = ".kbd.json"; + final int nameLen = name.length(); + final int extLen = extension.length(); + if (nameLen <= extLen) { + return false; + } + + String suffix = name.substring(nameLen - extLen, nameLen); + return (suffix.equalsIgnoreCase(extension)); + } + }; + + File[] files = null; + if (extKeyboardDir != null) { + files = extKeyboardDir.listFiles(kbdJsonFilter); + } + if (files == null) { + // read keyboard data from /data/data/... + File keyboardDir = new File(Apple2DisksMenu.getDataDir(activity) + File.separator + "keyboards"); + files = keyboardDir.listFiles(kbdJsonFilter); + if (files == null) { + Log.e(TAG, "OOPS, could not read keyboard data directory"); + return; + } + } + + Arrays.sort(files); + + final File[] allFiles = files; + String[] titles = new String[allFiles.length]; + int idx = 0; + for (File file : allFiles) { + titles[idx] = file.getName(); + ++idx; + } + + final String keyboardDirName = extKeyboardDir == null ? "Keyboards" : extKeyboardDir.getPath(); + + _alertDialogHandleSelection(activity, keyboardDirName, titles, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.KEYBOARD_ALT.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.KEYBOARD_ALT.saveInt(activity, value); + String path = allFiles[value].getPath(); + Apple2Preferences.KEYBOARD_ALT_PATH.saveString(activity, path); + } + }); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadChooser.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadChooser.java new file mode 100644 index 00000000..0c7acfed --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadChooser.java @@ -0,0 +1,340 @@ +/* + * 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.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import java.util.ArrayList; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2KeypadChooser implements Apple2MenuView { + + private final static String TAG = "Apple2KeypadChooser"; + + private Apple2Activity mActivity = null; + private View mSettingsView = null; + private ArrayList mViewStack = null; + private TextView mCurrentChoicePrompt = null; + + private String[] foo = null; + + private STATE_MACHINE mChooserState = STATE_MACHINE.CHOOSE_NORTHWEST; + + private boolean mTouchMenuEnabled = false; + private int mSavedTouchDevice = Apple2Preferences.TouchDeviceVariant.NONE.ordinal(); + + public Apple2KeypadChooser(Apple2Activity activity, ArrayList viewStack) { + mActivity = activity; + mViewStack = viewStack; + setup(); + } + + public final boolean isCalibrating() { + return true; + } + + public void onKeyTapCalibrationEvent(char ascii, int scancode) { + if (ascii == Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION) { + scancode = -1; + } + if (scancode == 0) { + return; + } + + String asciiStr = asciiRepresentation(ascii); + Log.d(TAG, "ascii:'" + asciiStr + "' scancode:" + scancode); + mChooserState.setValues(mActivity, ascii, scancode); + Apple2Preferences.nativeSetCurrentTouchDevice(Apple2Preferences.TouchDeviceVariant.JOYSTICK_KEYPAD.ordinal()); + mCurrentChoicePrompt.setText(getNextChoiceString() + asciiStr); + switch (mChooserState) { + case CHOOSE_TAP: + mActivity.nativeOnTouch(MotionEvent.ACTION_DOWN, 1, 0, new float[]{400.f}, new float[]{400.f}); + mActivity.nativeOnTouch(MotionEvent.ACTION_UP, 1, 0, new float[]{400.f}, new float[]{400.f}); + break; + case CHOOSE_SWIPEDOWN: + mActivity.nativeOnTouch(MotionEvent.ACTION_DOWN, 1, 0, new float[]{400.f}, new float[]{400.f}); + mActivity.nativeOnTouch(MotionEvent.ACTION_MOVE, 1, 0, new float[]{400.f}, new float[]{600.f}); + mActivity.nativeOnTouch(MotionEvent.ACTION_UP, 1, 0, new float[]{400.f}, new float[]{600.f}); + break; + case CHOOSE_SWIPEUP: + mActivity.nativeOnTouch(MotionEvent.ACTION_DOWN, 1, 0, new float[]{400.f}, new float[]{400.f}); + mActivity.nativeOnTouch(MotionEvent.ACTION_MOVE, 1, 0, new float[]{400.f}, new float[]{200.f}); + mActivity.nativeOnTouch(MotionEvent.ACTION_UP, 1, 0, new float[]{400.f}, new float[]{200.f}); + break; + default: + break; + } + + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + mChooserState = mChooserState.next(); + mCurrentChoicePrompt.setText(getNextChoiceString()); + Apple2Preferences.nativeSetCurrentTouchDevice(Apple2Preferences.TouchDeviceVariant.KEYBOARD.ordinal()); + } + }, 1000); + } + + public void show() { + if (isShowing()) { + return; + } + mActivity.pushApple2View(this); + } + + public void dismiss() { + for (Apple2MenuView apple2MenuView : mViewStack) { + if (apple2MenuView != this) { + mActivity.pushApple2View(apple2MenuView); + } + } + + Apple2Preferences.nativeTouchDeviceEndCalibrationMode(); + Apple2Preferences.nativeSetTouchMenuEnabled(mTouchMenuEnabled); + Apple2Preferences.nativeSetCurrentTouchDevice(mSavedTouchDevice); + + mActivity.popApple2View(this); + } + + public void dismissAll() { + dismiss(); + } + + public boolean isShowing() { + return mSettingsView.getParent() != null; + } + + public View getView() { + return mSettingsView; + } + + // ------------------------------------------------------------------------ + // internals + + private void setup() { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mSettingsView = inflater.inflate(R.layout.activity_chooser_keypad, null, false); + + mCurrentChoicePrompt = (TextView) mSettingsView.findViewById(R.id.currentChoicePrompt); + mCurrentChoicePrompt.setText(getNextChoiceString()); + + Button skipButton = (Button) mSettingsView.findViewById(R.id.skipButton); + skipButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Apple2KeypadChooser.this.onKeyTapCalibrationEvent((char)Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }); + + // temporarily undo these native touch settings while calibrating... + mTouchMenuEnabled = Apple2Preferences.TOUCH_MENU_ENABLED.booleanValue(mActivity); + Apple2Preferences.nativeSetTouchMenuEnabled(false); + mSavedTouchDevice = Apple2Preferences.CURRENT_TOUCH_DEVICE.intValue(mActivity); + Apple2Preferences.nativeSetCurrentTouchDevice(Apple2Preferences.TouchDeviceVariant.KEYBOARD.ordinal()); + + Apple2Preferences.nativeTouchDeviceBeginCalibrationMode(); + } + + private String asciiRepresentation(char ascii) { + switch (ascii) { + case Apple2KeyboardSettingsMenu.MOUSETEXT_OPENAPPLE: + return mActivity.getResources().getString(R.string.key_open_apple); + case Apple2KeyboardSettingsMenu.MOUSETEXT_CLOSEDAPPLE: + return mActivity.getResources().getString(R.string.key_closed_apple); + case Apple2KeyboardSettingsMenu.MOUSETEXT_UP: + return mActivity.getResources().getString(R.string.key_up); + case Apple2KeyboardSettingsMenu.MOUSETEXT_LEFT: + return mActivity.getResources().getString(R.string.key_left); + case Apple2KeyboardSettingsMenu.MOUSETEXT_RIGHT: + return mActivity.getResources().getString(R.string.key_right); + case Apple2KeyboardSettingsMenu.MOUSETEXT_DOWN: + return mActivity.getResources().getString(R.string.key_down); + case Apple2KeyboardSettingsMenu.ICONTEXT_CTRL: + return mActivity.getResources().getString(R.string.key_ctrl); + case Apple2KeyboardSettingsMenu.ICONTEXT_ESC: + return mActivity.getResources().getString(R.string.key_esc); + case Apple2KeyboardSettingsMenu.ICONTEXT_RETURN: + return mActivity.getResources().getString(R.string.key_ret); + case Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION: + return mActivity.getResources().getString(R.string.key_none); + case ' ': + case Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE: + return mActivity.getResources().getString(R.string.key_space); + default: + return "" + ascii; + } + } + + private String getNextChoiceString() { + String choose = mActivity.getResources().getString(R.string.keypad_choose_current); + return choose.replace("XXX", mChooserState.getKeyName(mActivity)); + } + + private enum STATE_MACHINE { + CHOOSE_NORTHWEST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_ul); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_NORTH { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_up); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_NORTHEAST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_ur); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_WEST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_l); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_CENTER { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_c); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_EAST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_r); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_SOUTHWEST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_dl); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_SOUTH { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_dn); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_SOUTHEAST { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_axis_dr); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_TAP { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_button_tap); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_SWIPEUP { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_button_swipeup); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, ascii, scancode); + } + }, + CHOOSE_SWIPEDOWN { + @Override + public String getKeyName(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_key_button_swipedown); + } + + @Override + public void setValues(Apple2Activity activity, char ascii, int scancode) { + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, ascii, scancode); + } + }; + + public static final int size = STATE_MACHINE.values().length; + + public abstract void setValues(Apple2Activity activity, char ascii, int scancode); + + public abstract String getKeyName(Apple2Activity activity); + + public STATE_MACHINE next() { + int nextOrd = this.ordinal() + 1; + if (nextOrd >= size) { + nextOrd = 0; + } + STATE_MACHINE nextState = STATE_MACHINE.values()[nextOrd]; + return nextState; + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadSettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadSettingsMenu.java new file mode 100644 index 00000000..01ba3c54 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2KeypadSettingsMenu.java @@ -0,0 +1,303 @@ +/* + * 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.view.View; +import android.widget.TextView; + +import java.util.ArrayList; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2KeypadSettingsMenu extends Apple2AbstractMenu { + + private final static String TAG = "Apple2KeypadSettingsMenu"; + + public Apple2KeypadSettingsMenu(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return true; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return true; + } + + enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + KEYPAD_CHOOSE_KEYS { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_choose); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_choose_summary); + } + + @Override + public final View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + String[] titles = new String[Apple2Preferences.KeypadPreset.size + 1]; + titles[0] = activity.getResources().getString(R.string.keypad_preset_custom); + System.arraycopy(Apple2Preferences.KeypadPreset.titles(activity), 0, titles, 1, Apple2Preferences.KeypadPreset.size); + + _alertDialogHandleSelection(activity, R.string.keypad_choose_title, titles, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.KEYPAD_KEYS.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.KEYPAD_KEYS.saveInt(activity, value); + if (value == 0) { + Apple2KeypadSettingsMenu keypadSettingsMenu = (Apple2KeypadSettingsMenu) settingsMenu; + keypadSettingsMenu.chooseKeys(activity); + } else { + Apple2Preferences.KeypadPreset.values()[value - 1].apply(activity); + } + } + }); + } + }, + KEYPAD_CALIBRATE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_calibrate); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_calibrate_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + ArrayList viewStack = new ArrayList(); + { + int idx = 0; + while (true) { + Apple2MenuView apple2MenuView = activity.peekApple2View(idx); + if (apple2MenuView == null) { + break; + } + viewStack.add(apple2MenuView); + ++idx; + } + } + + Apple2JoystickCalibration calibration = new Apple2JoystickCalibration(activity, viewStack, Apple2Preferences.TouchDeviceVariant.JOYSTICK_KEYPAD); + + // show this new view... + calibration.show(); + + // ...with nothing else underneath 'cept the emulator OpenGL layer + for (Apple2MenuView apple2MenuView : viewStack) { + activity.popApple2View(apple2MenuView); + } + } + }, + KEYPAD_ADVANCED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced_joystick_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2KeypadSettingsMenu.KeypadAdvanced(activity).show(); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } + + // ------------------------------------------------------------------------ + // internals + + private void chooseKeys(Apple2Activity activity) { + ArrayList viewStack = new ArrayList(); + { + int idx = 0; + while (true) { + Apple2MenuView apple2MenuView = activity.peekApple2View(idx); + if (apple2MenuView == null) { + break; + } + viewStack.add(apple2MenuView); + ++idx; + } + } + + Apple2KeypadChooser chooser = new Apple2KeypadChooser(activity, viewStack); + + // show this new view... + chooser.show(); + + // ...with nothing else underneath 'cept the emulator OpenGL layer + for (Apple2MenuView apple2MenuView : viewStack) { + activity.popApple2View(apple2MenuView); + } + } + + protected static class KeypadAdvanced extends Apple2AbstractMenu { + + private final static String TAG = "KeypadAdvanced"; + + public KeypadAdvanced(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return false; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return position == SETTINGS.JOYSTICK_ADVANCED.ordinal(); + } + + protected enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + KEYREPEAT_THRESHOLD { + @Override + public final String getTitle(Apple2Activity activity) { + return ""; + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_repeat_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.KEYREPEAT_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.KEYREPEAT_THRESHOLD.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.KEYREPEAT_THRESHOLD.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + ((float) progress / Apple2Preferences.KEYREPEAT_NUM_CHOICES)); + } + }); + } + }, + JOYSTICK_ADVANCED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced_joystick); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.settings_advanced_joystick_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2JoystickSettingsMenu.JoystickAdvanced(activity).show(); + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } + } +} + diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java new file mode 100644 index 00000000..e3c27d96 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MainMenu.java @@ -0,0 +1,202 @@ +/* + * 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.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.TextView; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2MainMenu { + + private final static int MENU_INSET = 20; + private final static String TAG = "Apple2MainMenu"; + + private Apple2Activity mActivity = null; + private Apple2View mParentView = null; + private PopupWindow mMainMenuPopup = null; + + public Apple2MainMenu(Apple2Activity activity, Apple2View parent) { + mActivity = activity; + mParentView = parent; + setup(); + } + + enum SETTINGS { + SHOW_SETTINGS { + @Override public String getTitle(Context ctx) { + return ctx.getResources().getString(R.string.menu_settings); + } + @Override public String getSummary(Context ctx) { + return ctx.getResources().getString(R.string.menu_settings_summary); + } + @Override public void handleSelection(Apple2MainMenu mainMenu) { + mainMenu.showSettings(); + } + }, + LOAD_DISK { + @Override public String getTitle(Context ctx) { + return ctx.getResources().getString(R.string.menu_disks); + } + @Override public String getSummary(Context ctx) { + return ctx.getResources().getString(R.string.menu_disks_summary); + } + @Override public void handleSelection(Apple2MainMenu mainMenu) { + mainMenu.showDisksMenu(); + } + }, + REBOOT_EMULATOR { + @Override public String getTitle(Context ctx) { + return ctx.getResources().getString(R.string.reboot); + } + @Override public String getSummary(Context ctx) { + return ctx.getResources().getString(R.string.reboot_summary); + } + @Override public void handleSelection(Apple2MainMenu mainMenu) { + mainMenu.mActivity.maybeReboot(); + } + }, + QUIT_EMULATOR { + @Override public String getTitle(Context ctx) { + return ctx.getResources().getString(R.string.quit); + } + @Override public String getSummary(Context ctx) { + return ctx.getResources().getString(R.string.quit_summary); + } + @Override public void handleSelection(Apple2MainMenu mainMenu) { + mainMenu.mActivity.maybeQuitApp(); + } + }; + + public abstract String getTitle(Context ctx); + public abstract String getSummary(Context ctx); + public abstract void handleSelection(Apple2MainMenu mainMenu); + + public static String[] titles(Context ctx) { + String[] titles = new String[values().length]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(ctx); + } + return titles; + } + } + + private void setup() { + + LayoutInflater inflater = (LayoutInflater)mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View listLayout=inflater.inflate(R.layout.activity_main_menu, null, false); + ListView mainMenuView = (ListView)listLayout.findViewById(R.id.main_popup_menu); + mainMenuView.setEnabled(true); + LinearLayout mainPopupContainer = (LinearLayout)listLayout.findViewById(R.id.main_popup_container); + + final String[] values = SETTINGS.titles(mActivity); + + ArrayAdapter adapter = new ArrayAdapter(mActivity, android.R.layout.simple_list_item_2, android.R.id.text1, values) { + @Override + public boolean areAllItemsEnabled() { + return true; + } + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + TextView tv = (TextView)view.findViewById(android.R.id.text2); + SETTINGS setting = SETTINGS.values()[position]; + tv.setText(setting.getSummary(mActivity)); + return view; + } + }; + mainMenuView.setAdapter(adapter); + mainMenuView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Log.d(TAG, "position:"+position+" tapped..."); + SETTINGS setting = SETTINGS.values()[position]; + setting.handleSelection(Apple2MainMenu.this); + } + }); + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) { + mMainMenuPopup = new PopupWindow(mainPopupContainer, android.app.ActionBar.LayoutParams.WRAP_CONTENT, android.app.ActionBar.LayoutParams.WRAP_CONTENT, true); + } else { + // 2015/03/11 ... there may well be a less hackish way to support Gingerbread, but eh ... diminishing returns + final int TOTAL_MARGINS = 16; + int totalHeight = TOTAL_MARGINS; + int maxWidth = 0; + for (int i = 0; i < adapter.getCount(); i++) { + View view = adapter.getView(i, null, mainMenuView); + view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + totalHeight += view.getMeasuredHeight(); + int width = view.getMeasuredWidth(); + if (width > maxWidth) { + maxWidth = width; + } + } + mMainMenuPopup = new PopupWindow(mainPopupContainer, maxWidth+TOTAL_MARGINS, totalHeight, true); + } + + // This kludgery allows touching the outside or back-buttoning to dismiss + mMainMenuPopup.setBackgroundDrawable(new BitmapDrawable()); + mMainMenuPopup.setOutsideTouchable(true); + mMainMenuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + Apple2MainMenu.this.mActivity.maybeResumeCPU(); + } + }); + } + + public void showDisksMenu() { + Apple2DisksMenu disksMenu = mActivity.getDisksMenu(); + disksMenu.show(); + mMainMenuPopup.dismiss(); + } + + public void showSettings() { + Apple2SettingsMenu settings = mActivity.getSettingsMenu(); + settings.show(); + mMainMenuPopup.dismiss(); + } + + public void show() { + if (mMainMenuPopup.isShowing()) { + return; + } + + mActivity.nativeEmulationPause(); + + mMainMenuPopup.showAtLocation(mParentView, Gravity.CENTER, 0, 0); + } + + public void dismiss() { + if (mMainMenuPopup.isShowing()) { + mMainMenuPopup.dismiss(); + // listener will resume ... + } + } + + public boolean isShowing() { + return mMainMenuPopup.isShowing(); + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MenuView.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MenuView.java new file mode 100644 index 00000000..4e56de48 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2MenuView.java @@ -0,0 +1,31 @@ +/* + * 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.view.View; + +public interface Apple2MenuView { + + public void show(); + + public boolean isShowing(); + + public void dismiss(); + + public void dismissAll(); + + public View getView(); + + public boolean isCalibrating(); + + public void onKeyTapCalibrationEvent(char ascii, int scancode); +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java new file mode 100644 index 00000000..1f38423b --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Preferences.java @@ -0,0 +1,1086 @@ +/* + * 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.util.Log; + +import java.io.File; + +import org.deadc0de.apple2ix.basic.R; + +public enum Apple2Preferences { + FIRST_TIME_CONFIGURED { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public void saveBoolean(Apple2Activity activity, boolean ignored) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), true).apply(); + } + }, + CURRENT_DISK_PATH { + @Override + public void load(final Apple2Activity activity) { + activity.getDisksMenu().setPathStackJSON(stringValue(activity)); + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), "[]"); + } + + @Override + public void saveString(Apple2Activity activity, String value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putString(toString(), value).apply(); + //load(activity); + } + }, + CURRENT_DRIVE_A_BUTTON { + @Override + public void load(final Apple2Activity activity) { + /* ... */ + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + + @Override + public void saveBoolean(Apple2Activity activity, boolean value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), value).apply(); + //load(activity); + } + }, + CURRENT_DISK_RO_BUTTON { + @Override + public void load(final Apple2Activity activity) { + /* ... */ + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + + @Override + public void saveBoolean(Apple2Activity activity, boolean value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), value).apply(); + //load(activity); + } + }, + CURRENT_DISK_A { + @Override + public void load(final Apple2Activity activity) { + insertDisk(activity, stringValue(activity), /*driveA:*/true, /*readOnly:*/CURRENT_DISK_A_RO.booleanValue(activity)); + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + + @Override + public void saveString(Apple2Activity activity, String str) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putString(toString(), str).apply(); + load(activity); + } + }, + CURRENT_DISK_A_RO { + @Override + public void load(final Apple2Activity activity) { + /* ... */ + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + + @Override + public void saveBoolean(Apple2Activity activity, boolean value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), value).apply(); + //load(activity); + } + }, + CURRENT_DISK_B { + @Override + public void load(final Apple2Activity activity) { + insertDisk(activity, stringValue(activity), /*driveA:*/false, /*readOnly:*/CURRENT_DISK_B_RO.booleanValue(activity)); + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + + @Override + public void saveString(Apple2Activity activity, String str) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putString(toString(), str).apply(); + load(activity); + } + }, + CURRENT_DISK_B_RO { + @Override + public void load(final Apple2Activity activity) { + /* ... */ + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + + @Override + public void saveBoolean(Apple2Activity activity, boolean value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), value).apply(); + //load(activity); + } + }, + CPU_SPEED_PERCENT { + @Override + public void load(Apple2Activity activity) { + nativeSetCPUSpeed(intValue(activity)); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 100); + } + }, + HIRES_COLOR { + @Override + public void load(Apple2Activity activity) { + nativeSetColor(intValue(activity)); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), HiresColor.INTERPOLATED.ordinal()); + } + }, + SPEAKER_ENABLED { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + boolean result = nativeSetSpeakerEnabled(enabled); + if (enabled && !result) { + warnError(activity, R.string.speaker_disabled_title, R.string.speaker_disabled_mesg); + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), false).apply(); + } + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + SPEAKER_VOLUME { + @Override + public void load(Apple2Activity activity) { + nativeSetSpeakerVolume(intValue(activity)); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), Volume.MEDIUM.ordinal()); + } + }, + MOCKINGBOARD_ENABLED { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + boolean result = nativeSetMockingboardEnabled(enabled); + if (enabled && !result) { + warnError(activity, R.string.mockingboard_disabled_title, R.string.mockingboard_disabled_mesg); + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), false).apply(); + } + } + }, + MOCKINGBOARD_VOLUME { + @Override + public void load(Apple2Activity activity) { + nativeSetMockingboardVolume(intValue(activity)); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), Volume.MEDIUM.ordinal()); + } + }, + AUDIO_LATENCY { + @Override + public void load(Apple2Activity activity) { + int tick = intValue(activity); + nativeSetAudioLatency(((float) tick / AUDIO_LATENCY_NUM_CHOICES)); + } + + @Override + public int intValue(Apple2Activity activity) { + + int defaultLatency = 0; + int sampleRateCanary = DevicePropertyCalculator.getRecommendedSampleRate(activity); + if (sampleRateCanary == DevicePropertyCalculator.defaultSampleRate) { + // quite possibly an audio-challenged device + defaultLatency = 8; // /AUDIO_LATENCY_NUM_CHOICES -> 0.4f + } else { + // reasonable default for high-end devices + defaultLatency = 5; // /AUDIO_LATENCY_NUM_CHOICES -> 0.25f + } + + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), defaultLatency); + } + }, + CURRENT_TOUCH_DEVICE { + @Override + public void load(Apple2Activity activity) { + int intVariant = intValue(activity); + nativeSetCurrentTouchDevice(intVariant); + TouchDeviceVariant variant = TouchDeviceVariant.values()[intVariant]; + switch (variant) { + case JOYSTICK: + loadAllJoystickButtons(activity); + break; + case JOYSTICK_KEYPAD: + loadAllKeypadKeys(activity); + break; + case KEYBOARD: + break; + default: + break; + } + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), TouchDeviceVariant.KEYBOARD.ordinal()); + } + }, + TOUCH_MENU_ENABLED { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + nativeSetTouchMenuEnabled(enabled); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + TOUCH_MENU_VISIBILITY { + @Override + public void load(Apple2Activity activity) { + int setting = intValue(activity); + float alpha = (float) setting / AUDIO_LATENCY_NUM_CHOICES; + nativeSetTouchMenuVisibility(alpha); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 5); + } + }, + JOYSTICK_AXIS_SENSITIVIY { + @Override + public void load(Apple2Activity activity) { + float sensitivity = floatValue(activity); + nativeSetTouchJoystickAxisSensitivity(sensitivity); + } + + @Override + public int intValue(Apple2Activity activity) { + final int pivot = JOYSTICK_AXIS_SENSITIVITY_DEC_NUMCHOICES; + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), pivot); + } + + @Override + public float floatValue(Apple2Activity activity) { + int tick = intValue(activity); + final int pivot = JOYSTICK_AXIS_SENSITIVITY_DEC_NUMCHOICES; + float sensitivity = 1.f; + if (tick < pivot) { + int decAmount = (pivot - tick); + sensitivity -= (JOYSTICK_AXIS_SENSITIVITY_DEC_STEP * decAmount); + } else if (tick > pivot) { + int incAmount = (tick - pivot); + sensitivity += (JOYSTICK_AXIS_SENSITIVITY_INC_STEP * incAmount); + } + return sensitivity; + } + }, + JOYSTICK_BUTTON_THRESHOLD { + @Override + public void load(Apple2Activity activity) { + int tick = intValue(activity); + nativeSetTouchJoystickButtonSwitchThreshold(tick * JOYSTICK_BUTTON_THRESHOLD_STEP); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 5); + } + }, + JOYSTICK_TAP_BUTTON { + @Override + public void load(Apple2Activity activity) { + loadAllJoystickButtons(activity); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), TouchJoystickButtons.BUTTON1.ordinal()); + } + }, + JOYSTICK_TAPDELAY { + @Override + public void load(Apple2Activity activity) { + int tick = intValue(activity); + nativeSetTouchJoystickTapDelay(((float) tick / TAPDELAY_NUM_CHOICES) * TAPDELAY_SCALE); + } + + @Override + public int intValue(Apple2Activity activity) { + int defaultLatency = 3; // /TAPDELAY_NUM_CHOICES * TAPDELAY_SCALE -> 0.075f + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), defaultLatency); + } + }, + JOYSTICK_SWIPEUP_BUTTON { + @Override + public void load(Apple2Activity activity) { + loadAllJoystickButtons(activity); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), TouchJoystickButtons.BOTH.ordinal()); + } + }, + JOYSTICK_SWIPEDOWN_BUTTON { + @Override + public void load(Apple2Activity activity) { + loadAllJoystickButtons(activity); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), TouchJoystickButtons.BUTTON2.ordinal()); + } + }, + JOYSTICK_AXIS_ON_LEFT { + @Override + public void load(Apple2Activity activity) { + nativeTouchJoystickSetAxisOnLeft(booleanValue(activity)); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + JOYSTICK_DIVIDER { + @Override + public void load(Apple2Activity activity) { + int tick = intValue(activity); + nativeTouchJoystickSetScreenDivision(((float) tick / JOYSTICK_DIVIDER_NUM_CHOICES)); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), JOYSTICK_DIVIDER_NUM_CHOICES >> 1); + } + }, + JOYSTICK_VISIBILITY { + @Override + public void load(Apple2Activity activity) { + nativeSetTouchJoystickVisibility(booleanValue(activity)); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + KEYPAD_KEYS { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), KeypadPreset.IJKM_SPACE.ordinal() + 1); + } + }, + KEYPAD_NORTHWEST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_NORTH_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_NORTHEAST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_WEST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_CENTER_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_EAST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_SOUTHWEST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_SOUTH_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_SOUTHEAST_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_TAP_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_SWIPEUP_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYPAD_SWIPEDOWN_KEY { + @Override + public void load(Apple2Activity activity) { + loadAllKeypadKeys(activity); + } + }, + KEYREPEAT_THRESHOLD { + @Override + public void load(Apple2Activity activity) { + int tick = intValue(activity); + nativeSetTouchDeviceKeyRepeatThreshold((float) tick / KEYREPEAT_NUM_CHOICES); + } + + @Override + public int intValue(Apple2Activity activity) { + int defaultLatency = KEYREPEAT_NUM_CHOICES / 4; + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), defaultLatency); + } + }, + KEYBOARD_ALT { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 0); + } + }, + KEYBOARD_ALT_PATH { + @Override + public void load(Apple2Activity activity) { + nativeLoadTouchKeyboardJSON(stringValue(activity)); + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + }, + KEYBOARD_VISIBILITY_ACTIVE { + @Override + public void load(Apple2Activity activity) { + int inactiveTick = KEYBOARD_VISIBILITY_INACTIVE.intValue(activity); + int activeTick = intValue(activity); + nativeSetTouchKeyboardVisibility((float) inactiveTick / ALPHA_SLIDER_NUM_CHOICES, (float) activeTick / ALPHA_SLIDER_NUM_CHOICES); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), ALPHA_SLIDER_NUM_CHOICES); + } + }, + KEYBOARD_VISIBILITY_INACTIVE { + @Override + public void load(Apple2Activity activity) { + int inactiveTick = intValue(activity); + int activeTick = KEYBOARD_VISIBILITY_ACTIVE.intValue(activity); + nativeSetTouchKeyboardVisibility((float) inactiveTick / ALPHA_SLIDER_NUM_CHOICES, (float) activeTick / ALPHA_SLIDER_NUM_CHOICES); + } + + @Override + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 5); + } + }, + KEYBOARD_LOWERCASE_ENABLED { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + nativeSetTouchKeyboardLowercaseEnabled(enabled); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), false); + } + }, + KEYBOARD_CLICK_ENABLED { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + CRASH_CHECK { + @Override + public void load(Apple2Activity activity) { + boolean enabled = booleanValue(activity); + } + + @Override + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), true); + } + }, + GL_VENDOR { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + }, + GL_RENDERER { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + }, + GL_VERSION { + @Override + public void load(Apple2Activity activity) { + /* ... */ + } + + @Override + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), ""); + } + }; + + + public enum HiresColor { + BW, + COLOR, + INTERPOLATED + } + + public enum Volume { + OFF(0), + ONE(1), + TWO(2), + THREE(3), + FOUR(4), + MEDIUM(5), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + MAX(10), + ELEVEN(11); + private int vol; + + Volume(int vol) { + this.vol = vol; + } + } + + public enum TouchDeviceVariant { + NONE(0), + JOYSTICK(1), + JOYSTICK_KEYPAD(2), + KEYBOARD(3); + private int dev; + + public static final int size = TouchDeviceVariant.values().length; + + TouchDeviceVariant(int dev) { + this.dev = dev; + } + + static TouchDeviceVariant next(int ord) { + ++ord; + if (ord >= size) { + ord = 1; + } + return TouchDeviceVariant.values()[ord]; + } + } + + public enum TouchJoystickButtons { + NONE(0), + BUTTON1(1), + BUTTON2(2), + BOTH(3); + private int butt; + + TouchJoystickButtons(int butt) { + this.butt = butt; + } + } + + public enum KeypadPreset { + ARROWS_SPACE { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_arrows_space); + } + + @Override + public void apply(Apple2Activity activity) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_UP, Apple2KeyboardSettingsMenu.SCANCODE_UP); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_LEFT, Apple2KeyboardSettingsMenu.SCANCODE_LEFT); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_RIGHT, Apple2KeyboardSettingsMenu.SCANCODE_RIGHT); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_DOWN, Apple2KeyboardSettingsMenu.SCANCODE_DOWN); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }, + AZ_LEFT_RIGHT_SPACE { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_az_left_right_space); + } + + @Override + public void apply(Apple2Activity activity) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) 'A', Apple2KeyboardSettingsMenu.SCANCODE_A); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_LEFT, Apple2KeyboardSettingsMenu.SCANCODE_LEFT); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_RIGHT, Apple2KeyboardSettingsMenu.SCANCODE_RIGHT); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) 'Z', Apple2KeyboardSettingsMenu.SCANCODE_Z); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }, + LEFT_RIGHT_SPACE { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_left_right_space); + } + + @Override + public void apply(Apple2Activity activity) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_LEFT, Apple2KeyboardSettingsMenu.SCANCODE_LEFT); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.MOUSETEXT_RIGHT, Apple2KeyboardSettingsMenu.SCANCODE_RIGHT); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }, + IJKM_SPACE { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_ijkm_space); + } + + @Override + public void apply(Apple2Activity activity) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) 'I', Apple2KeyboardSettingsMenu.SCANCODE_I); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) 'J', Apple2KeyboardSettingsMenu.SCANCODE_J); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) 'K', Apple2KeyboardSettingsMenu.SCANCODE_K); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) 'M', Apple2KeyboardSettingsMenu.SCANCODE_M); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }, + WADX_SPACE { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_wadx_space); + } + + @Override + public void apply(Apple2Activity activity) { + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) 'W', Apple2KeyboardSettingsMenu.SCANCODE_W); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) 'A', Apple2KeyboardSettingsMenu.SCANCODE_A); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) 'D', Apple2KeyboardSettingsMenu.SCANCODE_D); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) 'X', Apple2KeyboardSettingsMenu.SCANCODE_X); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_NONACTION, -1); + } + }, + CRAZY_SEAFOX_KEYS { + @Override + public String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_preset_crazy_seafox); + } + + @Override + public void apply(Apple2Activity activity) { + // Heh, the entire purpose of the keypad-variant touch joystick is to make this possible ;-) + Apple2Preferences.KEYPAD_NORTHWEST_KEY.saveChosenKey(activity, (char) 'Y', Apple2KeyboardSettingsMenu.SCANCODE_Y); + Apple2Preferences.KEYPAD_NORTH_KEY.saveChosenKey(activity, (char) 'U', Apple2KeyboardSettingsMenu.SCANCODE_U); + Apple2Preferences.KEYPAD_NORTHEAST_KEY.saveChosenKey(activity, (char) 'I', Apple2KeyboardSettingsMenu.SCANCODE_I); + Apple2Preferences.KEYPAD_WEST_KEY.saveChosenKey(activity, (char) 'H', Apple2KeyboardSettingsMenu.SCANCODE_H); + Apple2Preferences.KEYPAD_CENTER_KEY.saveChosenKey(activity, (char) 'J', Apple2KeyboardSettingsMenu.SCANCODE_J); + Apple2Preferences.KEYPAD_EAST_KEY.saveChosenKey(activity, (char) 'K', Apple2KeyboardSettingsMenu.SCANCODE_K); + Apple2Preferences.KEYPAD_SOUTHWEST_KEY.saveChosenKey(activity, (char) 'N', Apple2KeyboardSettingsMenu.SCANCODE_N); + Apple2Preferences.KEYPAD_SOUTH_KEY.saveChosenKey(activity, (char) 'M', Apple2KeyboardSettingsMenu.SCANCODE_M); + Apple2Preferences.KEYPAD_SOUTHEAST_KEY.saveChosenKey(activity, (char) ',', Apple2KeyboardSettingsMenu.SCANCODE_COMMA); + Apple2Preferences.KEYPAD_TAP_KEY.saveChosenKey(activity, (char) 'D', Apple2KeyboardSettingsMenu.SCANCODE_D); + Apple2Preferences.KEYPAD_SWIPEDOWN_KEY.saveChosenKey(activity, (char) 'F', Apple2KeyboardSettingsMenu.SCANCODE_F); + Apple2Preferences.KEYPAD_SWIPEUP_KEY.saveChosenKey(activity, (char) Apple2KeyboardSettingsMenu.ICONTEXT_VISUAL_SPACE, Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + } + }; + + public abstract String getTitle(Apple2Activity activity); + + public abstract void apply(Apple2Activity activity); + + public static final int size = KeypadPreset.values().length; + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (KeypadPreset preset : values()) { + titles[i++] = preset.getTitle(activity); + } + return titles; + } + } + + public final static int DECENT_AMOUNT_OF_CHOICES = 20; + public final static int AUDIO_LATENCY_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + public final static int ALPHA_SLIDER_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + public final static int JOYSTICK_DIVIDER_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + + public final static int TAPDELAY_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + public final static float TAPDELAY_SCALE = 0.5f; + + public final static int KEYREPEAT_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + + public final static String TAG = "Apple2Preferences"; + + public final static int JOYSTICK_BUTTON_THRESHOLD_NUM_CHOICES = DECENT_AMOUNT_OF_CHOICES; + public final static int JOYSTICK_BUTTON_THRESHOLD_STEP = 5; + + public final static float JOYSTICK_AXIS_SENSITIVITY_MIN = 0.25f; + public final static float JOYSTICK_AXIS_SENSITIVITY_DEFAULT = 1.f; + public final static float JOYSTICK_AXIS_SENSITIVITY_MAX = 4.f; + public final static float JOYSTICK_AXIS_SENSITIVITY_DEC_STEP = 0.05f; + public final static float JOYSTICK_AXIS_SENSITIVITY_INC_STEP = 0.25f; + public final static int JOYSTICK_AXIS_SENSITIVITY_DEC_NUMCHOICES = (int) ((JOYSTICK_AXIS_SENSITIVITY_DEFAULT - JOYSTICK_AXIS_SENSITIVITY_MIN) / JOYSTICK_AXIS_SENSITIVITY_DEC_STEP); // 15 + public final static int JOYSTICK_AXIS_SENSITIVITY_INC_NUMCHOICES = (int) ((JOYSTICK_AXIS_SENSITIVITY_MAX - JOYSTICK_AXIS_SENSITIVITY_DEFAULT) / JOYSTICK_AXIS_SENSITIVITY_INC_STEP); // 12 + public final static int JOYSTICK_AXIS_SENSITIVITY_NUM_CHOICES = JOYSTICK_AXIS_SENSITIVITY_DEC_NUMCHOICES + JOYSTICK_AXIS_SENSITIVITY_INC_NUMCHOICES; // 15 + 12 + + // set and apply + + public void saveBoolean(Apple2Activity activity, boolean value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putBoolean(toString(), value).apply(); + load(activity); + } + + public void saveInt(Apple2Activity activity, int value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(toString(), value).apply(); + load(activity); + } + + public void saveFloat(Apple2Activity activity, float value) { + throw new RuntimeException("DENIED! You're doing it wrong! =P"); + } + + public void saveString(Apple2Activity activity, String value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putString(toString(), value).apply(); + load(activity); + } + + public void saveHiresColor(Apple2Activity activity, HiresColor value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(toString(), value.ordinal()).apply(); + load(activity); + } + + public void saveVolume(Apple2Activity activity, Volume value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(toString(), value.ordinal()).apply(); + load(activity); + } + + public void saveTouchDevice(Apple2Activity activity, TouchDeviceVariant value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(toString(), value.ordinal()).apply(); + load(activity); + } + + public void saveTouchJoystickButtons(Apple2Activity activity, TouchJoystickButtons value) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(toString(), value.ordinal()).apply(); + load(activity); + } + + public void saveChosenKey(Apple2Activity activity, char ascii, int scancode) { + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(asciiString(), ascii).apply(); + activity.getPreferences(Context.MODE_PRIVATE).edit().putInt(scancodeString(), scancode).apply(); + load(activity); + } + + // accessors + + public boolean booleanValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getBoolean(toString(), false); + } + + public int intValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 0); + } + + public float floatValue(Apple2Activity activity) { + return (float) activity.getPreferences(Context.MODE_PRIVATE).getInt(toString(), 0); + } + + public String stringValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getString(toString(), null); + } + + public char asciiValue(Apple2Activity activity) { + return (char) activity.getPreferences(Context.MODE_PRIVATE).getInt(asciiString(), ' '); + } + + public int scancodeValue(Apple2Activity activity) { + return activity.getPreferences(Context.MODE_PRIVATE).getInt(scancodeString(), Apple2KeyboardSettingsMenu.SCANCODE_SPACE); + } + + public static void loadPreferences(Apple2Activity activity) { + for (Apple2Preferences pref : Apple2Preferences.values()) { + pref.load(activity); + } + } + + public static void resetPreferences(Apple2Activity activity) { + activity.getPreferences(Context.MODE_PRIVATE).edit().clear().commit(); + FIRST_TIME_CONFIGURED.saveBoolean(activity, true); + KeypadPreset.IJKM_SPACE.apply(activity); + loadPreferences(activity); + } + + public String asciiString() { + return toString() + "_ASCII"; + } + + public String scancodeString() { + return toString() + "_SCAN"; + } + + // ------------------------------------------------------------------------ + // internals ... + + protected abstract void load(Apple2Activity activity); + + protected static void warnError(Apple2Activity activity, int titleId, int mesgId) { + AlertDialog dialog = new AlertDialog.Builder(activity).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(titleId).setMessage(mesgId).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).create(); + activity.registerAndShowDialog(dialog); + } + + public static void loadAllKeypadKeys(Apple2Activity activity) { + int[] rosetteChars = new int[]{ + KEYPAD_NORTHWEST_KEY.asciiValue(activity), + KEYPAD_NORTH_KEY.asciiValue(activity), + KEYPAD_NORTHEAST_KEY.asciiValue(activity), + KEYPAD_WEST_KEY.asciiValue(activity), + KEYPAD_CENTER_KEY.asciiValue(activity), + KEYPAD_EAST_KEY.asciiValue(activity), + KEYPAD_SOUTHWEST_KEY.asciiValue(activity), + KEYPAD_SOUTH_KEY.asciiValue(activity), + KEYPAD_SOUTHEAST_KEY.asciiValue(activity), + }; + int[] rosetteScancodes = new int[]{ + KEYPAD_NORTHWEST_KEY.scancodeValue(activity), + KEYPAD_NORTH_KEY.scancodeValue(activity), + KEYPAD_NORTHEAST_KEY.scancodeValue(activity), + KEYPAD_WEST_KEY.scancodeValue(activity), + KEYPAD_CENTER_KEY.scancodeValue(activity), + KEYPAD_EAST_KEY.scancodeValue(activity), + KEYPAD_SOUTHWEST_KEY.scancodeValue(activity), + KEYPAD_SOUTH_KEY.scancodeValue(activity), + KEYPAD_SOUTHEAST_KEY.scancodeValue(activity), + }; + int[] buttonsChars = new int[]{ + KEYPAD_TAP_KEY.asciiValue(activity), + KEYPAD_SWIPEUP_KEY.asciiValue(activity), + KEYPAD_SWIPEDOWN_KEY.asciiValue(activity), + }; + int[] buttonsScancodes = new int[]{ + KEYPAD_TAP_KEY.scancodeValue(activity), + KEYPAD_SWIPEUP_KEY.scancodeValue(activity), + KEYPAD_SWIPEDOWN_KEY.scancodeValue(activity), + }; + nativeTouchJoystickSetKeypadTypes(rosetteChars, rosetteScancodes, buttonsChars, buttonsScancodes); + } + + public static void loadAllJoystickButtons(Apple2Activity activity) { + nativeSetTouchJoystickButtonTypes( + JOYSTICK_TAP_BUTTON.intValue(activity), + JOYSTICK_SWIPEUP_BUTTON.intValue(activity), + JOYSTICK_SWIPEDOWN_BUTTON.intValue(activity)); + } + + public static void insertDisk(Apple2Activity activity, String fullPath, boolean isDriveA, boolean isReadOnly) { + File file = new File(fullPath); + if (!file.exists()) { + fullPath = fullPath + ".gz"; + file = new File(fullPath); + } + if (file.exists()) { + activity.nativeChooseDisk(fullPath, isDriveA, isReadOnly); + } else { + Log.d(TAG, "Cannot insert: " + fullPath); + } + } + + // native hooks + + private static native void nativeSetColor(int color); + + private static native boolean nativeSetSpeakerEnabled(boolean enabled); + + private static native void nativeSetSpeakerVolume(int volume); + + private static native boolean nativeSetMockingboardEnabled(boolean enabled); + + private static native void nativeSetMockingboardVolume(int volume); + + private static native void nativeSetAudioLatency(float latencySecs); + + public static native void nativeSetCurrentTouchDevice(int device); + + private static native void nativeSetTouchJoystickButtonTypes(int down, int north, int south); + + private static native void nativeSetTouchJoystickTapDelay(float secs); + + private static native void nativeSetTouchJoystickAxisSensitivity(float multiplier); + + private static native void nativeSetTouchJoystickButtonSwitchThreshold(int delta); + + private static native void nativeSetTouchJoystickVisibility(boolean visibility); + + public static native void nativeSetTouchMenuEnabled(boolean enabled); + + private static native void nativeSetTouchMenuVisibility(float alpha); + + private static native void nativeSetTouchKeyboardVisibility(float inactiveAlpha, float activeAlpha); + + private static native void nativeSetTouchKeyboardLowercaseEnabled(boolean enabled); + + public static native int nativeGetCurrentTouchDevice(); + + public static native int nativeGetCPUSpeed(); + + public static native void nativeSetCPUSpeed(int percentSpeed); + + public static native void nativeTouchJoystickSetScreenDivision(float division); + + public static native void nativeTouchJoystickSetAxisOnLeft(boolean axisIsOnLeft); + + public static native void nativeTouchDeviceBeginCalibrationMode(); + + public static native void nativeTouchDeviceEndCalibrationMode(); + + private static native void nativeTouchJoystickSetKeypadTypes(int[] rosetteChars, int[] rosetteScancodes, int[] buttonsChars, int[] buttonsScancodes); + + private static native void nativeSetTouchDeviceKeyRepeatThreshold(float threshold); + + private static native void nativeLoadTouchKeyboardJSON(String path); + +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java new file mode 100644 index 00000000..f2dc14be --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java @@ -0,0 +1,386 @@ +/* + * 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.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import org.deadc0de.apple2ix.basic.BuildConfig; +import org.deadc0de.apple2ix.basic.R; + +public class Apple2SettingsMenu extends Apple2AbstractMenu { + + private final static String TAG = "Apple2SettingsMenu"; + + public Apple2SettingsMenu(Apple2Activity activity) { + super(activity); + } + + @Override + public final String[] allTitles() { + return SETTINGS.titles(mActivity); + } + + @Override + public final IMenuEnum[] allValues() { + return SETTINGS.values(); + } + + @Override + public final boolean areAllItemsEnabled() { + return false; + } + + @Override + public final boolean isEnabled(int position) { + if (position < 0 || position >= SETTINGS.size) { + throw new ArrayIndexOutOfBoundsException(); + } + return position != SETTINGS.TOUCH_MENU_VISIBILITY.ordinal(); + } + + enum SETTINGS implements Apple2AbstractMenu.IMenuEnum { + CURRENT_INPUT { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.input_current); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.input_current_summary); + } + + @Override + public final View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + _alertDialogHandleSelection(activity, R.string.input_current, new String[]{ + activity.getResources().getString(R.string.joystick), + activity.getResources().getString(R.string.keypad), + activity.getResources().getString(R.string.keyboard), + }, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.CURRENT_TOUCH_DEVICE.intValue(activity) - 1; + } + + @Override + public void saveInt(int value) { + Apple2Preferences.CURRENT_TOUCH_DEVICE.saveTouchDevice(activity, Apple2Preferences.TouchDeviceVariant.values()[value + 1]); + } + }); + } + }, + JOYSTICK_CONFIGURE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_configure); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.joystick_configure_summary); + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2JoystickSettingsMenu(activity).show(); + } + }, + KEYPAD_CONFIGURE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_configure); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keypad_configure_summary); + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2KeypadSettingsMenu(activity).show(); + } + }, + KEYBOARD_CONFIGURE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_configure); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.keyboard_configure_summary); + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2KeyboardSettingsMenu(activity).show(); + } + }, + AUDIO_CONFIGURE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.audio_configure); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.audio_configure_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + new Apple2AudioSettingsMenu(activity).show(); + } + }, + VIDEO_CONFIGURE { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.video_configure); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.video_configure_summary); + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + _addPopupIcon(activity, this, convertView); + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + _alertDialogHandleSelection(activity, R.string.video_configure, new String[]{ + settingsMenu.mActivity.getResources().getString(R.string.color_bw), + settingsMenu.mActivity.getResources().getString(R.string.color_color), + settingsMenu.mActivity.getResources().getString(R.string.color_interpolated), + }, new IPreferenceLoadSave() { + @Override + public int intValue() { + return Apple2Preferences.HIRES_COLOR.intValue(activity); + } + + @Override + public void saveInt(int value) { + Apple2Preferences.HIRES_COLOR.saveHiresColor(settingsMenu.mActivity, Apple2Preferences.HiresColor.values()[value]); + } + }); + } + }, + TOUCH_MENU_ENABLED { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.touch_menu_enable); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.touch_menu_enable_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.TOUCH_MENU_ENABLED.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.TOUCH_MENU_ENABLED.saveBoolean(activity, isChecked); + } + }); + return convertView; + } + }, + TOUCH_MENU_VISIBILITY { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.touch_menu_visibility); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.touch_menu_visibility_summary); + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + return _sliderView(activity, this, Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES, new IPreferenceSlider() { + @Override + public void saveInt(int progress) { + Apple2Preferences.TOUCH_MENU_VISIBILITY.saveInt(activity, progress); + } + + @Override + public int intValue() { + return Apple2Preferences.TOUCH_MENU_VISIBILITY.intValue(activity); + } + + @Override + public void showValue(int progress, final TextView seekBarValue) { + seekBarValue.setText("" + ((float) progress / Apple2Preferences.ALPHA_SLIDER_NUM_CHOICES)); + } + }); + } + }, + ABOUT { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.about_apple2ix); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.about_apple2ix_summary); + } + + @Override + public void handleSelection(Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + String url = "http://deadc0de.org/apple2ix/android/"; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + activity.startActivity(i); + } + }, + RESET_PREFERENCES { + @Override + public final String getTitle(Apple2Activity activity) { + return activity.getResources().getString(R.string.preferences_reset_title); + } + + @Override + public final String getSummary(Apple2Activity activity) { + return activity.getResources().getString(R.string.preferences_reset_summary); + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity).setIcon(R.drawable.ic_launcher).setCancelable(true).setTitle(R.string.preferences_reset_really).setMessage(R.string.preferences_reset_warning).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + Apple2Preferences.resetPreferences(activity); + } + }).setNegativeButton(R.string.no, null); + AlertDialog dialog = builder.create(); + activity.registerAndShowDialog(dialog); + } + }, + CRASH { + @Override + public final String getTitle(Apple2Activity activity) { + if (BuildConfig.DEBUG) { + // in debug mode we actually exercise the crash reporter ... + return activity.getResources().getString(R.string.crasher_title); + } else { + return activity.getResources().getString(R.string.crasher_check_title); + } + } + + @Override + public final String getSummary(Apple2Activity activity) { + if (BuildConfig.DEBUG) { + return activity.getResources().getString(R.string.crasher_summary); + } else { + return activity.getResources().getString(R.string.crasher_check_summary); + } + } + + @Override + public View getView(final Apple2Activity activity, View convertView) { + convertView = _basicView(activity, this, convertView); + if (!BuildConfig.DEBUG) { + CheckBox cb = _addCheckbox(activity, this, convertView, Apple2Preferences.CRASH_CHECK.booleanValue(activity)); + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Apple2Preferences.CRASH_CHECK.saveBoolean(activity, isChecked); + } + }); + } + return convertView; + } + + @Override + public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) { + + if (BuildConfig.DEBUG) { + _alertDialogHandleSelection(activity, R.string.crasher, Apple2CrashHandler.CrashType.titles(activity), new IPreferenceLoadSave() { + @Override + public int intValue() { + return -1; + } + + @Override + public void saveInt(int value) { + switch (value) { + case 0: { + final String[] str = new String[1]; + str[0] = null; + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + Log.d(TAG, "About to NPE : " + str[0].length()); + } + }); + } + break; + + default: + Apple2CrashHandler.getInstance().performCrash(value); + break; + } + } + }); + } + } + }; + + public static final int size = SETTINGS.values().length; + + @Override + public void handleSelection(Apple2Activity activity, Apple2AbstractMenu settingsMenu, boolean isChecked) { + /* ... */ + } + + @Override + public View getView(Apple2Activity activity, View convertView) { + return _basicView(activity, this, convertView); + } + + public static String[] titles(Apple2Activity activity) { + String[] titles = new String[size]; + int i = 0; + for (SETTINGS setting : values()) { + titles[i++] = setting.getTitle(activity); + } + return titles; + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SplashScreen.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SplashScreen.java new file mode 100644 index 00000000..03f82bf4 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SplashScreen.java @@ -0,0 +1,113 @@ +/* + * 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.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; + +import org.deadc0de.apple2ix.basic.R; + +public class Apple2SplashScreen implements Apple2MenuView { + + private final static String TAG = "Apple2SplashScreen"; + + private Apple2Activity mActivity = null; + private boolean mDismissable = true; + private View mSettingsView = null; + + public Apple2SplashScreen(Apple2Activity activity, boolean dismissable) { + mActivity = activity; + setup(); + setDismissable(dismissable); + } + + private void setup() { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mSettingsView = inflater.inflate(R.layout.activity_splash_screen, null, false); + + Button startButton = (Button) mSettingsView.findViewById(R.id.startButton); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Apple2SplashScreen.this.dismiss(); + } + }); + + Button prefsButton = (Button) mSettingsView.findViewById(R.id.prefsButton); + prefsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Apple2SettingsMenu settingsMenu = mActivity.getSettingsMenu(); + settingsMenu.show(); + } + }); + + Button disksButton = (Button) mSettingsView.findViewById(R.id.disksButton); + disksButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Apple2DisksMenu disksMenu = mActivity.getDisksMenu(); + disksMenu.show(); + } + }); + } + + public void setDismissable(boolean dismissable) { + mDismissable = dismissable; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + Button startButton = (Button) mSettingsView.findViewById(R.id.startButton); + startButton.setEnabled(mDismissable); + Button prefsButton = (Button) mSettingsView.findViewById(R.id.prefsButton); + prefsButton.setEnabled(mDismissable); + Button disksButton = (Button) mSettingsView.findViewById(R.id.disksButton); + disksButton.setEnabled(mDismissable); + } + }); + } + + public final boolean isCalibrating() { + return false; + } + + public void onKeyTapCalibrationEvent(char ascii, int scancode) { + /* ... */ + } + + public void show() { + if (isShowing()) { + return; + } + mActivity.pushApple2View(this); + } + + public void dismiss() { + if (mDismissable) { + mActivity.popApple2View(this); + } + } + + public void dismissAll() { + dismiss(); + } + + public boolean isShowing() { + return mSettingsView.getParent() != null; + } + + public View getView() { + return mSettingsView; + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2View.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2View.java new file mode 100644 index 00000000..f198217d --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2View.java @@ -0,0 +1,362 @@ +/* + * 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 + * + */ + +/* + * Sourced from AOSP "GL2JNI" sample code. + */ + +package org.deadc0de.apple2ix; + +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.util.Log; +import android.view.ViewTreeObserver; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.opengles.GL10; + +/** + * A simple GLSurfaceView sub-class that demonstrate how to perform + * OpenGL ES 2.0 rendering into a GL Surface. Note the following important + * details: + * + * - The class must use a custom context factory to enable 2.0 rendering. + * See ContextFactory class definition below. + * + * - The class must use a custom EGLConfigChooser to be able to select + * an EGLConfig that supports 2.0. This is done by providing a config + * specification to eglChooseConfig() that has the attribute + * EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag + * set. See ConfigChooser class definition below. + * + * - The class must select the surface's format, then choose an EGLConfig + * that matches it exactly (with regards to red/green/blue/alpha channels + * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. + */ +class Apple2View extends GLSurfaceView { + private final static String TAG = "Apple2View"; + private final static boolean DEBUG = false; + + private Apple2Activity mActivity = null; + private Runnable mGraphicsInitializedRunnable = null; + + private static native void nativeGraphicsInitialized(int width, int height); + + private static native void nativeGraphicsChanged(int width, int height); + + private static native void nativeRender(); + + public Apple2View(Apple2Activity activity, Runnable graphicsInitializedRunnable) { + super(activity.getApplication()); + mActivity = activity; + mGraphicsInitializedRunnable = graphicsInitializedRunnable; + + /* By default, GLSurfaceView() creates a RGB_565 opaque surface. + * If we want a translucent one, we should change the surface's + * format here, using PixelFormat.TRANSLUCENT for GL Surfaces + * is interpreted as any 32-bit surface with alpha by SurfaceFlinger. + */ + this.getHolder().setFormat(PixelFormat.TRANSLUCENT); + + /* Setup the context factory for 2.0 rendering. + * See ContextFactory class definition below + */ + setEGLContextFactory(new ContextFactory()); + + /* We need to choose an EGLConfig that matches the format of + * our surface exactly. This is going to be done in our + * custom config chooser. See ConfigChooser class definition + * below. + */ + setEGLConfigChooser(new ConfigChooser(8, 8, 8, 8, /*depth:*/0, /*stencil:*/0)); + + /* Set the renderer responsible for frame rendering */ + setRenderer(new Renderer()); + + // Another Android Annoyance ... + // Even though we no longer use the system soft keyboard (which would definitely trigger width/height changes to our OpenGL canvas), + // we still need to listen to dimension changes, because it seems on some janky devices you have an incorrect width/height set when + // the initial OpenGL onSurfaceChanged() callback occurs. For now, include this defensive coding... + getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + public void onGlobalLayout() { + Rect rect = new Rect(); + Apple2View.this.getWindowVisibleDisplayFrame(rect); + int h = rect.height(); + int w = rect.width(); + if (w < h) { + // assure landscape dimensions + final int w_ = w; + w = h; + h = w_; + } + nativeGraphicsChanged(w, h); + } + }); + + } + + private static class ContextFactory implements GLSurfaceView.EGLContextFactory { + private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + Log.w(TAG, "creating OpenGL ES 2.0 context"); + checkEglError("Before eglCreateContext", egl); + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + checkEglError("After eglCreateContext", egl); + return context; + } + + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + } + + private static void checkEglError(String prompt, EGL10 egl) { + int error; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error)); + } + } + + private static class ConfigChooser implements GLSurfaceView.EGLConfigChooser { + + public ConfigChooser(int r, int g, int b, int a, int depth, int stencil) { + mRedSize = r; + mGreenSize = g; + mBlueSize = b; + mAlphaSize = a; + mDepthSize = depth; + mStencilSize = stencil; + } + + /* This EGL config specification is used to specify 2.0 rendering. + * We use a minimum size of 4 bits for red/green/blue, but will + * perform actual matching in chooseConfig() below. + */ + private static int EGL_OPENGL_ES2_BIT = 4; + private static int[] s_configAttribs2 = { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + EGL10.EGL_ALPHA_SIZE, 4, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + + // Get the number of minimally matching EGL configurations + int[] num_config = new int[1]; + egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config); + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + // Allocate then read the array of minimally matching EGL configs + EGLConfig[] configs = new EGLConfig[numConfigs]; + egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config); + + if (DEBUG) { + printConfigs(egl, display, configs); + } + + // Now return the "best" one + EGLConfig best = chooseConfig(egl, display, configs); + if (best == null) { + Log.e(TAG, "OOPS! Did not pick an EGLConfig. What device are you using?! Android will now crash this app..."); + } else { + Log.w(TAG, "Using EGL CONFIG : "); + printConfig(egl, display, best); + } + return best; + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + + // We need at least mDepthSize and mStencilSize bits + if (d < mDepthSize || s < mStencilSize) { + continue; + } + + // We want an *exact* match for red/green/blue/alpha + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + + if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize) { + return config; + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private void printConfigs(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + int numConfigs = configs.length; + Log.w(TAG, String.format("%d configurations", numConfigs)); + for (int i = 0; i < numConfigs; i++) { + Log.w(TAG, String.format("Configuration %d:\n", i)); + printConfig(egl, display, configs[i]); + } + } + + private void printConfig(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attributes = { + EGL10.EGL_BUFFER_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_RED_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_HEIGHT, + EGL10.EGL_MAX_PBUFFER_PIXELS, + EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, + EGL10.EGL_NATIVE_VISUAL_ID, + EGL10.EGL_NATIVE_VISUAL_TYPE, + 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, + EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, + EGL10.EGL_TRANSPARENT_GREEN_VALUE, + EGL10.EGL_TRANSPARENT_BLUE_VALUE, + 0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB, + 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL, + 0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL, + EGL10.EGL_LUMINANCE_SIZE, + EGL10.EGL_ALPHA_MASK_SIZE, + EGL10.EGL_COLOR_BUFFER_TYPE, + EGL10.EGL_RENDERABLE_TYPE, + 0x3042 // EGL10.EGL_CONFORMANT + }; + String[] names = { + "EGL_BUFFER_SIZE", + "EGL_ALPHA_SIZE", + "EGL_BLUE_SIZE", + "EGL_GREEN_SIZE", + "EGL_RED_SIZE", + "EGL_DEPTH_SIZE", + "EGL_STENCIL_SIZE", + "EGL_CONFIG_CAVEAT", + "EGL_CONFIG_ID", + "EGL_LEVEL", + "EGL_MAX_PBUFFER_HEIGHT", + "EGL_MAX_PBUFFER_PIXELS", + "EGL_MAX_PBUFFER_WIDTH", + "EGL_NATIVE_RENDERABLE", + "EGL_NATIVE_VISUAL_ID", + "EGL_NATIVE_VISUAL_TYPE", + "EGL_PRESERVED_RESOURCES", + "EGL_SAMPLES", + "EGL_SAMPLE_BUFFERS", + "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", + "EGL_TRANSPARENT_RED_VALUE", + "EGL_TRANSPARENT_GREEN_VALUE", + "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", + "EGL_BIND_TO_TEXTURE_RGBA", + "EGL_MIN_SWAP_INTERVAL", + "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", + "EGL_ALPHA_MASK_SIZE", + "EGL_COLOR_BUFFER_TYPE", + "EGL_RENDERABLE_TYPE", + "EGL_CONFORMANT" + }; + int[] value = new int[1]; + for (int i = 0; i < attributes.length; i++) { + int attribute = attributes[i]; + String name = names[i]; + if ( egl.eglGetConfigAttrib(display, config, attribute, value)) { + Log.w(TAG, String.format(" %s: %d\n", name, value[0])); + } else { + // Log.w(TAG, String.format(" %s: failed\n", name)); + while (egl.eglGetError() != EGL10.EGL_SUCCESS); + } + } + } + + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + private int[] mValue = new int[1]; + } + + private class Renderer implements GLSurfaceView.Renderer { + + @Override + public void onDrawFrame(GL10 gl) { + nativeRender(); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + Apple2Preferences.GL_VENDOR.saveString(mActivity, GLES20.glGetString(GLES20.GL_VENDOR)); + Apple2Preferences.GL_RENDERER.saveString(mActivity, GLES20.glGetString(GLES20.GL_RENDERER)); + Apple2Preferences.GL_VERSION.saveString(mActivity, GLES20.glGetString(GLES20.GL_VERSION)); + + Log.v(TAG, "graphicsInitialized(" + width + ", " + height + ")"); + + if (width < height) { + // assure landscape dimensions + final int w_ = width; + width = height; + height = w_; + } + + nativeGraphicsInitialized(width, height); + + if (Apple2View.this.mGraphicsInitializedRunnable != null) { + Apple2View.this.mGraphicsInitializedRunnable.run(); + Apple2View.this.mGraphicsInitializedRunnable = null; + } + + Apple2View.this.mActivity.maybeResumeCPU(); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + // Do nothing. + } + } +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/DevicePropertyCalculator.java b/Android/app/src/main/java/org/deadc0de/apple2ix/DevicePropertyCalculator.java new file mode 100644 index 00000000..de4e742e --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/DevicePropertyCalculator.java @@ -0,0 +1,154 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2013-2014 Igor Zinken - http://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.deadc0de.apple2ix; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.os.Build; + +/** + * Created by IntelliJ IDEA. + * User: igorzinken + * Date: 03-06-13 + * Time: 13:16 + * To change this template use File | Settings | File Templates. + */ +public final class DevicePropertyCalculator +{ + public final static int defaultSampleRate = 22050; + + public static boolean detectLowLatency( Context aContext ) + { + // check for low latency audio + PackageManager pm = aContext.getPackageManager(); + + return pm.hasSystemFeature( PackageManager.FEATURE_AUDIO_LOW_LATENCY ); + } + + /* + Beginning with API level 17 (Android platform version 4.2), an application can query for the native or optimal + output sample rate and buffer size for the device's primary output stream. When combined with the feature test + just mentioned, an app can now configure itself appropriately for lower latency output on devices that claim support. + + The recommended sequence is: + + Check for API level 9 or higher, to confirm use of OpenSL ES. + Check for feature "android.hardware.audio.low_latency" using code such as this: + import android.content.pm.PackageManager; + ... + PackageManager pm = getContext().getPackageManager(); + boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); + Check for API level 17 or higher, to confirm use of android.media.AudioManager.getProperty(). + Get the native or optimal output sample rate and buffer size for this device's primary output stream, using code such as this: + import android.media.AudioManager; + ... + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)); + String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)); + Note that sampleRate and framesPerBuffer are Strings. First check for null and then convert to int using Integer.parseInt(). + Now use OpenSL ES to create an AudioPlayer with PCM buffer queue data locator. + The number of lower latency audio players is limited. If your application requires more than a few audio sources, consider mixing your audio at application level. Be sure to destroy your audio players when your activity is paused, as they are a global resource shared with other apps. + */ + public static int getRecommendedSampleRate( Context aContext ) + { + String SR_CHECK = null; + + // API level 17 available ? + if ( android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) + { + AudioManager am = ( AudioManager ) aContext.getSystemService( Context.AUDIO_SERVICE ); + + // Use the sample rate provided by AudioManager.getProperty(PROPERTY_OUTPUT_SAMPLE_RATE). + // Otherwise your buffers take a detour through the system resampler. + + SR_CHECK = am.getProperty( AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE ); + } + + return ( SR_CHECK != null ) ? Integer.parseInt( SR_CHECK ) : defaultSampleRate; + } + + /** + * retrieve the recommended buffer size for the device running the application + * you can increase / decrease the buffer size for lower latency or higher stability, but + * note you must use multiples of this recommendation !! Otherwise the buffer callback will + * occasionally get two calls per timeslice which can cause glitching unless CPU usage is + * really light + * + * some measurements ( combined w/ recommended sample rate above ): + * + * Samsung Galaxy S Plus on 2.3 Gingerbread : 4800 samples per buffer ( 44.1 kHz ) ADEQUATE ( 75 samples == SOMEWHAT STABLE + * ( unless under heavy stress ), 150 == PERFECT ) + * Samsung Galaxy Nexus 4.2.1 Jelly Bean : 144 samples per buffer ( 44.1 kHz ) INADEQUATE ( 288 samples == OK ) + * Samsung Galaxy S3 4.1.2 Jelly Bean : 2048 samples per buffer ( ---- kHz ) ADEQUATE ( 512 samples == OK ) + * Asus Nexus 7 on 4.2.2 Jelly Bean : 512 samples per buffer ( 44.1 kHz ) ACCURATE! ( perhaps 384 ?? ) + * HTC One V 4.0.3 Ice Cream Sandwich : 4800 samples per buffer ( 44.1 kHz ) ADEQUATE ( 300 samples == OK ) + * + * @param aContext {Context} + * @return {int} + */ + public static int getRecommendedBufferSize( Context aContext, boolean isStereo) + { + // prepare Native Audio engine + String BS_CHECK = null; + + // API level 17 available ? + if ( android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) + { + AudioManager am = ( AudioManager ) aContext.getSystemService( Context.AUDIO_SERVICE ); + + BS_CHECK = am.getProperty( AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER ); + } + return ( BS_CHECK != null ) ? Integer.parseInt( BS_CHECK ) : AudioTrack.getMinBufferSize( getRecommendedSampleRate( aContext ), + isStereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT ); + } + + public static int getMinimumBufferSize( Context aContext, boolean isStereo ) + { + // minimum buffer size we allow is the recommendation divided by four + int min = DevicePropertyCalculator.getRecommendedBufferSize( aContext, isStereo ) / 4; + + // however, we'd like to supply tha rea of 64 samples per buffer as an option + while ( min > 128 ) min /= 2; // 128 as we do a greater than check + + // but the minimum we allow is 32 samples per buffer + if ( min < 32 ) + min *= 2; + + return min; + } + + public static int getMaximumBufferSize( Context aContext, boolean isStereo ) + { + int max = DevicePropertyCalculator.getRecommendedBufferSize( aContext, isStereo ) * 8; + + // nothing TOO extravagant... 8192 should be enough... + while ( max > 10000 ) + max /= 2; + + return max; + } +} diff --git a/Android/app/src/main/jniLibs b/Android/app/src/main/jniLibs new file mode 120000 index 00000000..d63817ad --- /dev/null +++ b/Android/app/src/main/jniLibs @@ -0,0 +1 @@ +../../../libs \ No newline at end of file diff --git a/Android/app/src/main/res/drawable-hdpi/apple_iie.png b/Android/app/src/main/res/drawable-hdpi/apple_iie.png new file mode 100644 index 00000000..75ef1da0 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_launcher.png b/Android/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..eed28469 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/drawable-ldpi/apple_iie.png b/Android/app/src/main/res/drawable-ldpi/apple_iie.png new file mode 100644 index 00000000..75ef1da0 Binary files /dev/null and b/Android/app/src/main/res/drawable-ldpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-ldpi/ic_launcher.png b/Android/app/src/main/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..67874f3a Binary files /dev/null and b/Android/app/src/main/res/drawable-ldpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/apple_iie.png b/Android/app/src/main/res/drawable-mdpi/apple_iie.png new file mode 100644 index 00000000..75ef1da0 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_launcher.png b/Android/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..759a2313 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/apple_iie.png b/Android/app/src/main/res/drawable-xhdpi/apple_iie.png new file mode 100644 index 00000000..9ece1674 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_launcher.png b/Android/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bd0fb6fe Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/apple_iie.png b/Android/app/src/main/res/drawable-xxhdpi/apple_iie.png new file mode 100644 index 00000000..9ece1674 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/Android/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..c67812e1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/apple_iie.png b/Android/app/src/main/res/drawable-xxxhdpi/apple_iie.png new file mode 100644 index 00000000..42736224 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/apple_iie.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..f1880bf7 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/Android/app/src/main/res/layout/a2disk.xml b/Android/app/src/main/res/layout/a2disk.xml new file mode 100644 index 00000000..d3119c1a --- /dev/null +++ b/Android/app/src/main/res/layout/a2disk.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/layout/a2disk_confirmation.xml b/Android/app/src/main/res/layout/a2disk_confirmation.xml new file mode 100644 index 00000000..78c43458 --- /dev/null +++ b/Android/app/src/main/res/layout/a2disk_confirmation.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/res/layout/a2preference.xml b/Android/app/src/main/res/layout/a2preference.xml new file mode 100644 index 00000000..6f4f0a80 --- /dev/null +++ b/Android/app/src/main/res/layout/a2preference.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/layout/a2preference_slider.xml b/Android/app/src/main/res/layout/a2preference_slider.xml new file mode 100644 index 00000000..225d3712 --- /dev/null +++ b/Android/app/src/main/res/layout/a2preference_slider.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/res/layout/activity_calibrate_joystick.xml b/Android/app/src/main/res/layout/activity_calibrate_joystick.xml new file mode 100644 index 00000000..abbb40c3 --- /dev/null +++ b/Android/app/src/main/res/layout/activity_calibrate_joystick.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/Android/app/src/main/res/layout/activity_chooser_keypad.xml b/Android/app/src/main/res/layout/activity_chooser_keypad.xml new file mode 100644 index 00000000..1c990c80 --- /dev/null +++ b/Android/app/src/main/res/layout/activity_chooser_keypad.xml @@ -0,0 +1,37 @@ + + + + + +