/* * 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); } }