su.comp.bk.ui.BkEmuActivity.java Source code

Java tutorial

Introduction

Here is the source code for su.comp.bk.ui.BkEmuActivity.java

Source

/*
 * Created: 16.02.2012
 *
 * Copyright (C) 2012 Victor Antonovich (v.antonovich@gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package su.comp.bk.ui;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang.StringUtils;

import su.comp.bk.R;
import su.comp.bk.arch.Computer;
import su.comp.bk.arch.Computer.Configuration;
import su.comp.bk.arch.cpu.Cpu;
import su.comp.bk.arch.cpu.addressing.IndexDeferredAddressingMode;
import su.comp.bk.arch.cpu.opcode.EmtOpcode;
import su.comp.bk.arch.cpu.opcode.JsrOpcode;
import su.comp.bk.arch.io.FloppyController;
import su.comp.bk.arch.io.FloppyController.FloppyDriveIdentifier;
import su.comp.bk.arch.io.KeyboardController;
import su.comp.bk.arch.io.VideoController;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnKeyListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Main application activity.
 */
public class BkEmuActivity extends Activity {

    protected static final String TAG = BkEmuActivity.class.getName();

    // State save/restore: Last loaded emulator binary image file URI
    private static final String LAST_BIN_IMAGE_FILE_URI = BkEmuActivity.class.getName()
            + "#last_bin_image_file_uri";
    // State save/restore: Last selected disk image file URI
    private static final String LAST_DISK_IMAGE_FILE_URI = BkEmuActivity.class.getName()
            + "#last_disk_image_file_uri";

    public final static int STACK_TOP_ADDRESS = 01000;

    // Dialog IDs
    private static final int DIALOG_COMPUTER_MODEL = 1;
    private static final int DIALOG_ABOUT = 2;
    private static final int DIALOG_DISK_MOUNT_ERROR = 3;
    private static final int DIALOG_DISK_MANAGER = 4;

    // Intent request IDs
    private static final int REQUEST_MENU_BIN_IMAGE_FILE_LOAD = 1;
    private static final int REQUEST_EMT_BIN_IMAGE_FILE_LOAD = 2;
    private static final int REQUEST_MENU_DISK_IMAGE_FILE_SELECT = 3;

    // Google Play application URL to share
    private static final String APPLICATION_SHARE_URL = "https://play.google.com"
            + "/store/apps/details?id=su.comp.bk";

    public static final int MAX_TAPE_FILE_NAME_LENGTH = 16;

    private static final int MAX_FILE_NAME_DISPLAY_LENGTH = 15;
    private static final int FILE_NAME_DISPLAY_SUFFIX_LENGTH = 3;

    // Last loaded emulator binary image address
    protected int lastBinImageAddress;
    // Last loaded emulator binary image length
    protected int lastBinImageLength;
    // Last loaded emulator binary image URI string
    protected String lastBinImageFileUri;

    // Last selected disk image URI string
    protected String lastDiskImageFileUri;

    // Tape parameters block address
    protected int tapeParamsBlockAddr;

    private BkEmuView bkEmuView;

    protected Computer computer;

    protected String intentDataProgramImagePath;

    protected String intentDataDiskImagePath;

    protected Handler activityHandler;

    /**
     * Trying to load program image from path from intent data.
     */
    class IntentDataProgramImageLoader implements Runnable {
        @Override
        public void run() {
            try {
                int startAddress = loadBinImageFile(intentDataProgramImagePath);
                intentDataProgramImagePath = null;
                // Start loaded image
                synchronized (computer) {
                    if (startAddress < STACK_TOP_ADDRESS) {
                        // Loaded autostarting image
                        computer.getCpu().returnFromTrap(false);
                    } else {
                        // Loaded manually starting image
                        computer.getCpu().writeRegister(false, Cpu.PC, startAddress);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Can't load bootstrap emulator program image", e);
            }
        }
    }

    /**
     * Tape loader task.
     */
    class TapeLoaderTask implements Runnable {
        private final String tapeFileName;

        public TapeLoaderTask(String tapeFileName) {
            this.tapeFileName = tapeFileName;
        }

        @Override
        public void run() {
            boolean isBinImageLoaded = false;
            if (lastBinImageFileUri != null) {
                String binImageFilePath = null;
                try {
                    // Trying to load image file from last used location
                    binImageFilePath = lastBinImageFileUri.substring(0, lastBinImageFileUri.lastIndexOf('/') + 1)
                            .concat(tapeFileName);
                    loadBinImageFile(binImageFilePath);
                    isBinImageLoaded = true;
                } catch (Exception e) {
                    Log.d(TAG, "Can't load image from '" + binImageFilePath + "': " + e.getMessage());
                }
            }
            if (isBinImageLoaded) {
                doFinishBinImageLoad(true);
                computer.resume();
            } else {
                // Can't load image file from last used location, select file manually
                showBinImageFileLoadDialog(REQUEST_EMT_BIN_IMAGE_FILE_LOAD, tapeFileName);
            }
        }
    }

    /**
     * BK0010 tape operations handler.
     */
    class TapeOperations10Handler implements Cpu.OnTrapListener {
        @Override
        public void onTrap(Cpu cpu, int trapVectorAddress) {
            switch (trapVectorAddress) {
            case Cpu.TRAP_VECTOR_EMT:
                onEmtTrap(cpu);
                break;
            default:
                break;
            }
        }

        /**
         * EMT trap handler.
         * @param cpu {@link Cpu} reference
         */
        private void onEmtTrap(Cpu cpu) {
            int emtNumber = getTrapNumber(cpu, EmtOpcode.OPCODE);
            switch (emtNumber) {
            case 6: // EMT 6 - read char from keyboard
                if (intentDataProgramImagePath != null) {
                    // Monitor command prompt, load program from path from intent data
                    activityHandler.post(new IntentDataProgramImageLoader());
                } else if (intentDataDiskImagePath != null) {
                    // Monitor command prompt, trying to boot from mounted disk image
                    intentDataDiskImagePath = null;
                    // FIXME simulate bus error trap in case of boot error
                    cpu.push(cpu.readMemory(false, Cpu.TRAP_VECTOR_BUS_ERROR));
                    // Start booting
                    cpu.writeRegister(false, Cpu.PC, 0160000);
                }
                break;
            case 036: // EMT 36 - tape I/O
                // Check EMT handler isn't hooked
                int emtHandlerAddress = cpu.readMemory(false, Cpu.TRAP_VECTOR_EMT);
                if (computer.isReadOnlyMemoryAddress(emtHandlerAddress)) {
                    tapeParamsBlockAddr = cpu.readRegister(false, Cpu.R1);
                    Log.d(TAG, "EMT 36, R1=0" + Integer.toOctalString(tapeParamsBlockAddr));
                    handleTapeOperation(cpu);
                }
                break;
            case Computer.BUS_ERROR:
                Log.w(TAG, "Can't get EMT number");
                break;
            default:
                break;
            }
        }

        /**
         * Handle tape operation.
         * @param cpu {@link Cpu} reference
         */
        private void handleTapeOperation(Cpu cpu) {
            // Read command code
            int tapeCmdCode = cpu.readMemory(true, tapeParamsBlockAddr);
            switch (tapeCmdCode) {
            case 3: // Read from tape
                computer.pause();
                // Read file name
                byte[] tapeFileNameData = new byte[MAX_TAPE_FILE_NAME_LENGTH];
                for (int idx = 0; idx < tapeFileNameData.length; idx++) {
                    tapeFileNameData[idx] = (byte) cpu.readMemory(true, tapeParamsBlockAddr + idx + 6);
                }
                String tapeFileName = getFileName(tapeFileNameData);
                Log.d(TAG, "BK0010 tape load file: '" + tapeFileName + "'");
                activityHandler.post(new TapeLoaderTask(tapeFileName));
                break;
            default:
                break;
            }
        }
    }

    /**
     * BK0011 tape operations handler.
     */
    class TapeOperations11Handler implements Cpu.OnOpcodeListener {
        @Override
        public void onOpcodeExecuted(Cpu cpu, int opcode) {
            if (cpu.readRegister(false, Cpu.PC) == 0154620) {
                // .BMB10 BK0011 system call
                tapeParamsBlockAddr = cpu.readRegister(false, Cpu.R0);
                handleTapeOperation(cpu);
            }
        }

        /**
         * Handle tape operation.
         * @param cpu {@link Cpu} reference
         */
        private void handleTapeOperation(Cpu cpu) {
            // Read command code
            int tapeCmdCode = cpu.readMemory(true, tapeParamsBlockAddr);
            switch (tapeCmdCode) {
            case 1: // Read from tape
                computer.pause();
                // FIXME handle memory pages setup
                // Read file name
                byte[] tapeFileNameData = new byte[MAX_TAPE_FILE_NAME_LENGTH];
                for (int idx = 0; idx < tapeFileNameData.length; idx++) {
                    tapeFileNameData[idx] = (byte) cpu.readMemory(true, tapeParamsBlockAddr + idx + 6);
                }
                String tapeFileName = getFileName(tapeFileNameData);
                Log.d(TAG, "BK0011 tape load file: '" + tapeFileName + "'");
                activityHandler.post(new TapeLoaderTask(tapeFileName));
                break;
            default:
                break;
            }
        }
    }

    /**
     * Get string tape file name from its 16 bytes array presentation.
     * @param fileNameData internal file name array data
     * @return string file name presentation
     */
    public static String getFileName(byte[] fileNameData) {
        String fileName;
        if (fileNameData[0] != 0) { // BK0011 flag for any file
            try {
                fileName = new String(fileNameData, "koi8-r");
            } catch (UnsupportedEncodingException e) {
                fileName = new String(fileNameData);
            }
            fileName = fileName.trim().toUpperCase();
            // Strip spaces before extension (like in "NAME  .COD" in Basic)
            int dotIndex = fileName.lastIndexOf('.');
            if (dotIndex > 0) {
                fileName = fileName.substring(0, dotIndex).trim().concat(fileName.substring(dotIndex));
            }
        } else {
            fileName = "";
        }
        return fileName;
    }

    /**
     * Get trap (EMT/TRAP) number using pushed to stack PC.
     * @param cpu {@link Cpu} reference
     * @param trapBaseOpcode EMT/TRAP base opcode
     * @return trap number or BUS_ERROR in case of addressing error
     */
    public static int getTrapNumber(Cpu cpu, int trapBaseOpcode) {
        int trapNumber = Computer.BUS_ERROR;
        int pushedPc = cpu.readMemory(false, cpu.readRegister(false, Cpu.SP));
        if (pushedPc != Computer.BUS_ERROR) {
            // Read trap opcode
            int trapOpcode = cpu.readMemory(false, pushedPc - 2);
            if (trapOpcode != Computer.BUS_ERROR) {
                // Extract trap number from opcode
                trapNumber = trapOpcode - trapBaseOpcode;
            }
        }
        return trapNumber;
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate(), Intent: " + getIntent());
        super.onCreate(savedInstanceState);
        this.activityHandler = new Handler();
        LayoutInflater layoutInflater = getLayoutInflater();
        View mainView = layoutInflater.inflate(R.layout.main, null);
        bkEmuView = (BkEmuView) mainView.findViewById(R.id.emu_view);
        String intentDataString = getIntent().getDataString();
        if (intentDataString != null) {
            if (BkEmuFileDialog.isFileNameFormatMatched(intentDataString,
                    BkEmuFileDialog.FORMAT_FILTER_BIN_IMAGES)) {
                this.intentDataProgramImagePath = intentDataString;
            } else if (BkEmuFileDialog.isFileNameFormatMatched(intentDataString,
                    BkEmuFileDialog.FORMAT_FILTER_DISK_IMAGES)) {
                this.intentDataDiskImagePath = intentDataString;
            }
        }
        initializeComputer(savedInstanceState);
        // Mount intent disk image, if set
        if (this.intentDataDiskImagePath != null) {
            try {
                computer.getFloppyController().mountDiskImage(intentDataDiskImagePath, FloppyDriveIdentifier.A,
                        true);
            } catch (Exception e) {
                Log.e(TAG, "Can't mount bootstrap emulator disk image", e);
                this.intentDataDiskImagePath = null;
            }
        }
        View keyboardView = mainView.findViewById(R.id.keyboard);
        KeyboardController keyboardController = this.computer.getKeyboardController();
        keyboardController.setOnScreenKeyboardView(keyboardView);
        keyboardController.setOnScreenKeyboardVisibility(false);
        setContentView(mainView);
        // Show change log with latest changes once after application update
        BkEmuChangeLog changeLog = new BkEmuChangeLog(this);
        if (changeLog.isCurrentVersionGreaterThanLast()) {
            // Store current version to preferences store
            changeLog.saveCurrentVersionName();
            // Show change log dialog but not at the first run
            if (!changeLog.isFirstRun()) {
                changeLog.getDialog(false).show();
            }
        }
    }

    private void initializeComputer(Bundle savedInstanceState) {
        this.computer = new Computer();
        boolean isComputerInitialized = false;
        if (savedInstanceState != null) {
            // Trying to restore computer state
            try {
                this.computer.restoreState(getResources(), savedInstanceState);
                isComputerInitialized = true;
            } catch (Exception e) {
                Log.d(TAG, "Can't restore computer state", e);
            }
        }
        if (!isComputerInitialized) {
            // Computer state can't be restored, do startup initialization
            try {
                Configuration configuration = getComputerConfiguration();
                if (intentDataProgramImagePath != null) {
                    configuration = Configuration.BK_0010_MONITOR;
                } else if (intentDataDiskImagePath != null) {
                    configuration = Configuration.BK_0010_KNGMD;
                }
                this.computer.configure(getResources(), configuration);
                this.computer.reset();
                isComputerInitialized = true;
            } catch (Exception e) {
                Log.e(TAG, "Error while computer configuring", e);
            }
        }
        if (isComputerInitialized) {
            if (!computer.getConfiguration().isMemoryManagerPresent()) {
                computer.getCpu().setOnTrapListener(new TapeOperations10Handler());
            } else {
                TapeOperations11Handler handler = new TapeOperations11Handler();
                computer.getCpu().setOnOpcodeListener(
                        JsrOpcode.OPCODE | Cpu.PC | (Cpu.PC << 6) | (IndexDeferredAddressingMode.CODE << 3),
                        handler);
            }
            bkEmuView.setComputer(computer);
        } else {
            throw new IllegalStateException("Can't initialize computer state");
        }
    }

    protected void onStart() {
        Log.d(TAG, "onStart()");
        this.computer.start();
        super.onStart();
    }

    @Override
    protected void onRestart() {
        Log.d(TAG, "onRestart()");
        super.onRestart();
    }

    @Override
    protected void onResume() {
        Log.d(TAG, "onResume()");
        this.computer.resume();
        super.onResume();
    }

    @Override
    protected void onPause() {
        Log.d(TAG, "onPause()");
        this.computer.pause();
        super.onPause();
    }

    @Override
    protected void onStop() {
        Log.d(TAG, "onStop()");
        this.computer.stop();
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy()");
        this.computer.release();
        super.onDestroy();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        Log.d(TAG, "onSaveInstanceState()");
        // Save last emulator image file path
        outState.putString(LAST_BIN_IMAGE_FILE_URI, lastBinImageFileUri);
        // Save last disk image file path
        outState.putString(LAST_DISK_IMAGE_FILE_URI, lastDiskImageFileUri);
        this.computer.saveState(getResources(), outState);
        super.onSaveInstanceState(outState);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        // Restore last emulator image file path
        lastBinImageFileUri = savedInstanceState.getString(LAST_BIN_IMAGE_FILE_URI);
        // Restore last disk image file path
        lastDiskImageFileUri = savedInstanceState.getString(LAST_DISK_IMAGE_FILE_URI);
        super.onRestoreInstanceState(savedInstanceState);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return this.computer.getKeyboardController().handleKeyCode(keyCode, true)
                || super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return this.computer.getKeyboardController().handleKeyCode(keyCode, false) || super.onKeyUp(keyCode, event);
    }

    @Override
    public void onBackPressed() {
        KeyboardController keyboardController = this.computer.getKeyboardController();
        if (keyboardController.isOnScreenKeyboardVisible()) {
            keyboardController.setOnScreenKeyboardVisibility(false);
        } else {
            this.computer.pause();
            new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert)
                    .setTitle(R.string.exit_confirm_title).setMessage(R.string.exit_confirm_message)
                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            finish();
                        }
                    }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            BkEmuActivity.this.computer.resume();
                        }
                    }).setOnKeyListener(new OnKeyListener() {
                        @Override
                        public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP
                                    && !event.isCanceled()) {
                                BkEmuActivity.this.computer.resume();
                            }
                            return false;
                        }
                    }).show();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        boolean isFloppyControllerAttached = computer.getConfiguration().isFloppyControllerPresent();
        menu.findItem(R.id.menu_disk_manager).setEnabled(isFloppyControllerAttached);
        menu.findItem(R.id.menu_disk_manager).setVisible(isFloppyControllerAttached);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_toggle_keyboard:
            toggleOnScreenKeyboard();
            return true;
        case R.id.menu_toggle_screen_mode:
            toggleScreenMode();
            return true;
        case R.id.menu_reset:
            resetComputer();
            return true;
        case R.id.menu_change_model:
            showDialog(DIALOG_COMPUTER_MODEL);
            return true;
        case R.id.menu_open_image:
            showBinImageFileLoadDialog(REQUEST_MENU_BIN_IMAGE_FILE_LOAD, null);
            return true;
        case R.id.menu_disk_manager:
            showDialog(DIALOG_DISK_MANAGER);
            return true;
        case R.id.menu_about:
            showDialog(DIALOG_ABOUT);
            return true;
        case R.id.menu_share:
            shareApplication();
            return true;
        case R.id.menu_changelog:
            showChangelogDialog();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case DIALOG_COMPUTER_MODEL:
            final CharSequence[] models;
            List<String> modelList = new ArrayList<String>();
            for (Configuration model : Configuration.values()) {
                int modelNameId = getResources().getIdentifier(model.name().toLowerCase(), "string",
                        getPackageName());
                modelList.add((modelNameId != 0) ? getString(modelNameId) : model.name());
            }
            models = modelList.toArray(new String[modelList.size()]);
            return new AlertDialog.Builder(this).setTitle(R.string.menu_select_model).setSingleChoiceItems(models,
                    getComputerConfiguration().ordinal(), new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            // Mark selected item by tag
                            ListView listView = ((AlertDialog) dialog).getListView();
                            listView.setTag(Integer.valueOf(which));
                        }
                    }).setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // Get tagged selected item, if any
                            ListView listView = ((AlertDialog) dialog).getListView();
                            Integer selected = (Integer) listView.getTag();
                            if (selected != null) {
                                Configuration config = Configuration.values()[selected];
                                if (computer.getConfiguration() != config) {
                                    // Set new computer configuration and restart activity
                                    setComputerConfiguration(config);
                                    restartActivity(null);
                                }
                            }
                        }
                    }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // Do nothing on cancel
                        }
                    }).create();
        case DIALOG_ABOUT:
            Dialog aboutDialog = new Dialog(this);
            aboutDialog.setTitle(R.string.menu_about);
            aboutDialog.requestWindowFeature(Window.FEATURE_LEFT_ICON);
            aboutDialog.setContentView(R.layout.about_dialog);
            aboutDialog.getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
                    android.R.drawable.ic_dialog_info);
            TextView versionTextView = (TextView) aboutDialog.findViewById(R.id.about_version);
            try {
                versionTextView.setText(getResources().getString(R.string.about_version,
                        getPackageManager().getPackageInfo(getPackageName(), 0).versionName));
            } catch (NameNotFoundException e) {
            }
            return aboutDialog;
        case DIALOG_DISK_MANAGER:
            Dialog fddManagerDialog = new Dialog(this);
            fddManagerDialog.setTitle(R.string.menu_disk_manager);
            fddManagerDialog.setContentView(R.layout.fdd_mgr_dialog);
            return fddManagerDialog;
        case DIALOG_DISK_MOUNT_ERROR:
            return new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.err)
                    .setMessage(R.string.dialog_disk_mount_error).setPositiveButton(R.string.ok, null).create();
        }
        return null;
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch (id) {
        case DIALOG_DISK_MANAGER:
            prepareDiskManagerDialog(dialog);
            break;
        case DIALOG_ABOUT:
            prepareAboutDialog(dialog);
            break;
        }
        super.onPrepareDialog(id, dialog);
    }

    protected void prepareDiskManagerDialog(Dialog dialog) {
        prepareFloppyDriveView(dialog.findViewById(R.id.fdd_layout_a), FloppyDriveIdentifier.A);
        prepareFloppyDriveView(dialog.findViewById(R.id.fdd_layout_b), FloppyDriveIdentifier.B);
        prepareFloppyDriveView(dialog.findViewById(R.id.fdd_layout_c), FloppyDriveIdentifier.C);
        prepareFloppyDriveView(dialog.findViewById(R.id.fdd_layout_d), FloppyDriveIdentifier.D);
    }

    protected void prepareFloppyDriveView(final View fddView, final FloppyDriveIdentifier fddIdentifier) {
        updateFloppyDriveView(fddView, fddIdentifier);
        fddView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showMountDiskImageFileDialog(fddIdentifier);
            }
        });
        fddView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                unmountDiskImage(fddIdentifier);
                updateFloppyDriveView(v, fddIdentifier);
                return true;
            }
        });
    }

    protected void updateFloppyDriveView(final View fddView, final FloppyDriveIdentifier fddIdentifier) {
        FloppyController fddController = computer.getFloppyController();
        boolean isFddMounted = fddController.isFloppyDriveMounted(fddIdentifier);
        ImageView fddImageView = (ImageView) fddView.findViewWithTag("fdd_image");
        fddImageView.setImageResource(isFddMounted ? R.drawable.floppy_drive_loaded : R.drawable.floppy_drive);
        TextView fddFileTextView = (TextView) fddView.findViewWithTag("fdd_file");
        if (isFddMounted) {
            fddFileTextView.setTextColor(getResources().getColor(R.color.fdd_loaded));
            String fddImageFileName = Uri.parse(fddController.getFloppyDriveImageUri(fddIdentifier))
                    .getLastPathSegment();
            if (fddImageFileName.length() > MAX_FILE_NAME_DISPLAY_LENGTH) {
                // Trim file name to display
                int nameDotIndex = fddImageFileName.lastIndexOf('.');
                if (nameDotIndex < 0) {
                    nameDotIndex = fddImageFileName.length();
                }
                int nameSuffixIndex = nameDotIndex - FILE_NAME_DISPLAY_SUFFIX_LENGTH;
                int namePrefixIndex = MAX_FILE_NAME_DISPLAY_LENGTH - (fddImageFileName.length() - nameSuffixIndex);
                fddImageFileName = fddImageFileName.substring(0, namePrefixIndex).concat("...")
                        .concat(fddImageFileName.substring(nameSuffixIndex));
            }
            fddFileTextView.setText(fddImageFileName);
        } else {
            fddFileTextView.setTextColor(getResources().getColor(R.color.fdd_empty));
            fddFileTextView.setText(R.string.fdd_empty);
        }
    }

    protected void prepareAboutDialog(Dialog aboutDialog) {
        TextView perfTextView = (TextView) aboutDialog.findViewById(R.id.about_perf);
        float effectiveClockFrequency = this.computer.getEffectiveClockFrequency();
        perfTextView.setText(getResources().getString(R.string.about_perf, effectiveClockFrequency / 1000f,
                effectiveClockFrequency / this.computer.getClockFrequency() * 100f));
    }

    /**
     * Unmount disk image from given floppy drive.
     * @param fddIdentifier floppy drive identifier to unmount image
     */
    protected void unmountDiskImage(FloppyDriveIdentifier fddIdentifier) {
        try {
            FloppyController fddController = computer.getFloppyController();
            if (fddController != null && fddController.isFloppyDriveMounted(fddIdentifier)) {
                fddController.unmountDiskImage(fddIdentifier);
            }
        } catch (Exception e) {
            Log.e(TAG, "Floppy drive " + fddIdentifier + " unmounting error", e);
        }
    }

    /**
     * Show full changelog dialog.
     */
    private void showChangelogDialog() {
        new BkEmuChangeLog(this).getDialog(true).show();
    }

    /**
     * Get directory path for given file URI.
     * @param fileUriString file URI as string or <code>null</code>
     * @return directory path or external storage path if file URI is <code>null</code>
     * or given file directory doesn't exist
     */
    private static String getFileDirectoryPath(String fileUriString) {
        String directoryPath = Environment.getExternalStorageDirectory().getPath();
        if (fileUriString != null) {
            Uri fileUri = Uri.parse(fileUriString);
            File filePath = new File(fileUri.getPath());
            File fileDir = filePath.getParentFile();
            if (fileDir.exists()) {
                directoryPath = fileDir.getPath();
            }
        }
        return directoryPath;
    }

    /**
     * Show BIN emulator image file to load selection dialog.
     * @param tapeFileName file name to load (or <code>null</code> to load any file)
     */
    protected void showBinImageFileLoadDialog(int requestCode, String tapeFileName) {
        Intent intent = new Intent(getBaseContext(), BkEmuFileDialog.class);
        String startPath = getFileDirectoryPath(lastBinImageFileUri);
        intent.putExtra(BkEmuFileDialog.INTENT_START_PATH, startPath);
        if (tapeFileName != null && tapeFileName.length() > 0) {
            intent.putExtra(BkEmuFileDialog.INTENT_FORMAT_FILTER, new String[] { tapeFileName });
        }
        startActivityForResult(intent, requestCode);
    }

    /**
     * Show floppy disk image to mount selection dialog.
     * @param fddIdentifier floppy drive identifier to mount image
     */
    protected void showMountDiskImageFileDialog(FloppyDriveIdentifier fddIdentifier) {
        Intent intent = new Intent(getBaseContext(), BkEmuFileDialog.class);
        String startPath = getFileDirectoryPath(lastDiskImageFileUri);
        intent.putExtra(BkEmuFileDialog.INTENT_START_PATH, startPath);
        intent.putExtra(BkEmuFileDialog.INTENT_FORMAT_FILTER, BkEmuFileDialog.FORMAT_FILTER_DISK_IMAGES);
        intent.putExtra(FloppyDriveIdentifier.class.getName(), fddIdentifier.name());
        startActivityForResult(intent, REQUEST_MENU_DISK_IMAGE_FILE_SELECT);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.d(TAG, "onActivityResult()");
        switch (requestCode) {
        case REQUEST_MENU_BIN_IMAGE_FILE_LOAD:
            if (resultCode == Activity.RESULT_OK) {
                String binImageFilePath = data.getStringExtra(BkEmuFileDialog.INTENT_RESULT_PATH);
                Configuration configuration = computer.getConfiguration();
                if (configuration.isMemoryManagerPresent() || configuration.isFloppyControllerPresent()) {
                    binImageFileLoad(binImageFilePath);
                } else {
                    Uri binImageFileUri = new Uri.Builder().scheme("file").path(binImageFilePath).build();
                    restartActivity(binImageFileUri);
                }
            }
            break;
        case REQUEST_EMT_BIN_IMAGE_FILE_LOAD:
            boolean isImageLoaded = false;
            if (resultCode == Activity.RESULT_OK) {
                String binImageFilePath = data.getStringExtra(BkEmuFileDialog.INTENT_RESULT_PATH);
                isImageLoaded = binImageFileLoad(binImageFilePath);
            }
            doFinishBinImageLoad(isImageLoaded);
            break;
        case REQUEST_MENU_DISK_IMAGE_FILE_SELECT:
            FloppyController floppyController = computer.getFloppyController();
            if (resultCode == Activity.RESULT_OK && floppyController != null) {
                String diskImageFilePath = data.getStringExtra(BkEmuFileDialog.INTENT_RESULT_PATH);
                String diskImageFileUri = "file:" + diskImageFilePath;
                try {
                    FloppyDriveIdentifier driveIdentifier = FloppyDriveIdentifier
                            .valueOf(data.getStringExtra(FloppyDriveIdentifier.class.getName()));
                    floppyController.mountDiskImage(diskImageFileUri, driveIdentifier, false);
                    lastDiskImageFileUri = diskImageFileUri;
                    showDialog(DIALOG_DISK_MANAGER);
                } catch (Exception e) {
                    showDialog(DIALOG_DISK_MOUNT_ERROR);
                    Log.e(TAG, "can't mount disk image '" + diskImageFileUri + "'", e);
                }
            }
            break;
        default:
            break;
        }
    }

    protected boolean binImageFileLoad(String binImageFilePath) {
        boolean isImageLoaded = doBinImageFileLoad(binImageFilePath);
        if (isImageLoaded) {
            Toast.makeText(getApplicationContext(),
                    getResources().getString(R.string.toast_image_info, lastBinImageAddress, lastBinImageLength),
                    Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(getApplicationContext(),
                    getResources().getString(R.string.toast_image_error, binImageFilePath), Toast.LENGTH_LONG)
                    .show();
        }
        return isImageLoaded;
    }

    protected boolean doBinImageFileLoad(String binImageFilePath) {
        boolean isImageLoaded = false;
        try {
            loadBinImageFile("file:" + binImageFilePath);
            isImageLoaded = true;
        } catch (Exception e) {
            Log.e(TAG, "Can't load emulator image", e);
        }
        return isImageLoaded;
    }

    protected void doFinishBinImageLoad(boolean isImageLoadedSuccessfully) {
        // Set result in parameters block
        if (isImageLoadedSuccessfully) {
            synchronized (computer) {
                int tapeParamsBlockAddrNameIdx;
                if (!computer.getConfiguration().isMemoryManagerPresent()) { // BK0010
                    tapeParamsBlockAddrNameIdx = 26;
                    // Set "OK" result code
                    computer.writeMemory(true, tapeParamsBlockAddr + 1, 0);
                    // Write loaded image start address
                    computer.writeMemory(false, tapeParamsBlockAddr + 22, lastBinImageAddress);
                    // Write loaded image length
                    computer.writeMemory(false, tapeParamsBlockAddr + 24, lastBinImageLength);
                    // Return from EMT 36
                    computer.getCpu().returnFromTrap(false);
                } else { // BK0011
                    tapeParamsBlockAddrNameIdx = 28;
                    // Set "OK" result code
                    computer.getCpu().clearPswFlagC();
                    // Write loaded image start address
                    computer.writeMemory(false, tapeParamsBlockAddr + 24, lastBinImageAddress);
                    // Write loaded image length
                    computer.writeMemory(false, tapeParamsBlockAddr + 26, lastBinImageLength);
                    // Return from tape load routine
                    computer.getCpu().writeRegister(false, Cpu.PC, computer.getCpu().pop());
                }
                // Write loaded image name
                String tapeFileName = StringUtils.substringAfterLast(lastBinImageFileUri, "/");
                tapeFileName = StringUtils.substring(tapeFileName, 0, MAX_TAPE_FILE_NAME_LENGTH);
                byte[] tapeFileNameBuffer;
                try {
                    tapeFileNameBuffer = tapeFileName.getBytes("koi8-r");
                } catch (UnsupportedEncodingException e) {
                    tapeFileNameBuffer = tapeFileName.getBytes();
                }
                byte[] tapeFileNameData = new byte[MAX_TAPE_FILE_NAME_LENGTH];
                Arrays.fill(tapeFileNameData, (byte) ' ');
                System.arraycopy(tapeFileNameBuffer, 0, tapeFileNameData, 0,
                        Math.min(tapeFileNameBuffer.length, MAX_TAPE_FILE_NAME_LENGTH));
                for (int idx = 0; idx < tapeFileNameData.length; idx++) {
                    computer.getCpu().writeMemory(true, tapeParamsBlockAddr + tapeParamsBlockAddrNameIdx + idx,
                            tapeFileNameData[idx]);
                }
            }
        }
    }

    /**
     * Do activity restart.
     * @param binImageFileUri emulator image file {@link Uri} to set to restarted activity
     * (or <code>null</code> to start activity without emulator image set)
     */
    protected void restartActivity(Uri binImageFileUri) {
        Intent intent = getIntent();
        intent.setData(binImageFileUri);
        finish();
        startActivity(intent);
    }

    /**
     * Get current computer configuration as {@link Configuration} enum value.
     * @return configuration enum value
     */
    protected Configuration getComputerConfiguration() {
        SharedPreferences prefs = getPreferences(MODE_PRIVATE);
        String configName = prefs.getString(Configuration.class.getName(), null);
        return (configName == null) ? Configuration.BK_0010_BASIC : Configuration.valueOf(configName);
    }

    /**
     * Set current computer configuration set as {@link Configuration} enum value.
     * @param configuration configuration enum value to set
     */
    protected void setComputerConfiguration(Configuration configuration) {
        SharedPreferences prefs = getPreferences(MODE_PRIVATE);
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putString(Configuration.class.getName(), configuration.name());
        prefsEditor.commit();
    }

    /**
     * Load program image in bin format (address/length/data) from given path.
     * @param binImageFilePath emulator image file path
     * @return start address of loaded emulator image
     * @throws Exception in case of loading error
     */
    protected int loadBinImageFile(String binImageFilePath) throws Exception {
        Log.d(TAG, "loading image: " + binImageFilePath);
        BufferedInputStream binImageInput = new BufferedInputStream(new URL(binImageFilePath).openStream());
        ByteArrayOutputStream binImageOutput = new ByteArrayOutputStream();
        int readByte;
        while ((readByte = binImageInput.read()) != -1) {
            binImageOutput.write(readByte);
        }
        this.lastBinImageFileUri = binImageFilePath;
        return loadBinImage(binImageOutput.toByteArray());
    }

    /**
     * Load image in bin format (address/length/data) from byte array.
     * @param imageData image data byte array
     * @throws IOException in case of loading error
     */
    public int loadBinImage(byte[] imageData) throws IOException {
        if (imageData.length < 5 || imageData.length > 01000000) {
            throw new IllegalArgumentException("Invalid binary image file length: " + imageData.length);
        }
        DataInputStream imageDataInputStream = new DataInputStream(
                new ByteArrayInputStream(imageData, 0, imageData.length));
        lastBinImageAddress = (imageDataInputStream.readByte() & 0377)
                | ((imageDataInputStream.readByte() & 0377) << 8);
        lastBinImageLength = (imageDataInputStream.readByte() & 0377)
                | ((imageDataInputStream.readByte() & 0377) << 8);
        synchronized (computer) {
            for (int imageIndex = 0; imageIndex < lastBinImageLength; imageIndex++) {
                if (!computer.writeMemory(true, lastBinImageAddress + imageIndex, imageDataInputStream.read())) {
                    throw new IllegalStateException("Can't write binary image data to address: 0"
                            + Integer.toOctalString(lastBinImageAddress) + imageIndex);
                }
            }
        }
        Log.d(TAG, "loaded bin image file: address 0" + Integer.toOctalString(lastBinImageAddress) + ", length: "
                + lastBinImageLength);
        return lastBinImageAddress;
    }

    private void shareApplication() {
        Intent appShareIntent = new Intent(Intent.ACTION_SEND);
        appShareIntent.setType("text/plain");
        appShareIntent.putExtra(Intent.EXTRA_TEXT, APPLICATION_SHARE_URL);
        startActivity(Intent.createChooser(appShareIntent, null));
    }

    private void toggleOnScreenKeyboard() {
        Log.d(TAG, "toggling on-screen keyboard");
        KeyboardController keyboardController = computer.getKeyboardController();
        keyboardController.setOnScreenKeyboardVisibility(!keyboardController.isOnScreenKeyboardVisible());
    }

    private void toggleScreenMode() {
        Log.d(TAG, "toggling screen mode");
        VideoController videoController = computer.getVideoController();
        videoController.setColorMode(!videoController.isColorMode());
    }

    private void resetComputer() {
        Log.d(TAG, "resetting computer");
        Configuration config = getComputerConfiguration();
        if (computer.getConfiguration() != config) {
            // Set new computer configuration and restart activity
            setComputerConfiguration(config);
            restartActivity(null);
        } else {
            computer.reset();
        }
    }
}