com.thomasokken.free42.Free42Activity.java Source code

Java tutorial

Introduction

Here is the source code for com.thomasokken.free42.Free42Activity.java

Source

/*****************************************************************************
 * Free42 -- an HP-42S calculator simulator
 * Copyright (C) 2004-2019  Thomas Okken
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2,
 * as published by the Free Software Foundation.
 *
 * 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 com.thomasokken.free42;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.IntBuffer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Vibrator;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;

/**
 * This Activity class contains most of the Free42 'shell' functionality;
 * the skin-specific code is separated into the SkinLayout class.
 * This class works in conjunction with free42glue.cc, which is the JNI-
 * based interface to the Free42 'core' functionality (the core is
 * C++ and porting it to Java is not practical, hence the use of JNI).
 */
public class Free42Activity extends Activity {

    private static final String[] builtinSkinNames = new String[] { "Standard", "Landscape" };

    private static final int SHELL_VERSION = 11;

    private static final int PRINT_BACKGROUND_COLOR = Color.LTGRAY;

    private static final int MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;

    public static Free42Activity instance;

    public static final String MY_STORAGE_DIR = "/sdcard/Android/data/com.thomasokken.free42";

    static {
        System.loadLibrary("free42");
    }

    private CalcView calcView;
    private SkinLayout skin;
    private PrintView printView;
    private ScrollView printScrollView;
    private boolean printViewShowing;
    private PreferencesDialog preferencesDialog;
    private AlertDialog mainMenuDialog;
    private Handler mainHandler;
    private boolean alwaysOn;

    private SoundPool soundPool;
    private int[] soundIds;

    // Streams for reading and writing the state file
    private InputStream stateFileInputStream;
    private OutputStream stateFileOutputStream;

    // Streams for program import and export
    private InputStream programsInputStream;
    private OutputStream programsOutputStream;

    // Stuff to run core_keydown() on a background thread
    private CoreThread coreThread;
    private boolean coreWantsCpu;

    private int ckey;
    private boolean timeout3_active;

    private boolean low_battery;
    private BroadcastReceiver lowBatteryReceiver;

    // Persistent state
    private int orientation = 0; // 0=portrait, 1=landscape
    private String[] skinName = new String[] { builtinSkinNames[0], builtinSkinNames[0] };
    private String[] externalSkinName = new String[2];
    private boolean[] skinSmoothing = new boolean[2];
    private boolean[] displaySmoothing = new boolean[2];

    private boolean alwaysRepaintFullDisplay = false;
    private boolean keyClicksEnabled = true;
    private boolean keyVibrationEnabled = false;
    private int preferredOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
    private int style = 0;

    private final Runnable repeaterCaller = new Runnable() {
        public void run() {
            repeater();
        }
    };
    private final Runnable timeout1Caller = new Runnable() {
        public void run() {
            timeout1();
        }
    };
    private final Runnable timeout2Caller = new Runnable() {
        public void run() {
            timeout2();
        }
    };
    private final Runnable timeout3Caller = new Runnable() {
        public void run() {
            timeout3();
        }
    };

    ///////////////////////////////////////////////////////
    ///// Top-level code to interface with Android UI /////
    ///////////////////////////////////////////////////////

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

        int init_mode;
        IntHolder version = new IntHolder();
        try {
            stateFileInputStream = openFileInput("state");
        } catch (FileNotFoundException e) {
            stateFileInputStream = null;
        }
        if (stateFileInputStream != null) {
            if (read_shell_state(version))
                init_mode = 1;
            else {
                init_shell_state(-1);
                init_mode = 2;
            }
        } else {
            init_shell_state(-1);
            init_mode = 0;
        }
        setAlwaysRepaintFullDisplay(alwaysRepaintFullDisplay);
        if (alwaysOn)
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        if (style == 1)
            setTheme(android.R.style.Theme_NoTitleBar_Fullscreen);
        else if (style == 2) {
            try {
                Method m = View.class.getMethod("setSystemUiVisibility", int.class);
                m.invoke(getWindow().getDecorView(), PreferencesDialog.immersiveModeFlags);
            } catch (Exception e) {
            }
        }

        Configuration conf = getResources().getConfiguration();
        orientation = conf.orientation == Configuration.ORIENTATION_LANDSCAPE ? 1 : 0;

        mainHandler = new Handler();
        calcView = new CalcView(this);
        setContentView(calcView);
        printView = new PrintView(this);
        printScrollView = new ScrollView(this);
        printScrollView.setBackgroundColor(PRINT_BACKGROUND_COLOR);
        printScrollView.addView(printView);

        skin = null;
        if (skinName[orientation].length() == 0 && externalSkinName[orientation].length() > 0) {
            try {
                skin = new SkinLayout(externalSkinName[orientation], skinSmoothing[orientation],
                        displaySmoothing[orientation]);
            } catch (IllegalArgumentException e) {
            }
        }
        if (skin == null) {
            try {
                skin = new SkinLayout(skinName[orientation], skinSmoothing[orientation],
                        displaySmoothing[orientation]);
            } catch (IllegalArgumentException e) {
            }
        }
        if (skin == null) {
            try {
                skin = new SkinLayout(builtinSkinNames[0], skinSmoothing[orientation],
                        displaySmoothing[orientation]);
            } catch (IllegalArgumentException e) {
                // This one should never fail; we're loading a built-in skin.
            }
        }

        nativeInit();
        core_init(init_mode, version.value);
        if (stateFileInputStream != null) {
            try {
                stateFileInputStream.close();
            } catch (IOException e) {
            }
            stateFileInputStream = null;
        }

        lowBatteryReceiver = new BroadcastReceiver() {
            public void onReceive(Context ctx, Intent intent) {
                low_battery = intent.getAction().equals(Intent.ACTION_BATTERY_LOW);
                Rect inval = skin.update_annunciators(-1, -1, -1, -1, low_battery ? 1 : 0, -1, -1);
                if (inval != null)
                    calcView.postInvalidateScaled(inval.left, inval.top, inval.right, inval.bottom);
            }
        };
        IntentFilter iff = new IntentFilter();
        iff.addAction(Intent.ACTION_BATTERY_LOW);
        iff.addAction(Intent.ACTION_BATTERY_OKAY);
        registerReceiver(lowBatteryReceiver, iff);

        if (preferredOrientation != this.getRequestedOrientation())
            setRequestedOrientation(preferredOrientation);

        soundPool = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0);
        int[] soundResourceIds = { R.raw.tone0, R.raw.tone1, R.raw.tone2, R.raw.tone3, R.raw.tone4, R.raw.tone5,
                R.raw.tone6, R.raw.tone7, R.raw.tone8, R.raw.tone9, R.raw.squeak, R.raw.click };
        soundIds = new int[soundResourceIds.length];
        for (int i = 0; i < soundResourceIds.length; i++)
            soundIds[i] = soundPool.load(this, soundResourceIds[i], 1);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Check battery level -- this is necessary because the ACTTON_BATTERY_LOW
        // and ACTION_BATTERY_OKAY intents are not "sticky", i.e., we get those
        // notifications only when that status *changes*; we don't get any indication
        // of what that status *is* when the app is launched (or resumed?).
        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent batteryStatus = registerReceiver(null, ifilter);
        int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        if (low_battery)
            low_battery = level * 100 < scale * 20;
        else
            low_battery = level * 100 <= scale * 15;
        Rect inval = skin.update_annunciators(-1, -1, -1, -1, low_battery ? 1 : 0, -1, -1);
        if (inval != null)
            calcView.postInvalidateScaled(inval.left, inval.top, inval.right, inval.bottom);

        if (core_powercycle())
            start_core_keydown();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (style == 2) {
            try {
                Method m = View.class.getMethod("setSystemUiVisibility", int.class);
                m.invoke(getWindow().getDecorView(), PreferencesDialog.immersiveModeFlags);
            } catch (Exception e) {
            }
        }
    }

    @Override
    protected void onPause() {
        end_core_keydown();
        // Write state file
        File filesDir = getFilesDir();
        File stateFile = null;
        try {
            stateFile = File.createTempFile("state.", ".new", filesDir);
            stateFileOutputStream = new FileOutputStream(stateFile, true);
        } catch (IOException e) {
            stateFileOutputStream = null;
        }
        if (stateFileOutputStream != null) {
            write_shell_state();
            core_enter_background();
        }
        if (stateFileOutputStream != null) {
            try {
                stateFileOutputStream.close();
            } catch (IOException e) {
            }
            // Writing state file succeeded; rename state.new to state
            stateFile.renameTo(new File(filesDir, "state"));
            stateFileOutputStream = null;
        } else {
            // Writing state file failed; delete state.new, if it even exists
            if (stateFile != null)
                stateFile.delete();
        }
        printView.dump();
        if (printTxtStream != null) {
            try {
                printTxtStream.close();
            } catch (IOException e) {
            }
            printTxtStream = null;
        }
        if (printGifFile != null) {
            try {
                ShellSpool.shell_finish_gif(printGifFile);
            } catch (IOException e) {
            }
            try {
                printGifFile.close();
            } catch (IOException e) {
            }
            printGifFile = null;
        }
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        // N.B. In the Android build, core_quit() does not write the
        // core state; we assume that onPause() has been called previously,
        // and its core_enter_background() call takes care of saving state.
        // All this core_quit() call does it free up memory.
        core_quit();
        if (lowBatteryReceiver != null) {
            unregisterReceiver(lowBatteryReceiver);
            lowBatteryReceiver = null;
        }
        super.onDestroy();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (printViewShowing && keyCode == KeyEvent.KEYCODE_BACK) {
            doFlipCalcPrintout();
            return true;
        } else {
            return super.onKeyDown(keyCode, event);
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConf) {
        super.onConfigurationChanged(newConf);
        orientation = newConf.orientation == Configuration.ORIENTATION_LANDSCAPE ? 1 : 0;
        boolean[] ann_state = skin.getAnnunciators();
        SkinLayout newSkin = null;
        if (skinName[orientation].length() == 0 && externalSkinName[orientation].length() > 0) {
            try {
                newSkin = new SkinLayout(externalSkinName[orientation], skinSmoothing[orientation],
                        displaySmoothing[orientation], ann_state);
            } catch (IllegalArgumentException e) {
            }
        }
        if (newSkin == null) {
            try {
                newSkin = new SkinLayout(skinName[orientation], skinSmoothing[orientation],
                        displaySmoothing[orientation], ann_state);
            } catch (IllegalArgumentException e) {
            }
        }
        if (newSkin == null) {
            try {
                newSkin = new SkinLayout(builtinSkinNames[0], skinSmoothing[orientation],
                        displaySmoothing[orientation], ann_state);
            } catch (IllegalArgumentException e) {
                // This one should never fail; we're loading a built-in skin.
            }
        }
        if (newSkin != null)
            skin = newSkin;
        calcView.invalidate();
        core_repaint_display();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        // ignore
    }

    private void cancelRepeaterAndTimeouts1And2() {
        mainHandler.removeCallbacks(repeaterCaller);
        mainHandler.removeCallbacks(timeout1Caller);
        mainHandler.removeCallbacks(timeout2Caller);
    }

    private void cancelTimeout3() {
        mainHandler.removeCallbacks(timeout3Caller);
        timeout3_active = false;
    }

    private void postMainMenu() {
        if (mainMenuDialog == null) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("Main Menu");
            List<String> itemsList = new ArrayList<String>();
            itemsList.add("Copy");
            itemsList.add("Paste");
            itemsList.add("Preferences");
            itemsList.add("Show Print-Out");
            itemsList.add("Clear Print-Out");
            itemsList.add("About Free42");
            itemsList.add("Import Programs");
            itemsList.add("Export Programs");
            for (int i = 0; i < builtinSkinNames.length; i++)
                itemsList.add("Skin: \"" + builtinSkinNames[i] + "\"");
            itemsList.add("Skin: Other...");
            itemsList.add("Cancel");
            builder.setItems(itemsList.toArray(new String[itemsList.size()]),
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            mainMenuItemSelected(which);
                        }
                    });
            mainMenuDialog = builder.create();
        }
        mainMenuDialog.show();
    }

    private void mainMenuItemSelected(int which) {
        switch (which) {
        case 0:
            doCopy();
            return;
        case 1:
            doPaste();
            return;
        case 2:
            doPreferences();
            return;
        case 3:
            doFlipCalcPrintout();
            return;
        case 4:
            doClearPrintout();
            return;
        case 5:
            doAbout();
            return;
        case 6:
            doImport();
            return;
        case 7:
            doExport();
            return;
        default:
            int index = which - 8;
            if (index >= 0 && index < builtinSkinNames.length) {
                doSelectSkin(builtinSkinNames[index]);
                return;
            } else if (index == builtinSkinNames.length) {
                if (!checkStorageAccess())
                    return;
                FileSelectionDialog fsd = new FileSelectionDialog(this, new String[] { "layout", "*" }, false);
                if (externalSkinName[orientation].length() == 0)
                    fsd.setPath(topStorageDir() + "/Free42");
                else
                    fsd.setPath(externalSkinName[orientation] + ".layout");
                fsd.setOkListener(new FileSelectionDialog.OkListener() {
                    public void okPressed(String path) {
                        if (path.endsWith(".layout"))
                            doSelectSkin(path.substring(0, path.length() - 7));
                    }
                });
                fsd.show();
                return;
            }
        }
    }

    private void doCopy() {
        android.text.ClipboardManager clip = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
        clip.setText(core_copy());
    }

    private void doPaste() {
        android.text.ClipboardManager clip = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
        if (clip.hasText())
            core_paste(clip.getText().toString());
    }

    private void doFlipCalcPrintout() {
        printViewShowing = !printViewShowing;
        setContentView(printViewShowing ? printScrollView : calcView);
    }

    private void doClearPrintout() {
        printView.clear();
    }

    private void doImport() {
        if (!checkStorageAccess())
            return;
        FileSelectionDialog fsd = new FileSelectionDialog(this, new String[] { "raw", "*" }, false);
        fsd.setPath(topStorageDir());
        fsd.setOkListener(new FileSelectionDialog.OkListener() {
            public void okPressed(String path) {
                doImport2(path);
            }
        });
        fsd.show();
    }

    private void doImport2(String path) {
        try {
            programsInputStream = new FileInputStream(path);
        } catch (IOException e) {
            alert("Import failed: " + e.getMessage());
            return;
        }
        core_import_programs();
        redisplay();
        if (programsInputStream != null) {
            try {
                programsInputStream.close();
            } catch (IOException e) {
            }
            programsInputStream = null;
        }
    }

    private boolean[] selectedProgramIndexes;

    private void alert(String message) {
        runOnUiThread(new Alerter(message));
    }

    private class Alerter implements Runnable {
        private String message;

        public Alerter(String message) {
            this.message = message;
        }

        public void run() {
            AlertDialog.Builder builder = new AlertDialog.Builder(Free42Activity.this);
            builder.setMessage(message);
            builder.setPositiveButton("OK", null);
            builder.create().show();
        }
    }

    private void doExport() {
        if (!checkStorageAccess())
            return;
        String[] names = core_list_programs();
        selectedProgramIndexes = new boolean[names.length];

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Select Programs");
        builder.setMultiChoiceItems(names, selectedProgramIndexes,
                new DialogInterface.OnMultiChoiceClickListener() {
                    public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                        // I don't have to do anything here; the only reason why
                        // I create this listener is because if I pass 'null'
                        // instead, the selectedProgramIndexes array never gets
                        // updated.
                    }
                });
        DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                doProgramSelectionClick(dialog, which);
            }
        };
        builder.setPositiveButton("OK", listener);
        builder.setNegativeButton("Cancel", null);
        builder.create().show();
    }

    private void doProgramSelectionClick(DialogInterface dialog, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {
            boolean none = true;
            for (int i = 0; i < selectedProgramIndexes.length; i++)
                if (selectedProgramIndexes[i]) {
                    none = false;
                    break;
                }
            if (!none) {
                FileSelectionDialog fsd = new FileSelectionDialog(this, new String[] { "raw", "*" }, true);
                fsd.setPath(topStorageDir());
                fsd.setOkListener(new FileSelectionDialog.OkListener() {
                    public void okPressed(String path) {
                        doExport2(path);
                    }
                });
                fsd.show();
            }
        }
        dialog.dismiss();
    }

    private void doExport2(String path) {
        try {
            programsOutputStream = new FileOutputStream(path);
        } catch (IOException e) {
            alert("Export failed: " + e.getMessage());
            return;
        }

        int n = 0;
        for (int i = 0; i < selectedProgramIndexes.length; i++)
            if (selectedProgramIndexes[i])
                n++;
        int[] selection = new int[n];
        n = 0;
        for (int i = 0; i < selectedProgramIndexes.length; i++)
            if (selectedProgramIndexes[i])
                selection[n++] = i;
        core_export_programs(selection);

        if (programsOutputStream != null) {
            try {
                programsOutputStream.close();
            } catch (IOException e) {
            }
            programsOutputStream = null;
        }
    }

    private void doSelectSkin(String skinName) {
        try {
            boolean[] annunciators = skin.getAnnunciators();
            skin = new SkinLayout(skinName, skinSmoothing[orientation], displaySmoothing[orientation],
                    annunciators);
            if (skinName.startsWith("/")) {
                externalSkinName[orientation] = skinName;
                this.skinName[orientation] = "";
            } else
                this.skinName[orientation] = skinName;
            calcView.invalidate();
            core_repaint_display();
        } catch (IllegalArgumentException e) {
            shell_beeper(1835, 125);
        }
    }

    private void doPreferences() {
        if (preferencesDialog == null) {
            preferencesDialog = new PreferencesDialog(this);
            preferencesDialog.setOkListener(new PreferencesDialog.OkListener() {
                public void okPressed() {
                    doPreferencesOk();
                }
            });
        }

        CoreSettings cs = new CoreSettings();
        getCoreSettings(cs);
        preferencesDialog.setSingularMatrixError(cs.matrix_singularmatrix);
        preferencesDialog.setMatrixOutOfRange(cs.matrix_outofrange);
        preferencesDialog.setAutoRepeat(cs.auto_repeat);
        preferencesDialog.setAlwaysOn(shell_always_on(-1) != 0);
        preferencesDialog.setKeyClicks(keyClicksEnabled);
        preferencesDialog.setKeyVibration(keyVibrationEnabled);
        preferencesDialog.setOrientation(preferredOrientation);
        preferencesDialog.setStyle(style);
        preferencesDialog.setDisplayFullRepaint(alwaysRepaintFullDisplay);
        preferencesDialog.setSkinSmoothing(skinSmoothing[orientation]);
        preferencesDialog.setDisplaySmoothing(displaySmoothing[orientation]);
        preferencesDialog.setPrintToText(ShellSpool.printToTxt);
        preferencesDialog.setPrintToTextFileName(ShellSpool.printToTxtFileName);
        preferencesDialog.setPrintToGif(ShellSpool.printToGif);
        preferencesDialog.setPrintToGifFileName(ShellSpool.printToGifFileName);
        preferencesDialog.setMaxGifHeight(ShellSpool.maxGifHeight);
        preferencesDialog.show();
    }

    private void doPreferencesOk() {
        CoreSettings cs = new CoreSettings();
        getCoreSettings(cs);
        cs.matrix_singularmatrix = preferencesDialog.getSingularMatrixError();
        cs.matrix_outofrange = preferencesDialog.getMatrixOutOfRange();
        cs.auto_repeat = preferencesDialog.getAutoRepeat();
        shell_always_on(preferencesDialog.getAlwaysOn() ? 1 : 0);
        keyClicksEnabled = preferencesDialog.getKeyClicks();
        keyVibrationEnabled = preferencesDialog.getKeyVibration();
        int oldOrientation = preferredOrientation;
        preferredOrientation = preferencesDialog.getOrientation();
        style = preferencesDialog.getStyle();
        alwaysRepaintFullDisplay = preferencesDialog.getDisplayFullRepaint();
        putCoreSettings(cs);
        setAlwaysRepaintFullDisplay(alwaysRepaintFullDisplay);

        ShellSpool.maxGifHeight = preferencesDialog.getMaxGifHeight();

        boolean newPrintEnabled = preferencesDialog.getPrintToText();
        String newFileName = preferencesDialog.getPrintToTextFileName();
        if (printTxtStream != null && (!newPrintEnabled || !newFileName.equals(ShellSpool.printToTxtFileName))) {
            try {
                printTxtStream.close();
            } catch (IOException e) {
            }
            printTxtStream = null;
        }
        ShellSpool.printToTxt = newPrintEnabled;
        ShellSpool.printToTxtFileName = newFileName;

        newPrintEnabled = preferencesDialog.getPrintToGif();
        newFileName = preferencesDialog.getPrintToGifFileName();
        if (printGifFile != null && (!newPrintEnabled || !newFileName.equals(ShellSpool.printToGifFileName))) {
            try {
                ShellSpool.shell_finish_gif(printGifFile);
            } catch (IOException e) {
            }
            try {
                printGifFile.close();
            } catch (IOException e) {
            }
            printGifFile = null;
            gif_seq = 0;
        }
        ShellSpool.printToGif = newPrintEnabled;
        ShellSpool.printToGifFileName = newFileName;

        boolean newSkinSmoothing = preferencesDialog.getSkinSmoothing();
        boolean newDisplaySmoothing = preferencesDialog.getDisplaySmoothing();
        if (newSkinSmoothing != skinSmoothing[orientation]
                || newDisplaySmoothing != displaySmoothing[orientation]) {
            skinSmoothing[orientation] = newSkinSmoothing;
            displaySmoothing[orientation] = newDisplaySmoothing;
            skin.setSmoothing(newSkinSmoothing, newDisplaySmoothing);
            calcView.invalidate();
        }

        if (preferredOrientation != oldOrientation)
            setRequestedOrientation(preferredOrientation);
    }

    private void doAbout() {
        new AboutDialog(this).show();
    }

    public class AboutDialog extends Dialog {
        private AboutView view;

        public AboutDialog(Context context) {
            super(context);
            view = new AboutView(context);
            setContentView(view);
            this.setTitle("About Free42");
        }

        private class AboutView extends RelativeLayout {
            public AboutView(Context context) {
                super(context);

                ImageView icon = new ImageView(context);
                icon.setId(1);
                icon.setImageResource(R.drawable.icon);
                addView(icon);

                TextView label1 = new TextView(context);
                label1.setId(2);
                String version = "";
                try {
                    version = " " + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
                } catch (NameNotFoundException e) {
                }
                label1.setText("Free42" + version);
                LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);
                lp.addRule(RelativeLayout.ALIGN_TOP, icon.getId());
                lp.addRule(RelativeLayout.RIGHT_OF, icon.getId());
                addView(label1, lp);

                TextView label2 = new TextView(context);
                label2.setId(3);
                label2.setText("(C) 2004-2019 Thomas Okken");
                lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);
                lp.addRule(RelativeLayout.ALIGN_LEFT, label1.getId());
                lp.addRule(RelativeLayout.BELOW, label1.getId());
                addView(label2, lp);

                TextView label3 = new TextView(context);
                label3.setId(4);
                SpannableString s = new SpannableString("http://thomasokken.com/free42/");
                Linkify.addLinks(s, Linkify.WEB_URLS);
                label3.setText(s);
                label3.setMovementMethod(LinkMovementMethod.getInstance());
                lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);
                lp.addRule(RelativeLayout.ALIGN_LEFT, label2.getId());
                lp.addRule(RelativeLayout.BELOW, label2.getId());
                addView(label3, lp);

                TextView label4 = new TextView(context);
                label4.setId(5);
                s = new SpannableString("http://thomasokken.com/free42/42s.pdf");
                Linkify.addLinks(s, Linkify.WEB_URLS);
                label4.setText(s);
                label4.setMovementMethod(LinkMovementMethod.getInstance());
                lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);
                lp.addRule(RelativeLayout.ALIGN_LEFT, label3.getId());
                lp.addRule(RelativeLayout.BELOW, label3.getId());
                addView(label4, lp);

                Button okB = new Button(context);
                okB.setId(6);
                okB.setText("OK");
                okB.setOnClickListener(new OnClickListener() {
                    public void onClick(View view) {
                        AboutDialog.this.hide();
                    }
                });
                lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);
                lp.addRule(RelativeLayout.BELOW, label4.getId());
                lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
                addView(okB, lp);

            }
        }
    }

    /**
     * This class is calculator view used by the Free42 Activity.
     * Note that most of the heavy lifting takes place in the
     * Activity, not here.
     */
    private class CalcView extends View {

        private int width, height;
        private boolean possibleMenuEvent = false;

        public CalcView(Context context) {
            super(context);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            width = w;
            height = h;
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.scale(((float) width) / skin.getWidth(), ((float) height) / skin.getHeight());
            skin.repaint(canvas);
        }

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent e) {
            int what = e.getAction();
            if (what != MotionEvent.ACTION_DOWN && what != MotionEvent.ACTION_UP)
                return true;

            cancelRepeaterAndTimeouts1And2();

            if (what == MotionEvent.ACTION_DOWN) {
                int x = (int) (e.getX() * skin.getWidth() / width);
                int y = (int) (e.getY() * skin.getHeight() / height);
                IntHolder skeyHolder = new IntHolder();
                IntHolder ckeyHolder = new IntHolder();
                skin.find_key(core_menu(), x, y, skeyHolder, ckeyHolder);
                int skey = skeyHolder.value;
                ckey = ckeyHolder.value;
                if (ckey == 0) {
                    if (skin.in_menu_area(x, y))
                        this.possibleMenuEvent = true;
                    return true;
                }
                click();
                end_core_keydown();
                byte[] macro = skin.find_macro(ckey);
                if (timeout3_active && (macro != null || ckey != 28 /* SHIFT */)) {
                    cancelTimeout3();
                    core_timeout3(0);
                }
                Rect inval = skin.set_active_key(skey);
                if (inval != null)
                    invalidateScaled(inval);
                boolean running;
                BooleanHolder enqueued = new BooleanHolder();
                IntHolder repeat = new IntHolder();
                if (macro == null) {
                    // Plain ol' key
                    running = core_keydown(ckey, enqueued, repeat, true);
                } else {
                    boolean one_key_macro = macro.length == 1 || (macro.length == 2 && macro[0] == 28);
                    if (!one_key_macro)
                        skin.set_display_enabled(false);
                    for (int i = 0; i < macro.length - 1; i++) {
                        core_keydown(macro[i] & 255, enqueued, repeat, true);
                        if (!enqueued.value)
                            core_keyup();
                    }
                    running = core_keydown(macro[macro.length - 1] & 255, enqueued, repeat, true);
                    if (!one_key_macro)
                        skin.set_display_enabled(true);
                }
                if (running)
                    start_core_keydown();
                else {
                    if (repeat.value != 0)
                        mainHandler.postDelayed(repeaterCaller, repeat.value == 1 ? 1000 : 500);
                    else if (!enqueued.value)
                        mainHandler.postDelayed(timeout1Caller, 250);
                }
            } else {
                if (possibleMenuEvent) {
                    possibleMenuEvent = false;
                    int x = (int) (e.getX() * skin.getWidth() / width);
                    int y = (int) (e.getY() * skin.getHeight() / height);
                    if (skin.in_menu_area(x, y))
                        Free42Activity.this.postMainMenu();
                }
                ckey = 0;
                Rect inval = skin.set_active_key(-1);
                if (inval != null)
                    invalidateScaled(inval);
                end_core_keydown();
                coreWantsCpu = core_keyup();
                if (coreWantsCpu)
                    start_core_keydown();
            }

            return true;
        }

        public void postInvalidateScaled(int left, int top, int right, int bottom) {
            left = (int) Math.floor(((double) left) * width / skin.getWidth());
            top = (int) Math.floor(((double) top) * height / skin.getHeight());
            right = (int) Math.ceil(((double) right) * width / skin.getWidth());
            bottom = (int) Math.ceil(((double) bottom) * height / skin.getHeight());
            postInvalidate(left - 1, top - 1, right + 2, bottom + 2);
        }

        private void invalidateScaled(Rect inval) {
            inval.left = (int) Math.floor(((double) inval.left) * width / skin.getWidth());
            inval.top = (int) Math.floor(((double) inval.top) * height / skin.getHeight());
            inval.right = (int) Math.ceil(((double) inval.right) * width / skin.getWidth());
            inval.bottom = (int) Math.ceil(((double) inval.bottom) * height / skin.getHeight());
            inval.inset(-1, -1);
            invalidate(inval);
        }
    }

    /**
     * This class is the print-out view used by the Free42 Activity.
     * Note that most of the heavy lifting takes place in the
     * Activity, not here.
     */
    private class PrintView extends View {

        private static final int BYTESPERLINE = 18;
        // Certain devices have trouble with LINES = 16384; the print-out view collapses.
        // No idea how to detect this behavior, so unclear how to work around it.
        // Playing safe by making the print-out buffer smaller.
        // private static final int LINES = 16384;
        private static final int LINES = 8192;

        private byte[] buffer = new byte[LINES * BYTESPERLINE];
        private int top, bottom;
        private int printHeight;
        private int scale;

        public PrintView(Context context) {
            super(context);
            InputStream printInputStream = null;
            try {
                printInputStream = openFileInput("print");
                byte[] intBuf = new byte[4];
                if (printInputStream.read(intBuf) != 4)
                    throw new IOException();
                int len = (intBuf[0] << 24) | ((intBuf[1] & 255) << 16) | ((intBuf[2] & 255) << 8)
                        | (intBuf[3] & 255);
                if (len > buffer.length) {
                    int skip = len - buffer.length;
                    if (skip % BYTESPERLINE != 0)
                        skip = ((skip / BYTESPERLINE) + 1) * BYTESPERLINE;
                    printInputStream.skip(skip);
                    len -= skip;
                }
                int n = printInputStream.read(buffer, 0, len);
                if (n != len)
                    throw new IOException();
                top = 0;
                bottom = len;
            } catch (IOException e) {
                top = bottom = 0;
            } finally {
                if (printInputStream != null)
                    try {
                        printInputStream.close();
                    } catch (IOException e2) {
                    }
            }

            printHeight = bottom / BYTESPERLINE;
            int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
            scale = screenWidth / 143;
            if (scale == 0)
                scale = 1;
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // Pretending our height is never zero, to keep the HTC Aria
            // from throwing a fit. See also the printHeight == 0 case in
            // onDraw().
            setMeasuredDimension(143 * scale, Math.max(printHeight, 1) * scale);
        }

        @SuppressLint("DrawAllocation")
        @Override
        protected void onDraw(Canvas canvas) {
            Rect clip = canvas.getClipBounds();

            if (printHeight == 0) {
                // onMeasure() pretends that our height isn't really zero
                // even if printHeight == 0; this is to prevent the HTC Aria
                // from freaking out. Because of this pretense, we now have
                // to paint something, even though there isn't anything to
                // paint... So we just paint the clip rectangle using the
                // scroll view's background color.
                Paint p = new Paint();
                p.setColor(PRINT_BACKGROUND_COLOR);
                p.setStyle(Paint.Style.FILL);
                canvas.drawRect(clip, p);
                return;
            }

            // Extend the clip rectangle so that it doesn't include any
            // fractional pixels
            clip.left = clip.left / scale * scale;
            clip.top = clip.top / scale * scale;
            clip.right = (clip.right + scale - 1) / scale * scale;
            clip.bottom = (clip.bottom + scale - 1) / scale * scale;

            // Construct a temporary bitmap
            int src_x = clip.left / scale;
            int src_y = clip.top / scale;
            int src_width = (clip.right - clip.left) / scale;
            int src_height = (clip.bottom - clip.top) / scale;
            Bitmap tmpBitmap = Bitmap.createBitmap(src_width, src_height, Bitmap.Config.ARGB_8888);
            IntBuffer tmpBuffer = IntBuffer.allocate(src_width * src_height);
            int[] tmpArray = tmpBuffer.array();
            for (int y = 0; y < src_height; y++) {
                int yy = y + src_y + (top / BYTESPERLINE);
                if (yy >= LINES)
                    yy -= LINES;
                for (int x = 0; x < src_width; x++) {
                    int xx = x + src_x;
                    boolean set = (buffer[yy * BYTESPERLINE + (xx >> 3)] & (1 << (xx & 7))) != 0;
                    tmpArray[y * src_width + x] = set ? Color.BLACK : Color.WHITE;
                }
            }
            tmpBitmap.copyPixelsFromBuffer(tmpBuffer);
            canvas.drawBitmap(tmpBitmap, new Rect(0, 0, src_width, src_height), clip, new Paint());
        }

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent e) {
            return true;
        }

        @Override
        public void onSizeChanged(int w, int h, int oldw, int oldh) {
            printScrollView.fullScroll(View.FOCUS_DOWN);
        }

        private Object invalidatePendingMonitor = new Object();
        private boolean invalidatePending;
        private Object layoutPendingMonitor = new Object();
        private boolean layoutPending;

        public void print(byte[] bits, int bytesperline, int x, int y, int width, int height) {
            int oldPrintHeight = printHeight;
            for (int yy = y; yy < y + height; yy++) {
                for (int xx = 0; xx < BYTESPERLINE; xx++)
                    buffer[bottom + xx] = 0;
                for (int xx = x; xx < x + width; xx++) {
                    boolean set = (bits[yy * bytesperline + (xx >> 3)] & (1 << (xx & 7))) != 0;
                    if (set)
                        buffer[bottom + (xx >> 3)] |= 1 << (xx & 7);
                }
                bottom += BYTESPERLINE;
                printHeight++;
                if (bottom >= buffer.length)
                    bottom = 0;
                if (bottom == top) {
                    top += BYTESPERLINE;
                    printHeight--;
                    if (top >= buffer.length)
                        top = 0;
                }
            }
            if (printHeight != oldPrintHeight) {
                synchronized (layoutPendingMonitor) {
                    if (!layoutPending) {
                        mainHandler.post(new Runnable() {
                            public void run() {
                                synchronized (layoutPendingMonitor) {
                                    printView.requestLayout();
                                    layoutPending = false;
                                }
                            }
                        });
                        layoutPending = true;
                    }
                }
            } else {
                synchronized (invalidatePendingMonitor) {
                    if (!invalidatePending) {
                        mainHandler.post(new Runnable() {
                            public void run() {
                                synchronized (invalidatePendingMonitor) {
                                    printScrollView.fullScroll(View.FOCUS_DOWN);
                                    printView.postInvalidate();
                                    invalidatePending = false;
                                }
                            }
                        });
                        invalidatePending = true;
                    }
                }
            }
        }

        public void clear() {
            top = bottom = 0;
            printHeight = 0;
            printView.requestLayout();
        }

        public void dump() {
            OutputStream printOutputStream = null;
            try {
                printOutputStream = openFileOutput("print", Context.MODE_PRIVATE);
                int len = bottom - top;
                if (len < 0)
                    len += buffer.length;
                byte[] intBuf = new byte[4];
                intBuf[0] = (byte) (len >> 24);
                intBuf[1] = (byte) (len >> 16);
                intBuf[2] = (byte) (len >> 8);
                intBuf[3] = (byte) len;
                printOutputStream.write(intBuf);
                if (top <= bottom)
                    printOutputStream.write(buffer, top, bottom - top);
                else {
                    printOutputStream.write(buffer, top, buffer.length - top);
                    printOutputStream.write(buffer, 0, bottom);
                }
            } catch (IOException e) {
                // Ignore
            } finally {
                if (printOutputStream != null)
                    try {
                        printOutputStream.close();
                    } catch (IOException e2) {
                    }
            }
        }
    }

    ////////////////////////////////////////////////////////////////////
    ///// This section is where all the real 'shell' work is done. /////
    ////////////////////////////////////////////////////////////////////

    private boolean read_shell_state(IntHolder version) {
        try {
            if (state_read_int() != FREE42_MAGIC())
                return false;
            version.value = state_read_int();
            if (version.value < 0 || version.value > FREE42_VERSION())
                return false;
            int shell_version = state_read_int();
            ShellSpool.printToGif = state_read_boolean();
            ShellSpool.printToGifFileName = state_read_string();
            ShellSpool.printToTxt = state_read_boolean();
            ShellSpool.printToTxtFileName = state_read_string();
            if (shell_version >= 1)
                ShellSpool.maxGifHeight = state_read_int();
            if (shell_version >= 2)
                skinName[0] = state_read_string();
            if (shell_version >= 3)
                externalSkinName[0] = state_read_string();
            if (shell_version >= 4) {
                skinName[1] = state_read_string();
                externalSkinName[1] = state_read_string();
                keyClicksEnabled = state_read_boolean();
            } else {
                skinName[1] = skinName[0];
                externalSkinName[1] = externalSkinName[0];
                keyClicksEnabled = true;
            }
            if (shell_version >= 5)
                preferredOrientation = state_read_int();
            else
                preferredOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
            if (shell_version >= 6) {
                skinSmoothing[0] = state_read_boolean();
                displaySmoothing[0] = state_read_boolean();
            }
            if (shell_version >= 7) {
                skinSmoothing[1] = state_read_boolean();
                displaySmoothing[1] = state_read_boolean();
            }
            if (shell_version >= 8)
                keyVibrationEnabled = state_read_boolean();
            if (shell_version >= 9) {
                style = state_read_int();
                int maxStyle = PreferencesDialog.immersiveModeSupported ? 2 : 1;
                if (style > maxStyle)
                    style = maxStyle;
            } else
                style = 0;
            if (shell_version >= 10)
                alwaysRepaintFullDisplay = state_read_boolean();
            if (shell_version >= 11)
                alwaysOn = state_read_boolean();
            init_shell_state(shell_version);
        } catch (IllegalArgumentException e) {
            return false;
        }
        return true;
    }

    private void init_shell_state(int shell_version) {
        switch (shell_version) {
        case -1:
            ShellSpool.printToGif = false;
            ShellSpool.printToGifFileName = "";
            ShellSpool.printToTxt = false;
            ShellSpool.printToTxtFileName = "";
            // fall through
        case 0:
            ShellSpool.maxGifHeight = 256;
            // fall through
        case 1:
            skinName[0] = "Standard";
            // fall through
        case 2:
            externalSkinName[0] = topStorageDir() + "/Free42/" + skinName[0];
            // fall through
        case 3:
            skinName[1] = "Landscape";
            externalSkinName[1] = topStorageDir() + "/Free42/" + skinName[1];
            keyClicksEnabled = true;
            // fall through
        case 4:
            preferredOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
            // fall through
        case 5:
            skinSmoothing[0] = true;
            displaySmoothing[0] = false;
            // fall through
        case 6:
            skinSmoothing[1] = skinSmoothing[0];
            displaySmoothing[1] = displaySmoothing[0];
            // fall through
        case 7:
            keyVibrationEnabled = false;
            // fall through
        case 8:
            style = 0;
            // fall through
        case 9:
            alwaysRepaintFullDisplay = false;
            // fall through
        case 10:
            alwaysOn = false;
            // fall through
        case 11:
            // current version (SHELL_VERSION = 11),
            // so nothing to do here since everything
            // was initialized from the state file.
            ;
        }
    }

    private void write_shell_state() {
        try {
            state_write_int(FREE42_MAGIC());
            state_write_int(FREE42_VERSION());
            state_write_int(SHELL_VERSION);
            state_write_boolean(ShellSpool.printToGif);
            state_write_string(ShellSpool.printToGifFileName);
            state_write_boolean(ShellSpool.printToTxt);
            state_write_string(ShellSpool.printToTxtFileName);
            state_write_int(ShellSpool.maxGifHeight);
            state_write_string(skinName[0]);
            state_write_string(externalSkinName[0]);
            state_write_string(skinName[1]);
            state_write_string(externalSkinName[1]);
            state_write_boolean(keyClicksEnabled);
            state_write_int(preferredOrientation);
            state_write_boolean(skinSmoothing[0]);
            state_write_boolean(displaySmoothing[0]);
            state_write_boolean(skinSmoothing[1]);
            state_write_boolean(displaySmoothing[1]);
            state_write_boolean(keyVibrationEnabled);
            state_write_int(style);
            state_write_boolean(alwaysRepaintFullDisplay);
            state_write_boolean(alwaysOn);
        } catch (IllegalArgumentException e) {
        }
    }

    private byte[] int_buf = new byte[4];

    private int state_read_int() throws IllegalArgumentException {
        if (shell_read_saved_state(int_buf) != 4)
            throw new IllegalArgumentException();
        return (int_buf[0] << 24) | ((int_buf[1] & 255) << 16) | ((int_buf[2] & 255) << 8) | (int_buf[3] & 255);
    }

    private void state_write_int(int i) throws IllegalArgumentException {
        int_buf[0] = (byte) (i >> 24);
        int_buf[1] = (byte) (i >> 16);
        int_buf[2] = (byte) (i >> 8);
        int_buf[3] = (byte) i;
        if (!shell_write_saved_state(int_buf))
            throw new IllegalArgumentException();
    }

    private byte[] boolean_buf = new byte[1];

    private boolean state_read_boolean() throws IllegalArgumentException {
        if (shell_read_saved_state(boolean_buf) != 1)
            throw new IllegalArgumentException();
        return boolean_buf[0] != 0;
    }

    private void state_write_boolean(boolean b) throws IllegalArgumentException {
        boolean_buf[0] = (byte) (b ? 1 : 0);
        if (!shell_write_saved_state(boolean_buf))
            throw new IllegalArgumentException();
    }

    private String state_read_string() throws IllegalArgumentException {
        int length = state_read_int();
        byte[] buf = new byte[length];
        if (length > 0 && shell_read_saved_state(buf) != length)
            throw new IllegalArgumentException();
        try {
            return new String(buf, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Won't happen; UTF-8 is always supported.
            return null;
        }
    }

    private void state_write_string(String s) throws IllegalArgumentException {
        byte[] buf;
        try {
            buf = s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Won't happen; UTF-8 is always supported.
            throw new IllegalArgumentException();
        }
        state_write_int(buf.length);
        shell_write_saved_state(buf);
    }

    private class CoreThread extends Thread {
        public boolean coreWantsCpu;

        public void run() {
            BooleanHolder enqueued = new BooleanHolder();
            IntHolder repeat = new IntHolder();
            coreWantsCpu = core_keydown(0, enqueued, repeat, false);
        }
    }

    private void start_core_keydown() {
        coreThread = new CoreThread();
        coreThread.start();
    }

    private void end_core_keydown() {
        if (coreThread != null) {
            core_keydown_finish();
            try {
                coreThread.join();
            } catch (InterruptedException e) {
            }
            coreWantsCpu = coreThread.coreWantsCpu;
            coreThread = null;
        } else {
            coreWantsCpu = false;
        }
    }

    private void repeater() {
        cancelRepeaterAndTimeouts1And2();
        if (ckey == 0)
            return;
        int repeat = core_repeat();
        if (repeat != 0)
            mainHandler.postDelayed(repeaterCaller, repeat == 1 ? 200 : 100);
        else
            mainHandler.postDelayed(timeout1Caller, 250);
    }

    private void timeout1() {
        cancelRepeaterAndTimeouts1And2();
        if (ckey != 0) {
            core_keytimeout1();
            mainHandler.postDelayed(timeout2Caller, 1750);
        }
    }

    private void timeout2() {
        cancelRepeaterAndTimeouts1And2();
        if (ckey != 0)
            core_keytimeout2();
    }

    private void timeout3() {
        cancelTimeout3();
        end_core_keydown();
        core_timeout3(1);
        // Resume program after PSE
        BooleanHolder enqueued = new BooleanHolder();
        IntHolder repeat = new IntHolder();
        boolean running = core_keydown(0, enqueued, repeat, true);
        if (running)
            start_core_keydown();
    }

    private void click() {
        if (keyClicksEnabled)
            playSound(11, 0);
        if (keyVibrationEnabled) {
            Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            v.vibrate(50);
        }
    }

    private void playSound(int index, int duration) {
        soundPool.play(soundIds[index], 1f, 1f, 0, 0, 1f);
    }

    private static String topStorageDir() {
        return Environment.getExternalStorageDirectory().getAbsolutePath();
    }

    //////////////////////////////////////////////////////////////////////////
    ///// Stubs for accessing the FREE42_MAGIC and FREE42_VERSION macros /////
    //////////////////////////////////////////////////////////////////////////

    private native int FREE42_MAGIC();

    private native int FREE42_VERSION();

    ///////////////////////////////////////////
    ///// Stubs for shell->core interface /////
    ///////////////////////////////////////////

    private native void nativeInit();

    private native void core_keydown_finish();

    private native void core_init(int read_state, int version);

    private native void core_enter_background();

    private native void core_quit();

    private native void core_repaint_display();

    private native boolean core_menu();

    //private native boolean core_alpha_menu();
    //private native boolean core_hex_menu();
    private native boolean core_keydown(int key, BooleanHolder enqueued, IntHolder repeat,
            boolean immediate_return);

    private native int core_repeat();

    private native void core_keytimeout1();

    private native void core_keytimeout2();

    private native boolean core_timeout3(int repaint);

    private native boolean core_keyup();

    private native boolean core_powercycle();

    private native String[] core_list_programs();

    //private native int core_program_size(int prgm_index);
    private native void core_export_programs(int[] indexes);

    private native void core_import_programs();

    private native String core_copy();

    private native void core_paste(String s);

    private native void getCoreSettings(CoreSettings settings);

    private native void putCoreSettings(CoreSettings settings);

    private native void redisplay();

    private native void setAlwaysRepaintFullDisplay(boolean alwaysRepaint);

    private static class CoreSettings {
        public boolean matrix_singularmatrix;
        public boolean matrix_outofrange;
        public boolean auto_repeat;
        @SuppressWarnings("unused")
        public boolean enable_ext_accel;
        @SuppressWarnings("unused")
        public boolean enable_ext_locat;
        @SuppressWarnings("unused")
        public boolean enable_ext_heading;
        @SuppressWarnings("unused")
        public boolean enable_ext_time;
        @SuppressWarnings("unused")
        public boolean enable_ext_fptest;
    }

    ///////////////////////////////////////////////////
    ///// Implementation of core->shell interface /////
    ///////////////////////////////////////////////////

    /**
     * shell_blitter()
     *
     * Callback invoked by the emulator core to cause the display, or some portion
     * of it, to be repainted.
     *
     * 'bits' is a pointer to a 1 bpp (monochrome) bitmap. The bits within a byte
     * are laid out with left corresponding to least significant, right
     * corresponding to most significant; this corresponds to the convention for
     * X11 images, but it is the reverse of the convention for MacOS and its
     * derivatives (Microsoft Windows and PalmOS).
     * The bytes are laid out sequentially, that is, bits[0] is at the top
     * left corner, bits[1] is to the right of bits[0], bits[2] is to the right of
     * bits[1], and so on; this corresponds to X11, MacOS, Windows, and PalmOS
     * usage.
     * 'bytesperline' is the number of bytes per line of the bitmap; this means
     * that the bits just below bits[0] are at bits[bytesperline].
     * 'x', 'y', 'width', and 'height' define the part of the bitmap that needs to
     * be repainted. 'x' and 'y' are 0-based coordinates, with (0, 0) being the top
     * left corner of the bitmap, and x coordinates increasing to the right, and y
     * coordinates increasing downwards. 'width' and 'height' are the width and
     * height of the area to be repainted.
     */
    public void shell_blitter(byte[] bits, int bytesperline, int x, int y, int width, int height) {
        Rect inval = skin.display_blitter(bits, bytesperline, x, y, width, height);
        calcView.postInvalidateScaled(inval.left, inval.top, inval.right, inval.bottom);
    }

    /**
     * shell_beeper()
     * Callback invoked by the emulator core to play a sound.
     * The first parameter is the frequency in Hz; the second is the
     * duration in ms. The sound volume is up to the GUI to control.
     * Sound playback should be synchronous (the beeper function should
     * not return until the sound has finished), if possible.
     */
    public void shell_beeper(int frequency, int duration) {
        int sound_number = 10;
        for (int i = 0; i < 10; i++) {
            if (frequency <= cutoff_freqs[i]) {
                sound_number = i;
                break;
            }
        }
        playSound(sound_number, sound_number == 10 ? 125 : 250);
        try {
            Thread.sleep(sound_number == 10 ? 125 : 250);
        } catch (InterruptedException e) {
        }
    }

    private final int[] cutoff_freqs = { 164, 220, 243, 275, 293, 324, 366, 418, 438, 550 };

    private PrintAnnunciatorTurnerOffer pato = null;

    private class PrintAnnunciatorTurnerOffer extends Thread {
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            } finally {
                if (pato == this)
                    pato = null;
                else
                    return;
            }
            // Don't invalidate if the skin is null, which could happen
            // if we're in the process of switching between portrait and
            // landscape modes.
            SkinLayout currentSkin = skin;
            if (currentSkin != null) {
                Rect inval = currentSkin.update_annunciators(-1, -1, 0, -1, -1, -1, -1);
                if (inval != null)
                    calcView.postInvalidateScaled(inval.left, inval.top, inval.right, inval.bottom);
            }
        }
    }

    /**
     * shell_annunciators()
     * Callback invoked by the emulator core to change the state of the display
     * annunciators (up/down, shift, print, run, battery, (g)rad).
     * Every parameter can have values 0 (turn off), 1 (turn on), or -1 (leave
     * unchanged).
     * The battery annunciator is missing from the list; this is the only one of
     * the lot that the emulator core does not actually have any control over, and
     * so the shell is expected to handle that one by itself.
     */
    public void shell_annunciators(int updn, int shf, int prt, int run, int g, int rad) {
        boolean prt_off = false;
        if (prt != -1) {
            PrintAnnunciatorTurnerOffer p = pato;
            pato = null;
            if (p != null)
                p.interrupt();
            if (prt == 0) {
                prt = -1;
                prt_off = true;
            }
        }
        Rect inval = skin.update_annunciators(updn, shf, prt, run, -1, g, rad);
        if (inval != null)
            calcView.postInvalidateScaled(inval.left, inval.top, inval.right, inval.bottom);
        if (prt_off) {
            pato = new PrintAnnunciatorTurnerOffer();
            pato.start();
        }
    }

    /**
     * Callback to ask the shell to call core_timeout3() after the given number of
     * milliseconds. If there are keystroke events during that time, the timeout is
     * cancelled. (Pressing 'shift' does not cancel the timeout.)
     * This function supports the delay after SHOW, MEM, and shift-VARMENU.
     */
    public void shell_request_timeout3(int delay) {
        cancelTimeout3();
        mainHandler.postDelayed(timeout3Caller, delay);
        timeout3_active = true;
    }

    /**
     * shell_read_saved_state()
     *
     * Callback to read from the saved state. The function will read up to n
     * bytes into the buffer pointed to by buf, and return the number of bytes
     * actually read. The function returns -1 if an error was encountered; a return
     * value of 0 signifies the end of input.
     * The emulator core should only call this function from core_init(), and only
     * if core_init() was called with an argument of 1. (Nothing horrible will
     * happen if you try to call this function during other contexts, but you will
     * always get an error then.)
     */
    public int shell_read_saved_state(byte[] buf) {
        if (stateFileInputStream == null)
            return -1;
        try {
            int n = stateFileInputStream.read(buf);
            if (n <= 0) {
                stateFileInputStream.close();
                stateFileInputStream = null;
                return 0;
            } else
                return n;
        } catch (IOException e) {
            try {
                stateFileInputStream.close();
            } catch (IOException e2) {
            }
            stateFileInputStream = null;
            return -1;
        }
    }

    /**
     * shell_write_saved_state()
     * Callback to dump the saved state to persistent storage.
     * Returns 'true' on success, 'false' on error.
     * The emulator core should only call this function from core_quit(). (Nothing
     * horrible will happen if you try to call this function during other contexts,
     * but you will always get an error then.)
     */
    public boolean shell_write_saved_state(byte[] buf) {
        if (stateFileOutputStream == null)
            return false;
        try {
            stateFileOutputStream.write(buf);
            return true;
        } catch (IOException e) {
            try {
                stateFileOutputStream.close();
            } catch (IOException e2) {
            }
            stateFileOutputStream = null;
            return false;
        }
    }

    /**
     * shell_get_mem()
     * Callback to get the amount of free memory in bytes.
     */
    public int shell_get_mem() {
        long freeMem = Runtime.getRuntime().freeMemory();
        return freeMem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) freeMem;
    }

    /**
     * shell_low_battery()
     * Callback to find out if the battery is low. Used to emulate flag 49 and the
     * battery annunciator.
     */
    public int shell_low_battery() {
        return low_battery ? 1 : 0;
    }

    /**
     * shell_powerdown()
     * Callback to tell the shell that the emulator wants to power down.
     * Only called in response to OFF (shift-EXIT or the OFF command); automatic
     * power-off is left to the OS and/or shell.
     */
    public void shell_powerdown() {
        finish();
    }

    /**
     * shell_always_on()
     * Callback for setting and querying the shell's Continuous On status.
     */
    public int shell_always_on(int ao) {
        int ret = alwaysOn ? 1 : 0;
        if (ao != -1) {
            alwaysOn = ao != 0;
            if (alwaysOn)
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            else
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
        return ret;
    }

    /**
     * shell_decimal_point()
     * Returns 0 if the host's locale uses comma as the decimal separator;
     * returns 1 if it uses dot or anything else.
     * Used to initialize flag 28 on hard reset.
     */
    public int shell_decimal_point() {
        DecimalFormat df = new DecimalFormat();
        DecimalFormatSymbols dfsym = df.getDecimalFormatSymbols();
        return dfsym.getDecimalSeparator() == ',' ? 0 : 1;
    }

    private OutputStream printTxtStream;
    private RandomAccessFile printGifFile;
    private int gif_lines;
    private int gif_seq = 0;

    /**
     * shell_print()
     * Printer emulation. The first 2 parameters are the plain text version of the
     * data to be printed; the remaining 6 parameters are the bitmap version. The
     * former is used for text-mode copying and for spooling to text files; the
     * latter is used for graphics-mode copying, spooling to image files, and
     * on-screen display.
     */
    public void shell_print(byte[] text, byte[] bits, int bytesperline, int x, int y, int width, int height) {
        printView.print(bits, bytesperline, x, y, width, height);

        if (ShellSpool.printToTxt) {
            try {
                if (printTxtStream == null)
                    if (new File(ShellSpool.printToTxtFileName).exists()) {
                        printTxtStream = new FileOutputStream(ShellSpool.printToTxtFileName, true);
                    } else {
                        printTxtStream = new FileOutputStream(ShellSpool.printToTxtFileName);
                        printTxtStream.write(new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF });
                    }
                ShellSpool.shell_spool_txt(text, printTxtStream);
            } catch (IOException e) {
                if (printTxtStream != null) {
                    try {
                        printTxtStream.close();
                    } catch (IOException e2) {
                    }
                    printTxtStream = null;
                }
                ShellSpool.printToTxt = false;
                alert("An error occurred while printing to " + ShellSpool.printToTxtFileName + ": " + e.getMessage()
                        + "\nPrinting to text file disabled.");
            }
        }

        if (ShellSpool.printToGif) {
            if (printGifFile != null && gif_lines + height > ShellSpool.maxGifHeight) {
                try {
                    ShellSpool.shell_finish_gif(printGifFile);
                } catch (IOException e) {
                }
                try {
                    printGifFile.close();
                } catch (IOException e) {
                }
                printGifFile = null;
            }

            String name = null;
            if (printGifFile == null) {
                while (true) {
                    gif_seq = (gif_seq + 1) % 10000;

                    name = ShellSpool.printToGifFileName;
                    int len = name.length();

                    /* Strip ".gif" extension, if present */
                    if (len >= 4 && name.substring(len - 4).equals(".gif")) {
                        name = name.substring(0, len - 4);
                        len -= 4;
                    }

                    /* Strip ".[0-9]+", if present */
                    while (len > 0) {
                        char c = name.charAt(len - 1);
                        if (c >= '0' && c <= '9')
                            name = name.substring(0, --len);
                        else
                            break;
                    }
                    if (len > 0 && name.charAt(len - 1) == '.')
                        name = name.substring(0, --len);

                    String seq = "000" + gif_seq;
                    seq = seq.substring(seq.length() - 4);
                    name += "." + seq + ".gif";

                    if (!new File(name).exists())
                        break;
                }
            }

            try {
                if (name != null) {
                    printGifFile = new RandomAccessFile(name, "rw");
                    gif_lines = 0;
                    ShellSpool.shell_start_gif(printGifFile, ShellSpool.maxGifHeight);
                }
                ShellSpool.shell_spool_gif(bits, bytesperline, x, y, width, height, printGifFile);
                gif_lines += height;
            } catch (IOException e) {
                if (printGifFile != null) {
                    try {
                        printGifFile.close();
                    } catch (IOException e2) {
                    }
                    printGifFile = null;
                }
                ShellSpool.printToGif = false;
                alert("An error occurred while printing to " + ShellSpool.printToGifFileName + ": " + e.getMessage()
                        + "\nPrinting to GIF file disabled.");
            }

            if (printGifFile != null && gif_lines + 9 > ShellSpool.maxGifHeight) {
                try {
                    ShellSpool.shell_finish_gif(printGifFile);
                } catch (IOException e) {
                }
                try {
                    printGifFile.close();
                } catch (IOException e) {
                }
                printGifFile = null;
            }
        }
    }

    /**
     * shell_write()
     *
     * Callback for core_export_programs(). Returns 0 if a problem occurred;
     * core_export_programs() should abort in that case.
     */
    public int shell_write(byte[] buf) {
        if (programsOutputStream == null)
            return 0;
        try {
            programsOutputStream.write(buf);
            return 1;
        } catch (IOException e) {
            try {
                programsOutputStream.close();
            } catch (IOException e2) {
            }
            programsOutputStream = null;
            return 0;
        }
    }

    /**
     * shell_read()
     *
     * Callback for core_import_programs(). Returns the number of bytes actually
     * read. Returns -1 if an error occurred; a return value of 0 signifies end of
     * input.
     */
    public int shell_read(byte[] buf) {
        if (programsInputStream == null)
            return -1;
        try {
            int n = programsInputStream.read(buf);
            if (n <= 0) {
                programsInputStream.close();
                programsInputStream = null;
                return 0;
            } else
                return n;
        } catch (IOException e) {
            try {
                programsInputStream.close();
            } catch (IOException e2) {
            }
            programsInputStream = null;
            return -1;
        }
    }

    private boolean accel_inited, accel_exists;
    private double accel_x, accel_y, accel_z;

    public int shell_get_acceleration(DoubleHolder x, DoubleHolder y, DoubleHolder z) {
        if (!accel_inited) {
            accel_inited = true;
            SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
            Sensor s = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            if (s == null)
                return 0;
            boolean success = sm.registerListener(new SensorEventListener() {
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                    // Don't care
                }

                public void onSensorChanged(SensorEvent event) {
                    // Transform the measurements to conform to the iPhone
                    // conventions. The conversion factor used here is the
                    // 'standard gravity'.
                    accel_x = event.values[0] / -9.80665;
                    accel_y = event.values[1] / -9.80665;
                    accel_z = event.values[2] / -9.80665;
                }
            }, s, SensorManager.SENSOR_DELAY_NORMAL);
            if (!success)
                return 0;
            accel_exists = true;
        }

        if (accel_exists) {
            x.value = accel_x;
            y.value = accel_y;
            z.value = accel_z;
            return 1;
        } else {
            return 0;
        }
    }

    private boolean locat_inited, locat_exists;
    private double locat_lat, locat_lon, locat_lat_lon_acc, locat_elev, locat_elev_acc;

    public int shell_get_location(DoubleHolder lat, DoubleHolder lon, DoubleHolder lat_lon_acc, DoubleHolder elev,
            DoubleHolder elev_acc) {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            locat_inited = false;
            ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.ACCESS_FINE_LOCATION },
                    MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
            return 0;
        }
        if (!locat_inited) {
            locat_inited = true;
            LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
            Criteria cr = new Criteria();
            cr.setAccuracy(Criteria.ACCURACY_FINE);
            String provider = lm.getBestProvider(cr, true);
            if (provider == null) {
                locat_exists = false;
                return 0;
            }
            LocationListener ll = new LocationListener() {
                public void onLocationChanged(Location location) {
                    // TODO: Verify units etc.
                    locat_lat = location.getLatitude();
                    locat_lon = location.getLongitude();
                    locat_lat_lon_acc = location.getAccuracy();
                    locat_elev = location.getAltitude();
                    locat_elev_acc = location.hasAltitude() ? locat_lat_lon_acc : -1;
                }

                public void onProviderDisabled(String provider) {
                    // Ignore
                }

                public void onProviderEnabled(String provider) {
                    // Ignore
                }

                public void onStatusChanged(String provider, int status, Bundle extras) {
                    // Ignore
                }
            };
            try {
                lm.requestLocationUpdates(provider, 60000, 1, ll, Looper.getMainLooper());
            } catch (IllegalArgumentException e) {
                return 0;
            } catch (SecurityException e) {
                return 0;
            }
            locat_exists = true;
        }

        if (locat_exists) {
            lat.value = locat_lat;
            lon.value = locat_lon;
            lat_lon_acc.value = locat_lat_lon_acc;
            elev.value = locat_elev;
            elev_acc.value = locat_elev_acc;
            return 1;
        } else
            return 0;
    }

    private boolean heading_inited, heading_exists;
    private double heading_mag, heading_true, heading_acc, heading_x, heading_y, heading_z;

    public int shell_get_heading(DoubleHolder mag_heading, DoubleHolder true_heading, DoubleHolder acc_heading,
            DoubleHolder x, DoubleHolder y, DoubleHolder z) {
        if (!heading_inited) {
            heading_inited = true;
            SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
            Sensor s1 = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
            Sensor s2 = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            if (s1 == null)
                return 0;
            SensorEventListener listener = new SensorEventListener() {
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                    // Don't care
                }

                public void onSensorChanged(SensorEvent event) {
                    // TODO: Verify this on a real phone, and
                    // check if the orientation matches the iPhone.
                    // There doesn't seem to be an API to obtain true
                    // heading, so I should set true_heading to 0
                    // and heading_acc to -1; the current code just
                    // exists to let me investigate the components
                    // returned by Orientation events.
                    if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
                        heading_mag = event.values[0];
                        heading_true = event.values[1];
                        heading_acc = event.values[2];
                    } else {
                        heading_x = event.values[0];
                        heading_y = event.values[1];
                        heading_z = event.values[2];
                    }
                }
            };
            boolean success = sm.registerListener(listener, s1, SensorManager.SENSOR_DELAY_UI);
            if (!success)
                return 0;
            sm.registerListener(listener, s2, SensorManager.SENSOR_DELAY_UI);
            heading_exists = true;
        }

        if (heading_exists) {
            mag_heading.value = heading_mag;
            true_heading.value = heading_true;
            acc_heading.value = heading_acc;
            x.value = heading_x;
            y.value = heading_y;
            z.value = heading_z;
            return 1;
        } else {
            return 0;
        }
    }

    public void shell_log(String s) {
        System.err.print(s);
    }

    public static boolean checkStorageAccess() {
        return instance.checkStorageAccess2();
    }

    private boolean checkStorageAccess2() {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
            if (android.os.Build.VERSION.SDK_INT >= 19 /* KitKat; 4.4 */)
                new File(MY_STORAGE_DIR).mkdirs();
            return true;
        }
        ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
                MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        return false;
    }
}