org.puder.trs80.EmulatorActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.puder.trs80.EmulatorActivity.java

Source

/*
 * Copyright 2012-2013, Arno Puder
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.puder.trs80;

import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.view.MenuItemCompat;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;

import com.crashlytics.android.Crashlytics;
import com.google.common.base.Optional;

import org.greenrobot.eventbus.EventBus;
import org.puder.trs80.cast.CastMessageSender;
import org.puder.trs80.cast.RemoteCastScreen;
import org.puder.trs80.configuration.Configuration;
import org.puder.trs80.configuration.ConfigurationManager;
import org.puder.trs80.configuration.EmulatorState;
import org.puder.trs80.configuration.KeyboardLayout;
import org.puder.trs80.io.FileManager;
import org.puder.trs80.keyboard.KeyboardManager;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import static org.puder.trs80.configuration.KeyboardLayout.KEYBOARD_EXTERNAL;
import static org.puder.trs80.configuration.KeyboardLayout.KEYBOARD_GAME_CONTROLLER;
import static org.puder.trs80.configuration.KeyboardLayout.KEYBOARD_TILT;

public class EmulatorActivity extends BaseActivity
        implements SensorEventListener, GameControllerListener, OnGlobalLayoutListener {

    public static final String EXTRA_CONFIGURATION_ID = "conf_id";
    private static final String TAG = "EmulatorActivity";

    // Action Menu
    private static final int MENU_OPTION_PAUSE = 0;
    private static final int MENU_OPTION_REWIND = 1;
    private static final int MENU_OPTION_RESET = 2;
    private static final int MENU_OPTION_PASTE = 3;
    private static final int MENU_OPTION_SOUND_ON = 4;
    private static final int MENU_OPTION_SOUND_OFF = 5;
    private static final int MENU_OPTION_TUTORIAL = 6;
    private static final int MENU_OPTION_HELP = 7;

    private static final String CONFIGURATION_TUTORIAL_NAME = "TRS-80 Tutorial";

    private Configuration currentConfiguration;
    private EmulatorState emulatorState;
    private Hardware currentHardware;
    private Thread cpuThread;
    private RenderThread renderThread;
    private TextView logView;
    private int orientation;
    private boolean soundMuted = false;
    private MenuItem pasteMenuItem = null;
    private MenuItem muteMenuItem = null;
    private MenuItem unmuteMenuItem = null;
    private SensorManager sensorManager = null;
    private Sensor sensorAccelerometer;
    private KeyboardManager keyboardManager;
    private ViewGroup keyboardContainer = null;
    private GameController gameController;
    private int rotation;
    private ClipboardManager clipboardManager;
    private AsyncTask taskSetup;
    private Rect windowRect;
    private boolean isCasting;
    private SurfaceHolder surfaceHolder;
    private boolean isGeneratingFont;
    private boolean isStopped;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TRS80Application.setCrashedFlag(false);

        Intent intent = getIntent();
        int id = intent.getIntExtra(EXTRA_CONFIGURATION_ID, -1);

        if (id == -1) {
            /*
             * We got killed by Android and then re-launched. The only thing we
             * can do is exit.
             */
            TRS80Application.setCrashedFlag(true);
            finish();
            return;
        }

        init(savedInstanceState, id);

        /*
         * We first set a dummy layout that only consists of one empty view. We set
         * a layout listener on this view in order to measure the screen dimensions.
         * This seems to be the only workable way to properly do this with N's split-
         * screen mode.
         */
        isGeneratingFont = true;
        isStopped = false;
        setContentView(R.layout.emulator_measure);
        final View root = findViewById(R.id.emulator_measure);
        root.getViewTreeObserver().addOnGlobalLayoutListener(this);

        AlertDialogUtil.showHint(this, R.string.hint_emulator, R.string.menu_tutorial, new Runnable() {
            @Override
            public void run() {
                showTutorial();
            }
        });
    }

    @Override
    public void onGlobalLayout() {
        removeGlobalLayoutListener();
        setupEmulator();
    }

    private void removeGlobalLayoutListener() {
        final View root = findViewById(R.id.emulator_measure);
        if (root == null || !root.getViewTreeObserver().isAlive()) {
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            root.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        } else {
            //noinspection deprecation
            root.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }

    private void init(Bundle savedInstanceState, int id) {
        try {
            emulatorState = EmulatorState.forConfigId(id, FileManager.Creator.get(getResources()));
            ConfigurationManager configManager = ConfigurationManager.get(getApplicationContext());
            Optional<Configuration> configOpt = configManager.getConfigById(id);
            if (!configOpt.isPresent()) {
                Log.e(TAG, "Configuration not found.");
                return;
            }
            currentConfiguration = configOpt.get();
        } catch (IOException e) {
            Log.e(TAG, "Cannot create emulator state.", e);
            return;
        }
        isCasting = CastMessageSender.get().isReadyToSend();
        currentHardware = new Hardware(currentConfiguration);

        clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);

        keyboardManager = new KeyboardManager();
        gameController = new GameController(this);

        boolean isInMultiWindowMode = false;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            isInMultiWindowMode = isInMultiWindowMode();
        }

        orientation = getResources().getConfiguration().orientation;
        if (orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE && !isCasting
                && !isInMultiWindowMode) {
            getSupportActionBar().hide();
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
        } else {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        if (savedInstanceState == null) {
            int err = XTRS.init(currentConfiguration, emulatorState);
            if (err != 0) {
                //showError(err);
                finish();
                return;
            }
            RemoteCastScreen.get().sendConfiguration(currentConfiguration);
            emulatorState.loadState();
        }
    }

    private void setupEmulator() {
        windowRect = new Rect();
        View root = findViewById(R.id.emulator_measure);
        windowRect.top = 0;
        windowRect.left = 0;
        windowRect.right = root.getWidth();
        windowRect.bottom = root.getHeight();
        initRootView();
        startEmulation();
    }

    private void startEmulation() {
        XTRS.setEmulatorActivity(this);

        updateMenuIcons();

        RemoteCastScreen.get().startSession();
        if (getKeyboardType() == KEYBOARD_TILT) {
            startAccelerometer();
        }

        isGeneratingFont = true;

        taskSetup = new AsyncTask<Rect, Void, Void>() {
            @Override
            protected Void doInBackground(Rect... rects) {
                currentHardware.generateFont(rects[0], this);
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                isGeneratingFont = false;
                if (isStopped) {
                    return;
                }
                findViewById(R.id.spinner).setVisibility(View.GONE);
                /*
                 * The following requestLayout is necessary because sometimes layouting
                 * R.layout.emulator that is set in initRootView() finishes before the
                 * TRS screen dimensions have been computed in doInBackground() of this
                 * AsyncTask.
                 */
                findViewById(R.id.screen).requestLayout();
                createRenderThread();
                renderThread.setHardwareSpecs(currentHardware);
                renderThread.setSurfaceHolder(surfaceHolder);
                renderThread.start();
                startCPUThread();
            }
        }.execute(windowRect);
    }

    @Override
    public void onRestart() {
        super.onRestart();
        if (TRS80Application.hasCrashed()) {
            return;
        }
        isStopped = false;
        if (taskSetup == null && !isGeneratingFont) {
            startEmulation();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (TRS80Application.hasCrashed()) {
            return;
        }
        isStopped = true;
        removeGlobalLayoutListener();
        RemoteCastScreen.get().endSession();
        if (getKeyboardType() == KEYBOARD_TILT) {
            stopAccelerometer();
        }
        if (taskSetup != null) {
            taskSetup.cancel(true);
            taskSetup = null;
        }
        Log.d(TAG, "Stopping CPU thread...");
        stopCPUThread();
        Log.d(TAG, "Stopping render thread...");
        stopRenderThread();
        Log.d(TAG, "Done.");
        XTRS.setEmulatorActivity(null);
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_OPTION_PAUSE, Menu.NONE, this.getString(R.string.menu_pause))
                        .setIcon(R.drawable.pause_icon),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_OPTION_RESET, Menu.NONE, this.getString(R.string.menu_reset))
                        .setIcon(R.drawable.reset_icon),
                MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_OPTION_REWIND, Menu.NONE, this.getString(R.string.menu_rewind))
                        .setIcon(R.drawable.rewind_icon),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        pasteMenuItem = menu.add(Menu.NONE, MENU_OPTION_PASTE, Menu.NONE, this.getString(R.string.menu_paste));
        MenuItemCompat.setShowAsAction(pasteMenuItem.setIcon(R.drawable.paste_icon),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        if (currentConfiguration.isSoundMuted()) {
            // Mute sound permanently and don't show mute/unmute icons
            setSoundMuted(true);
        } else {
            muteMenuItem = menu.add(Menu.NONE, MENU_OPTION_SOUND_OFF, Menu.NONE,
                    this.getString(R.string.menu_sound_off));
            MenuItemCompat.setShowAsAction(muteMenuItem.setIcon(R.drawable.sound_off_icon),
                    MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
            unmuteMenuItem = menu.add(Menu.NONE, MENU_OPTION_SOUND_ON, Menu.NONE,
                    this.getString(R.string.menu_sound_on));
            MenuItemCompat.setShowAsAction(unmuteMenuItem.setIcon(R.drawable.sound_on_icon),
                    MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
        }
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_OPTION_TUTORIAL, Menu.NONE, this.getString(R.string.menu_tutorial)),
                MenuItemCompat.SHOW_AS_ACTION_NEVER);
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_OPTION_HELP, Menu.NONE, this.getString(R.string.menu_help))
                        .setIcon(R.drawable.help_icon_white),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        updateMenuIcons();
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
        case MENU_OPTION_PAUSE:
            finish();
            return true;
        case MENU_OPTION_REWIND:
            View root = findViewById(R.id.emulator);
            Snackbar.make(root, R.string.rewinding_cassette, Snackbar.LENGTH_SHORT).show();
            XTRS.rewindCassette();
            return true;
        case MENU_OPTION_RESET:
            XTRS.reset();
            return true;
        case MENU_OPTION_PASTE:
            paste();
            return true;
        case MENU_OPTION_SOUND_ON:
            setSoundMuted(true);
            updateMenuIcons();
            return true;
        case MENU_OPTION_SOUND_OFF:
            setSoundMuted(false);
            updateMenuIcons();
            return true;
        case MENU_OPTION_TUTORIAL:
            showTutorial();
            return true;
        case MENU_OPTION_HELP:
            showDialog(R.string.help_title_emulator, -1, R.string.help_emulator);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (gameController.dispatchKeyEvent(event)) {
            return true;
        }
        boolean consumed = false;
        if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
            consumed = keyboardManager.keyDown(event);
        }
        if (event.getAction() == KeyEvent.ACTION_UP && event.getRepeatCount() == 0) {
            consumed = keyboardManager.keyUp(event);
        }
        return consumed || super.dispatchKeyEvent(event);
    }

    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {
        return gameController.dispatchGenericMotionEvent(event);
    }

    @Override
    public void onGameControllerAction(GameController.Action action) {
        switch (action) {
        case LEFT_DOWN:
            keyboardManager.pressKeyLeft();
            break;
        case LEFT_UP:
            keyboardManager.unpressKeyLeft();
            break;
        case TOP_DOWN:
            keyboardManager.pressKeyUp();
            break;
        case TOP_UP:
            keyboardManager.unpressKeyUp();
            break;
        case RIGHT_DOWN:
            keyboardManager.pressKeyRight();
            break;
        case RIGHT_UP:
            keyboardManager.unpressKeyRight();
            break;
        case BOTTOM_DOWN:
            keyboardManager.pressKeyDown();
            break;
        case BOTTOM_UP:
            keyboardManager.unpressKeyDown();
            break;
        case CENTER_DOWN:
            keyboardManager.pressKeySpace();
            break;
        case CENTER_UP:
            keyboardManager.unpressKeySpace();
            break;
        }
    }

    public void onKeyboardSwitchClicked(View view) {
        List<String> keyboardTypes = Arrays.asList(getResources().getStringArray(R.array.conf_keyboard_type));
        AlertDialog.Builder builder = AlertDialogUtil.createAlertDialog(this);
        builder.setSingleChoiceItems(keyboardTypes.toArray(new String[keyboardTypes.size()]), getKeyboardType().id,
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        AlertDialogUtil.dismissDialog(EmulatorActivity.this);
                        Optional<KeyboardLayout> layout = KeyboardLayout.fromId(which);
                        if (!layout.isPresent()) {
                            return;
                        }

                        switch (orientation) {
                        case android.content.res.Configuration.ORIENTATION_LANDSCAPE:
                            currentConfiguration.setKeyboardLayoutLandscape(layout.get());
                            break;
                        default:
                            currentConfiguration.setKeyboardLayoutPortrait(layout.get());
                            break;
                        }
                        initKeyboardView();
                    }
                });
        AlertDialogUtil.showDialog(this, builder);
    }

    private void createRenderThread() {
        if (renderThread != null) {
            return;
        }
        renderThread = new RenderThread(isCasting);
        renderThread.setPriority(Thread.MAX_PRIORITY);
        renderThread.setHardwareSpecs(currentHardware);
        renderThread.setSurfaceHolder(surfaceHolder);
    }

    private void stopRenderThread() {
        boolean retry = true;
        if (renderThread != null && renderThread.isAlive()) {
            renderThread.setRunning(false);
            renderThread.interrupt();
            while (retry) {
                try {
                    renderThread.join();
                    retry = false;
                } catch (InterruptedException e) {
                }
            }
            takeScreenshot();
        }
        renderThread = null;
    }

    private void startCPUThread() {
        cpuThread = new Thread(new Runnable() {

            @Override
            public void run() {
                XTRS.run();
            }
        });
        XTRS.setRunning(true);
        cpuThread.setPriority(Thread.MAX_PRIORITY);
        cpuThread.start();
    }

    private void stopCPUThread() {
        if (cpuThread == null) {
            return;
        }
        boolean retry = true;
        XTRS.setRunning(false);
        while (retry) {
            try {
                cpuThread.join();
                retry = false;
            } catch (InterruptedException e) {
            }
        }
        cpuThread = null;
        currentConfiguration.setCassettePosition(XTRS.getCassettePosition());
        emulatorState.saveState();
    }

    private void startAccelerometer() {
        lockOrientation();
        if (sensorManager == null) {
            sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
            sensorAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        }
        sensorManager.registerListener(this, sensorAccelerometer, SensorManager.SENSOR_DELAY_GAME);
    }

    private void stopAccelerometer() {
        unlockScreenOrientation();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
    }

    private void initRootView() {
        setContentView(R.layout.emulator);
        View top = this.findViewById(R.id.emulator);
        top.setFocusable(true);
        top.setFocusableInTouchMode(true);
        top.requestFocus();
        logView = (TextView) findViewById(R.id.log);

        if (CastMessageSender.get().isReadyToSend()) {
            ImageView castIcon = (ImageView) findViewById(R.id.cast_icon);
            castIcon.setVisibility(View.VISIBLE);
        } else {
            Screen screen = (Screen) findViewById(R.id.screen);
            screen.setVisibility(View.VISIBLE);
        }

        initKeyboardView();
    }

    private void initKeyboardView() {
        stopAccelerometer();
        keyboardContainer = (ViewGroup) findViewById(R.id.keyboard_container);
        keyboardContainer.removeAllViews();
        final KeyboardLayout keyboardType = getKeyboardType();
        showKeyboardHint(keyboardType);
        if (keyboardType == KEYBOARD_GAME_CONTROLLER || keyboardType == KEYBOARD_EXTERNAL) {
            keyboardContainer.getRootView().findViewById(R.id.switch_keyboard).setVisibility(View.GONE);
            return;
        }
        keyboardContainer.getRootView().findViewById(R.id.switch_keyboard).setVisibility(View.VISIBLE);
        // if (android.os.Build.VERSION.SDK_INT >=
        // Build.VERSION_CODES.HONEYCOMB) {
        // root.setMotionEventSplittingEnabled(true);
        // }
        currentHardware.computeKeyDimensions(windowRect, getKeyboardType());

        int layoutId = 0;
        switch (keyboardType) {
        case KEYBOARD_LAYOUT_COMPACT:
            layoutId = R.layout.keyboard_compact;
            break;
        case KEYBOARD_LAYOUT_ORIGINAL:
            layoutId = R.layout.keyboard_original;
            break;
        case KEYBOARD_LAYOUT_JOYSTICK:
            layoutId = R.layout.keyboard_joystick;
            break;
        case KEYBOARD_TILT:
            layoutId = R.layout.keyboard_tilt;
            break;
        }
        getLayoutInflater().inflate(layoutId, keyboardContainer, true);

        /*
         * The following code is a hack to work around a problem with the
         * keyboard layout in Android. The second keyboard should have
         * visibility GONE initially when the keyboard layout is inflated.
         * However, doing so messes up the layout of the second keyboard
         * (R.id.keyboard_view_2). This does not happen when visibility is
         * VISIBLE. So, to work around this issue, the initial visibility in the
         * keyboard layout is VISIBLE and we use a layout listener to make it
         * GONE after the layout has been computed and just before it will be
         * rendered.
         */
        ViewTreeObserver vto = keyboardContainer.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {

                View kb2 = keyboardContainer.findViewById(R.id.keyboard_view_2);
                if (kb2 != null) {
                    kb2.setVisibility(View.GONE);
                }
                ViewTreeObserver obs = keyboardContainer.getViewTreeObserver();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    obs.removeOnGlobalLayoutListener(this);
                } else {
                    //noinspection deprecation
                    obs.removeGlobalOnLayoutListener(this);
                }
            }
        });
        keyboardContainer.requestLayout();
        if (keyboardType == KEYBOARD_TILT) {
            startAccelerometer();
        }
    }

    private void lockOrientation() {
        Display display = getWindowManager().getDefaultDisplay();
        rotation = display.getRotation();
        int height;
        int width;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) {
            height = display.getHeight();
            width = display.getWidth();
        } else {
            Point size = new Point();
            display.getSize(size);
            height = size.y;
            width = size.x;
        }
        switch (rotation) {
        case Surface.ROTATION_90:
            if (width > height) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
            }
            break;
        case Surface.ROTATION_180:
            if (height > width) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
            }
            break;
        case Surface.ROTATION_270:
            if (width > height) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            }
            break;
        default:
            if (height > width) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            }
        }
    }

    private void unlockScreenOrientation() {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
    }

    private void showKeyboardHint(KeyboardLayout keyboardType) {
        int messageId = -1;
        switch (keyboardType) {
        case KEYBOARD_LAYOUT_JOYSTICK:
            messageId = R.string.hint_keyboard_joystick;
            break;
        case KEYBOARD_TILT:
            messageId = R.string.hint_keyboard_tilt;
            break;
        case KEYBOARD_EXTERNAL:
            messageId = R.string.hint_keyboard_external;
            break;
        }
        if (messageId == -1) {
            return;
        }
        AlertDialogUtil.showHint(this, messageId);
    }

    private void paste() {
        if (!clipboardManager.hasPrimaryClip()) {
            return;
        }
        ClipData clip = clipboardManager.getPrimaryClip();
        ClipData.Item item = clip.getItemAt(0);
        CharSequence text = item.getText();
        if (text != null) {
            XTRS.paste(text.toString().replace('\n', '\015'));
        }
    }

    private void showTutorial() {
        Optional<String> name = currentConfiguration.getName();
        if (name.isPresent() && !CONFIGURATION_TUTORIAL_NAME.equals(name.get())) {
            showDialog(R.string.help_title_emulator, -1, R.string.tutorial_prereq);
            return;
        }
        new Tutorial(keyboardManager, findViewById(R.id.emulator)).show();
    }

    private KeyboardLayout getKeyboardType() {
        final android.content.res.Configuration conf = getResources().getConfiguration();
        if (conf.keyboard != android.content.res.Configuration.KEYBOARD_NOKEYS) {
            return KEYBOARD_EXTERNAL;
        }

        Optional<KeyboardLayout> landscape = currentConfiguration.getKeyboardLayoutLandscape();
        Optional<KeyboardLayout> portrait = currentConfiguration.getKeyboardLayoutPortrait();

        switch (orientation) {
        case android.content.res.Configuration.ORIENTATION_LANDSCAPE:
            if (landscape.isPresent()) {
                return landscape.get();
            }
        default:
            if (portrait.isPresent()) {
                return portrait.get();
            }
        }
        return KeyboardLayout.KEYBOARD_LAYOUT_ORIGINAL;
    }

    private void setSoundMuted(boolean muted) {
        this.soundMuted = muted;
        XTRS.setSoundMuted(muted);
    }

    private void takeScreenshot() {
        int width = currentHardware.getScreenWidth();
        int height = currentHardware.getScreenHeight();
        if (renderThread != null && width > 0 && height > 0) {
            int id = currentConfiguration.getId();
            Bitmap screenshot = renderThread.takeScreenshot(currentHardware);
            emulatorState.saveScreenshot(screenshot);
            EventBus.getDefault().post(new ScreenshotTakenEvent(id));
        }
    }

    private void updateMenuIcons() {
        if (muteMenuItem != null && unmuteMenuItem != null) {
            if (soundMuted) {
                muteMenuItem.setVisible(true);
                unmuteMenuItem.setVisible(false);
            } else {
                muteMenuItem.setVisible(false);
                unmuteMenuItem.setVisible(true);
            }
        }

        if (pasteMenuItem != null) {
            boolean hasClip = false;
            if (clipboardManager.hasPrimaryClip()) {
                ClipData clip = clipboardManager.getPrimaryClip();
                ClipData.Item item = clip.getItemAt(0);
                CharSequence text = item.getText();
                hasClip = (text != null) && !text.equals("");
            }
            pasteMenuItem.setEnabled(hasClip);
        }
    }

    public void log(final String msg) {
        logView.post(new Runnable() {

            @Override
            public void run() {
                logView.setText(msg);
            }
        });
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    /**
     * negateX, negateY, xSrc, ySrc
     */
    final static int[][] axisSwap = { { 1, -1, 0, 1 }, // ROTATION_0
            { -1, -1, 1, 0 }, // ROTATION_90
            { -1, 1, 0, 1 }, // ROTATION_180
            { 1, 1, 1, 0 } }; // ROTATION_270

    final static float ACCELEROMETER_PRESS_THRESHOLD = 1.0f;
    final static float ACCELEROMETER_UNPRESS_THRESHOLD = 0.3f;

    private boolean leftKeyPressed = false;
    private boolean rightKeyPressed = false;
    private boolean upKeyPressed = false;
    private boolean downKeyPressed = false;

    @Override
    public void onSensorChanged(SensorEvent event) {
        int[] as = axisSwap[rotation];
        float xValue = as[0 /* negateX */] * event.values[as[2/* xSrc */]];
        float yValue = as[1 /* negateY */] * event.values[as[3 /* ySrc */]];
        if (xValue < -ACCELEROMETER_PRESS_THRESHOLD && !rightKeyPressed) {
            rightKeyPressed = true;
            keyboardManager.pressKeyRight();
        }
        if (xValue > -ACCELEROMETER_UNPRESS_THRESHOLD && rightKeyPressed) {
            rightKeyPressed = false;
            keyboardManager.unpressKeyRight();
        }
        if (xValue > ACCELEROMETER_PRESS_THRESHOLD && !leftKeyPressed) {
            leftKeyPressed = true;
            keyboardManager.pressKeyLeft();
        }
        if (xValue < ACCELEROMETER_UNPRESS_THRESHOLD && leftKeyPressed) {
            leftKeyPressed = false;
            keyboardManager.unpressKeyLeft();
        }
        if (yValue < -ACCELEROMETER_PRESS_THRESHOLD && !downKeyPressed) {
            downKeyPressed = true;
            keyboardManager.pressKeyDown();
        }
        if (yValue > -ACCELEROMETER_UNPRESS_THRESHOLD && downKeyPressed) {
            downKeyPressed = false;
            keyboardManager.unpressKeyDown();
        }
        if (yValue > ACCELEROMETER_PRESS_THRESHOLD && !upKeyPressed) {
            upKeyPressed = true;
            keyboardManager.pressKeyUp();
        }
        if (yValue < ACCELEROMETER_UNPRESS_THRESHOLD && upKeyPressed) {
            upKeyPressed = false;
            keyboardManager.unpressKeyUp();
        }
    }

    public void notImplemented(String msg) {
        TRS80Application.setCrashedFlag(true);
        Crashlytics.setString("NOT_IMPLEMENTED", msg);
        Crashlytics.setInt("MODEL", currentConfiguration.getModel());
        Crashlytics.setString("NAME", currentConfiguration.getName().or("-"));
        Optional<String> path = currentConfiguration.getDiskPath(0);
        Crashlytics.setString("DISK_0", path.or("-"));
        throw new RuntimeException();
    }

    public int getKeyWidth() {
        return currentHardware.getKeyWidth();
    }

    public int getKeyHeight() {
        return currentHardware.getKeyHeight();
    }

    public int getKeyMargin() {
        return currentHardware.getKeyMargin();
    }

    public KeyboardManager getKeyboardManager() {
        return keyboardManager;
    }

    public int getScreenWidth() {
        return currentHardware.getScreenWidth();
    }

    public int getScreenHeight() {
        return currentHardware.getScreenHeight();
    }

    public void setSurfaceHolder(SurfaceHolder holder) {
        surfaceHolder = holder;
        if (renderThread != null) {
            renderThread.setSurfaceHolder(holder);
        } else {
            stopRenderThread();
        }
    }
}