package com.froop.app.kegs; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.AsyncTask; import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.View; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View.OnTouchListener; import android.widget.Button; import android.widget.ToggleButton; import android.app.ActionBar; import android.app.Activity; import android.app.DialogFragment; import android.view.Menu; import android.view.MenuItem; import java.util.ArrayDeque; public class KegsMain extends Activity implements KegsKeyboard.StickyReset, AssetImages.AssetsReady, DiskLoader.ImageReady { private static final String FRAGMENT_ROM = "rom"; private static final String FRAGMENT_DOWNLOAD = "download"; private static final String FRAGMENT_ERROR = "error"; private static final String FRAGMENT_SPEED = "speed"; private static final String FRAGMENT_DISKIMAGE = "diskimage"; private static final String FRAGMENT_SWAPDISK = "swapdisk"; private static final String FRAGMENT_ZIPDISK = "zipdisk"; private static final String FRAGMENT_LOADING = "loading"; private ConfigFile mConfigFile; private KegsThread mKegsThread; protected KegsViewGL mKegsView; private KegsKeyboard mKegsKeyboard; private TouchMouse mTouchMouse; private TouchJoystick mJoystick; private boolean mModeMouse = true; private boolean mOverrideActionBar = false; private long mScreenSizeTime = 0; private boolean mPaused = false; private final ArrayDeque mResumeQueue = new ArrayDeque(); private final Runnable mErrorFinish = new Runnable() { public void run() { finish(); } }; private DiskLoader mDiskLoader = null; private final DiskImage.DriveNumber mDriveNumber = new DiskImage.DriveNumber(); private void withUIActive(final Runnable runnable) { if(!mPaused) { runnable.run(); } else { mResumeQueue.add(runnable); } } public void onAssetsReady(boolean result) { // TODO: deal with error conditions from assets as a warning. } private final View.OnClickListener mButtonClick = new View.OnClickListener() { public void onClick(View v) { final int click_id = v.getId(); int key_id = -1; boolean sticky = false; if (click_id == R.id.key_escape) { key_id = KegsKeyboard.KEY_ESCAPE; } else if (click_id == R.id.key_return) { key_id = KegsKeyboard.KEY_RETURN; } else if (click_id == R.id.key_tab) { key_id = KegsKeyboard.KEY_TAB; } else if (click_id == R.id.key_control) { key_id = KegsKeyboard.KEY_CONTROL; sticky = true; } else if (click_id == R.id.key_open_apple) { key_id = KegsKeyboard.KEY_OPEN_APPLE; sticky = true; } else if (click_id == R.id.key_closed_apple) { key_id = KegsKeyboard.KEY_CLOSED_APPLE; sticky = true; } else if (click_id == R.id.key_left) { key_id = KegsKeyboard.KEY_LEFT; } else if (click_id == R.id.key_right) { key_id = KegsKeyboard.KEY_RIGHT; } else if (click_id == R.id.key_up) { key_id = KegsKeyboard.KEY_UP; } else if (click_id == R.id.key_down) { key_id = KegsKeyboard.KEY_DOWN; } else { Log.e("kegs", "UNKNOWN BUTTON " + click_id + " CLICKED"); } if (key_id != -1) { if (sticky) { mKegsKeyboard.keyDownSticky(key_id, !((ToggleButton)v).isChecked()); } else { mKegsKeyboard.keyDownUp(key_id); } } } }; private boolean areControlsVisible() { return findViewById(R.id.b1).getVisibility() == View.VISIBLE; } public void onStickyReset() { ((ToggleButton)findViewById(R.id.key_control)).setChecked(false); ((ToggleButton)findViewById(R.id.key_open_apple)).setChecked(false); ((ToggleButton)findViewById(R.id.key_closed_apple)).setChecked(false); } protected void loadDiskImage(final DiskImage image) { if (image.action == DiskImage.BOOT) { getThread().doPowerOff(); } loadDiskImageWhenReady(image); } private void loadDiskImageWhenReady(final DiskImage image) { boolean nativeReady; if (image.action == DiskImage.BOOT) { nativeReady = getThread().nowWaitingForPowerOn(); } else { nativeReady = true; } if (!nativeReady) { mKegsView.postDelayed(new Runnable() { public void run() { loadDiskImageWhenReady(image); } }, 100); return; } withUIActive(new Runnable() { public void run() { if (image.action == DiskImage.BOOT) { mConfigFile.setConfig(image); getThread().allowPowerOn(); } else if (image.action == DiskImage.SWAP) { if (image.isHardDrive()) { image.chooseDriveNumber(mDriveNumber); } getThread().getEventQueue().add(image.getDiskImageEvent()); } } }); } public void onImageCancelled(final boolean result, final DiskImage image) { if (getThread().nowWaitingForPowerOn()) { // Emulator never powered on and the user aborted. finish(); } } public void onImageReady(final boolean result, final DiskImage image) { mDiskLoader = null; withUIActive(new Runnable() { public void run() { dismissFragment(FRAGMENT_LOADING); if (!result) { final Runnable cancel; if (getThread().nowWaitingForPowerOn()) { // Emulator never powered on and we failed, exit when user clicks OK. cancel = mErrorFinish; } else { cancel = null; } new ErrorDialogFragment(R.string.image_error, cancel).show( getFragmentManager(), FRAGMENT_ERROR); } else if (image.action != DiskImage.ASK) { loadDiskImage(image); } else { new SwapDiskFragment(image).show(getFragmentManager(), FRAGMENT_SWAPDISK); } } }); } public void prepareDiskImage(DiskImage image) { dismissFragment(FRAGMENT_ROM); // Note: should probably bail if visible. dismissFragment(FRAGMENT_SPEED); dismissFragment(FRAGMENT_DISKIMAGE); dismissFragment(FRAGMENT_SWAPDISK); dismissFragment(FRAGMENT_ZIPDISK); if (image.filename.endsWith(".zip") || image.filename.endsWith(".ZIP")) { final ZipDiskFragment zip = new ZipDiskFragment(image); if (zip.needsDialog()) { zip.show(getFragmentManager(), FRAGMENT_ZIPDISK); return; } else { image = zip.getFirstImage(); } } runDiskLoader(image); } // So that other fragments can transition into the DiskLoader. public void runDiskLoader(final DiskImage image) { final Runnable cancel = new Runnable() { public void run() { if (mDiskLoader != null) { mDiskLoader.cancel(true); mDiskLoader = null; } } }; final DiskLoader.ImageReady notify = this; withUIActive(new Runnable() { public void run() { // In case there was already another DiskLoader. cancel.run(); dismissFragment(FRAGMENT_LOADING); mDiskLoader = new DiskLoader(notify, mConfigFile, image); if (mDiskLoader.willBeSlow()) { new SpecialProgressDialog(cancel).show(getFragmentManager(), FRAGMENT_LOADING); } if (android.os.Build.VERSION.SDK_INT >= 11) { mDiskLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { mDiskLoader.execute(); } } }); } private DiskImage getBootDiskImage(Intent intent) { if (intent != null && intent.getAction().equals(Intent.ACTION_VIEW)) { final Uri uri = intent.getData(); if (uri != null && uri.getScheme().equals("file")) { final String path = uri.getPath(); if (path != null) { return DiskImage.fromPath(path); } } } return null; } // The first boot upon application launch. private void boot() { final DiskImage image = getBootDiskImage(getIntent()); if (image != null) { prepareDiskImage(image); } else { mConfigFile.defaultConfig(); getThread().allowPowerOn(); mKegsView.postDelayed(new Runnable() { public void run() { withUIActive(new Runnable() { public void run() { if (findFragment(FRAGMENT_DISKIMAGE) == null) { new DiskImageFragment(mConfigFile, DiskImage.BOOT).show(getFragmentManager(), FRAGMENT_DISKIMAGE); } } }); } }, 1400); } } protected void getRomFile(String romfile) { final DialogFragment download = new DownloadDialogFragment(); download.show(getFragmentManager(), FRAGMENT_DOWNLOAD); if (android.os.Build.VERSION.SDK_INT >= 11) { new DownloadRom().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, romfile); } else { new DownloadRom().execute(romfile); } } class DownloadRom extends AsyncTask { private String mRomfile; protected Boolean doInBackground(String ... raw_romfile) { mRomfile = raw_romfile[0]; return new DownloadHelper().save( "https://jsan.co/KEGS/" + mRomfile, mConfigFile.getConfigPath() + "/" + mRomfile); } protected void onPostExecute(final Boolean success) { withUIActive(new Runnable() { public void run() { final DialogFragment frag = (DialogFragment)getFragmentManager().findFragmentByTag(FRAGMENT_DOWNLOAD); if (frag != null) { frag.dismiss(); } if (!success) { if (!isCancelled()) { new ErrorDialogFragment(R.string.rom_error, mErrorFinish).show( getFragmentManager(), FRAGMENT_ERROR); } } else { boot(); } } }); } } public static class DownloadDialogFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ProgressDialog dialog = new ProgressDialog(getActivity()); // TODO: should probably use an XML layout for this. dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setMessage(getResources().getText(R.string.rom_check)); if (android.os.Build.VERSION.SDK_INT <= 10 || android.os.Build.VERSION.SDK_INT >= 14) { // This seems to crash on some vendors Honeycomb. dialog.setProgressNumberFormat(null); } if (android.os.Build.VERSION.SDK_INT >= 14) { dialog.setProgressPercentFormat(null); } dialog.setIndeterminate(true); dialog.setCancelable(false); dialog.setCanceledOnTouchOutside(false); return dialog; } @Override public void onCancel(DialogInterface dialog) { super.onCancel(dialog); ((KegsMain)getActivity()).finish(); } } public KegsThread getThread() { return mKegsThread; } // Designate the upper right corner as a special zone that // toggles the action bar. private Rect getSpecialActionBarRect(BitmapSize bitmapSize) { final int left = (int)(bitmapSize.getScreenWidth() * 0.80); final int top = 0; final int right = bitmapSize.getScreenWidth(); final int bottom = (int)(bitmapSize.getScreenHeight() * 0.10); // Only have a special zone if the action bar is not supposed to be visible. if (!bitmapSize.showActionBar() && left != 0 && bottom != 0) { return new Rect(left, top, right, bottom); } else { return null; } } private void updateActionBar(boolean showActionBar) { showActionBar = mOverrideActionBar || showActionBar; final ActionBar actionBar = getActionBar(); if (actionBar == null) { return; } if (showActionBar) { actionBar.show(); } else { actionBar.hide(); } } private void updateScreenSize(int width, int height, long sent) { if (mScreenSizeTime != sent) { // There's a newer event coming soon, wait for it. // // This is a bug workaround. We need to wait for the size to settle // before acting on it. If we update *immediately* it seems to // confuse Android. If we update multiple times, Android creates // completely screwed up animations during the rotation. // // So, we mark mScreenSize with a message time and tell it to update // 250ms later. We only want to act on the last update that is sent, // but we can't easily cancel the fact that the message is being sent, // so bail out here if the time doesn't match the last message. return; } final BitmapSize bitmapSize = new BitmapSize(width, height, getResources().getDisplayMetrics()); updateActionBar(bitmapSize.showActionBar()); mKegsView.updateScreenSize(bitmapSize); // Update scale of mouse movements. mTouchMouse.updateScale(bitmapSize.getScaleX(), bitmapSize.getScaleY()); // Update special click zone that toggles the ActionBar. final TouchSpecialZone zone = new TouchSpecialZone(getSpecialActionBarRect(bitmapSize)) { public void activate() { mOverrideActionBar = !mOverrideActionBar; updateActionBar(mOverrideActionBar); } }; mTouchMouse.setSpecialZone(zone); mJoystick.setSpecialZone(zone); // Force another redraw of the bitmap into the canvas. Bug workaround. getThread().updateScreen(); } public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Fire off a guess at a new size during this request, // it makes the animation transition look better. final BitmapSize quickSize = BitmapSize.quick(this); updateActionBar(quickSize.showActionBar()); mKegsView.updateScreenSize(quickSize); getThread().updateScreen(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return mKegsKeyboard.keyEvent(event) || super.dispatchKeyEvent(event); } // @Override // public boolean dispatchGenericMotionEvent(MotionEvent event) { // // Joystick! if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 && event.getAction() == MotionEvent.ACTION_MOVE) {} // // See also GameControllerInput.java from ApiDemos // return super.dispatchGenericMotionEvent(event); // } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); // BUG: no overflow menu on devices with menu button // BUG: when action bar is hidden, menu bar only shows overflow items getMenuInflater().inflate(R.menu.actions, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // remember to call supportInvalidateOptionsMenu() for this to be run MenuItem item; item = menu.findItem(R.id.action_joystick); if (item != null) { item.setIcon(mModeMouse ? R.drawable.ic_bt_misc_hid : R.drawable.ic_bt_pointing_hid); item.setTitle(mModeMouse ? R.string.input_joystick : R.string.input_mouse); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Action bar was clicked. final int item_id = item.getItemId(); if (item_id == R.id.action_keyboard) { InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.toggleSoftInput(0, 0); } return true; } else if (item_id == R.id.action_speed) { new SpeedFragment().show(getFragmentManager(), FRAGMENT_SPEED); return true; } else if (item_id == R.id.action_joystick) { mModeMouse = !mModeMouse; invalidateOptionsMenu(); // update icon return true; } else if (item_id == R.id.action_diskimage) { new DiskImageFragment(mConfigFile, DiskImage.ASK).show(getFragmentManager(), FRAGMENT_DISKIMAGE); return true; } else if (item_id == R.id.action_more_keys) { final int vis = areControlsVisible() ? View.GONE : View.VISIBLE; findViewById(R.id.b1).setVisibility(vis); findViewById(R.id.b2).setVisibility(vis); findViewById(R.id.b3).setVisibility(vis); return true; } else if (item_id == R.id.action_power_off) { getThread().doPowerOff(); finish(); } return false; } private DialogFragment findFragment(final String tag) { return (DialogFragment)getFragmentManager().findFragmentByTag(tag); } private boolean dismissFragment(final String tag) { final DialogFragment frag = findFragment(tag); if (frag == null) { return false; } else { frag.dismiss(); return true; } } private void handleNewIntent(Intent intent) { final DiskImage image = getBootDiskImage(intent); if (image == null) { return; // Nothing to do. } image.action = DiskImage.ASK; prepareDiskImage(image); } @Override protected void onNewIntent(final Intent intent) { // "An activity will always be paused before receiving a new intent." withUIActive(new Runnable() { public void run() { handleNewIntent(intent); } }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); updateActionBar(BitmapSize.quick(this).showActionBar()); mKegsView = findViewById(R.id.kegsview); mConfigFile = new ConfigFile(this); mKegsThread = new KegsThread(mConfigFile.getConfigFile(), mKegsView.getBitmap()); mKegsThread.registerUpdateScreenInterface(mKegsView); mTouchMouse = new TouchMouse(this, getThread().getEventQueue()); mJoystick = new TouchJoystick(getThread().getEventQueue()); final SpecialRelativeLayout mainView = findViewById(R.id.mainview); if (android.os.Build.VERSION.SDK_INT >= 26) { mainView.setDefaultFocusHighlightEnabled(false); } mainView.setClickable(true); mainView.setLongClickable(true); mainView.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { // TODO: consider using two listeners and setOnTouchListener them if (mModeMouse) { return mTouchMouse.onTouchEvent(event); } else { return mJoystick.onTouchEvent(event); } } }); mainView.setNotifySizeChanged( new SpecialRelativeLayout.NotifySizeChanged() { public void onSizeChanged(final int w, final int h, int oldw, int oldh) { final long now = System.currentTimeMillis(); mScreenSizeTime = now; mKegsView.postDelayed(new Runnable() { public void run() { updateScreenSize(w, h, now); } }, 250); } } ); mKegsKeyboard = new KegsKeyboard(getThread().getEventQueue()); mKegsKeyboard.setOnStickyReset(this); findViewById(R.id.key_escape).setOnClickListener(mButtonClick); findViewById(R.id.key_return).setOnClickListener(mButtonClick); findViewById(R.id.key_tab).setOnClickListener(mButtonClick); findViewById(R.id.key_control).setOnClickListener(mButtonClick); findViewById(R.id.key_open_apple).setOnClickListener(mButtonClick); findViewById(R.id.key_closed_apple).setOnClickListener(mButtonClick); findViewById(R.id.key_left).setOnClickListener(mButtonClick); findViewById(R.id.key_right).setOnClickListener(mButtonClick); findViewById(R.id.key_up).setOnClickListener(mButtonClick); findViewById(R.id.key_down).setOnClickListener(mButtonClick); // Make sure local copy of internal disk images exist. if (android.os.Build.VERSION.SDK_INT >= 11) { new AssetImages(this, mConfigFile).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { new AssetImages(this, mConfigFile).execute(); } final String romfile = mConfigFile.whichRomFile(); if (romfile == null) { new RomDialogFragment().show(getFragmentManager(), FRAGMENT_ROM); } else { boot(); } } @Override protected void onPause() { super.onPause(); getThread().onPause(); mKegsView.onPause(); mPaused = true; } @Override protected void onResume() { super.onResume(); getThread().onResume(); mKegsView.onResume(); mPaused = false; Runnable runnable; while ((runnable = mResumeQueue.poll()) != null) { runnable.run(); } } @Override protected void onDestroy() { super.onDestroy(); mResumeQueue.clear(); Log.w("kegs", "onDestroy called, halting"); // Force process to exit. We cannot handle another onCreate // once a KegsView has been active. (JNI kegsmain has already run) java.lang.Runtime.getRuntime().halt(0); } static { System.loadLibrary("kegs"); } }