com.dirkgassen.wator.ui.activity.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.dirkgassen.wator.ui.activity.MainActivity.java

Source

/*
 * MainActivity.java is part of Wa-Tor (C) 2016 by Dirk Gassen.
 *
 * Wa-Tor 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.
 *
 * Wa-Tor 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.dirkgassen.wator.ui.activity;

import java.util.ArrayList;
import java.util.List;

import com.dirkgassen.wator.R;
import com.dirkgassen.wator.ui.fragment.NewWorld;
import com.dirkgassen.wator.ui.view.RangeSlider;
import com.dirkgassen.wator.utils.RollingAverage;
import com.dirkgassen.wator.simulator.Simulator;
import com.dirkgassen.wator.simulator.SimulatorRunnable;
import com.dirkgassen.wator.simulator.WorldHost;
import com.dirkgassen.wator.simulator.WorldObserver;
import com.dirkgassen.wator.simulator.WorldParameters;

import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

/**
 * Main activity for the app.
 */
// Adding the hamburger menu: followed this tutorial:
//     http://codetheory.in/android-navigation-drawer/
public class MainActivity extends AppCompatActivity
        implements WorldHost, SimulatorRunnable.SimulatorRunnableObserver, NewWorld.WorldCreator {

    /** Stores information about one commad in the drawer. */
    abstract class DrawerCommandItem {

        /** ID that uniquely identifies the command */
        public final int id;

        /** ID of the icon of the command. */
        public final int icon;

        /** Command title. */
        public final String title;

        /** A description of the command. */
        public final String subtitle;

        /**
         * Creates a new command
         *
         * @param id       ID for the new command
         * @param icon     ID of the icon to use
         * @param title    title for the command
         * @param subtitle description of th ecommand
         */
        DrawerCommandItem(int id, int icon, String title, String subtitle) {
            this.id = id;
            this.icon = icon;
            this.title = title;
            this.subtitle = subtitle;
        }

        abstract public void execute();
    }

    /** A {@link android.widget.ListAdapter} for our drawer commands */
    class DrawerListAdapter extends BaseAdapter {

        /** List of commands available */
        private final List<DrawerCommandItem> drawerCommands;

        /** Layout inflater to use to inflate the views for the items in the list */
        private final LayoutInflater inflater;

        /** @return number of commands in this adapter */
        @Override
        public int getCount() {
            return drawerCommands.size();
        }

        /**
         * Returns a given command in the list.
         *
         * @param position position of the command in the list
         * @return desired command
         */
        @Override
        public Object getItem(int position) {
            return drawerCommands.get(position);
        }

        /**
         * Returns the ID of the command at a certain position (if there is a command)
         *
         * @param position position of the command
         * @return ID of the command
         */
        @Override
        public long getItemId(int position) {
            DrawerCommandItem item = drawerCommands.get(position);
            if (item == null) {
                return 0;
            }
            return item.id;
        }

        /**
         * Returns the view for the given position.
         *
         * @param position    index of the row
         * @param convertView an existing view that should be recycled
         * @param parent      The parent that this view will eventually be attached to
         * @return            view for the position
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.drawer_command_item, parent, false);
            }
            DrawerCommandItem item = drawerCommands.get(position);
            ((TextView) convertView.findViewById(R.id.drawer_command_title)).setText(item.title);
            ((TextView) convertView.findViewById(R.id.drawer_command_subtitle)).setText(item.subtitle);
            ((ImageView) convertView.findViewById(R.id.drawer_command_icon)).setImageResource(item.icon);
            return convertView;
        }

        /**
         * Creates a new drawer command adapter
         *
         * @param drawerCommands list of commands for this adapter
         */
        public DrawerListAdapter(List<DrawerCommandItem> drawerCommands) {
            this.drawerCommands = drawerCommands;
            inflater = LayoutInflater.from(MainActivity.this);
        }

    }

    /** Groups the command IDs together */
    static class Commands {
        /** ID of the command to create a new world */
        private static final int NEW_WORLD_COMMAND = 1;

        /** Id of the "about" command */
        private static final int ABOUT_COMMAND = 2;
    }

    /** Groups the keys for saving world data in a Bundle */
    static class WorldKeys {
        /** Key for the fish age array */
        private static final String FISH_AGE_KEY = "fishAge";

        /** Key for the shark age array */
        private static final String SHARK_AGE_KEY = "sharkAge";

        /** Key for the fish age */
        private static final String SHARK_HUNGER_KEY = "sharkHunger";

        /** Key for the array containinng the x coordinates of the fish positions */
        private static final String FISH_POSITIONS_X_KEY = "fishPositionsX";

        /** Key for the array containing the y coordinates of the fish positions */
        private static final String FISH_POSITIONS_Y_KEY = "fishPositionsY";

        /** Key for the fish breed time */
        private static final String FISH_BREED_TIME_KEY = "fishBreedTime";

        /** Key for the shark breed time */
        private static final String SHARK_BREED_TIME_KEY = "sharkBreedTime";

        /** Key for the shark starve time */
        private static final String SHARK_STARVE_TIME_KEY = "sharkStarveTime";

        /** Key for the array containing the x coordinates of the shark positions */
        private static final String SHARK_POSITIONS_X_KEY = "sharkPositionsX";

        /** Key for the array containing the y coordinates of the shark positions */
        private static final String SHARK_POSITIONS_Y_KEY = "sharkPositionsY";

        /** Key for the width of the world */
        private static final String WORLD_WIDTH_KEY = "worldWidth";

        /** Key for the height of the world */
        private static final String WORLD_HEIGHT_KEY = "worldHeight";

        /** Key for the initial number of fish */
        private static final String INITIAL_FISH_COUNT_KEY = "initialFishCount";

        /** Key for the initial number of shark */
        private static final String INITIAL_SHARK_COUNT_KEY = "initialSharkCount";

        /** Key for the simulation FPS  */
        private static final String TARGET_FPS_KEY = "targetFps";
    }

    /** Tag for the "new world" fragment */
    private static final String NEW_WORLD_FRAGMENT_TAG = "New World";

    /**
     * Set of {@link com.dirkgassen.wator.simulator.WorldObserver} objects that are notified whenever the simulator
     * ticked.
     */
    private WorldObserver worldObservers[] = new WorldObserver[4];

    /**
     * Stores the number of observers stored in {@link #worldObservers}. Note that elements of that array can be
     * unused
     */
    private int worldObserverCount = 0;

    /**
     * A mutex on which we need to synchronize whenever we access the {@link #worldObservers} array
     * (adding, removing or iterating over them)
     */
    final private Object worldObserverMutex = new Object();

    /** Simulator object that runs the world */
    private Simulator simulator;

    /** {@link java.lang.Runnable} for the thread that ticks the world */
    private SimulatorRunnable simulatorRunnable;

    /** Thread that updates the {@link #worldObservers} */
    private Thread worldUpdateNotifierThread;

    /** Keeps track of the average drawing time */
    private RollingAverage drawingAverageTime;

    /** Stores the wall time of the next FPS update (so that we update the FPS indicators less often) */
    private long nextFpsUpdate = 0;

    /** The view that should contain the new world fragment */
    private View newWorldView;

    /** Contains the current simulation frame rate */
    private TextView currentSimFps;

    /** Displays the current frame rate of the drawing */
    private TextView currentDrawFps;

    /** Slider for the desired frame rate */
    private RangeSlider desiredFpsSlider;

    /** Slider for setting the number of threads to tick the world */
    private RangeSlider threadsSlider;

    /** The {@link DrawerLayout} of the main view */
    private DrawerLayout drawerLayout;

    /** Ties together the {@link android.support.v7.app.ActionBar} and our {@link #drawerLayout}*/
    private ActionBarDrawerToggle drawerToggle;

    /** Handler to run stuff on the UI thread */
    private Handler handler;

    /** A {@link Runnable} that updates the FPS in the drawer */
    private Runnable updateFpsRunnable;

    /** Stores the parameters of the most recently created world */
    private WorldParameters previousWorldParameters;

    /** The FPS indicators will be colored with this color when the FPS is lower than desired */
    private int fpsWarningColor;

    /** The FPS indicators will be colored with this color when the FPS is higher or equal to the desired FPS */
    private int fpsOkColor;

    /** Notifies all {@link #worldObservers} of a world change */
    private void worldUpdated() {
        synchronized (worldObserverMutex) {
            if (worldObserverCount > 0) {
                Simulator.WorldInspector world = simulator.getWorldToPaint();
                try {
                    for (int observerNo = 0; observerNo < worldObserverCount; observerNo++) {
                        worldObservers[observerNo].worldUpdated(world);
                        world.reset();
                    }
                    if (Log.isLoggable("Wa-Tor", Log.VERBOSE)) {
                        Log.v("Wa-Tor", "Fish: " + world.getFishCount() + "; sharks: " + world.getSharkCount());
                    }
                    if (world.getSharkCount() == 0) {
                        simulatorRunnable.stopTicking();
                    }
                } finally {
                    world.release();
                }
            }
        }
        // Note: not synchronizing here, but rather a rudimentary check to avoid posting if it's not
        // necessary. If the drawer opens while we are executing and one of these fields change to non-null
        // then the frame rate will be update next time. If it's the other way around the updateFpsRunnable
        // should synchronize and check again.
        if (currentSimFps != null || currentDrawFps != null && nextFpsUpdate < System.currentTimeMillis()) {
            handler.post(updateFpsRunnable);
            nextFpsUpdate = System.currentTimeMillis() + 6000L;
        }
    }

    /**
     * Creates a list with commands for our drawer.
     *
     * @return command list
     */
    private List<DrawerCommandItem> getDrawerCommands() {
        List<DrawerCommandItem> commandList = new ArrayList<DrawerCommandItem>();
        commandList.add(new DrawerCommandItem(Commands.NEW_WORLD_COMMAND, R.drawable.new_world_icon,
                getString(R.string.create_new_world_command), getString(R.string.create_new_world_description)) {
            @Override
            public void execute() {
                drawerLayout.closeDrawers();
                toggleNewWorldFragment();
            }
        });
        commandList.add(new DrawerCommandItem(Commands.ABOUT_COMMAND, R.drawable.about_icon,
                getString(R.string.about_command), getString(R.string.about_description)) {
            @Override
            public void execute() {
                showAbout();
            }
        });
        return commandList;
    }

    /** Shows an alert with fancy info about this App. */
    @SuppressLint("InflateParams")
    private void showAbout() {
        View aboutView = getLayoutInflater().inflate(R.layout.about, null /* root */, true /* attachToRoot */);
        TextView versionView = (TextView) aboutView.findViewById(R.id.version);
        PackageInfo packageInfo;
        try {
            packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            String version = getString(R.string.version, packageInfo.versionName);
            versionView.setText(version);
        } catch (PackageManager.NameNotFoundException e) {
            versionView.setText(R.string.unknown_version);
        }

        new AlertDialog.Builder(this).setView(aboutView).setCancelable(true)
                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface d, int c) {
                        d.dismiss();
                    }
                }).create().show();
    }

    /**
     * Shows the "new world" fragment if it was hidden.
     * @return {@code true} if the fragment was not visible; {@code false} otherwise
     */
    synchronized private boolean showNewWorldFragment() {
        if (newWorldView.getVisibility() == View.GONE) {
            FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction ft = fm.beginTransaction();
            Fragment newWorldFragment = new NewWorld();
            ft.add(R.id.new_world_fragment_container, newWorldFragment, NEW_WORLD_FRAGMENT_TAG);
            ft.commit();
            fm.executePendingTransactions();
            newWorldView.startAnimation(AnimationUtils.loadAnimation(this, R.anim.slide_in_down));
            newWorldView.setVisibility(View.VISIBLE);
            return true;
        }
        return false;
    }

    /**
     * Hides the "new world" fragment if it was showing.
     * @return {@code true} if the fragment was visible; {@code false} otherwise
     */
    synchronized private boolean hideNewWorldFragment() {
        if (newWorldView.getVisibility() != View.GONE) {
            Animation animation = AnimationUtils.loadAnimation(this, R.anim.slide_out_up);
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    // Nothing to do here
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    FragmentManager fm = getSupportFragmentManager();
                    Fragment fragment = fm.findFragmentByTag(NEW_WORLD_FRAGMENT_TAG);
                    FragmentTransaction ft = fm.beginTransaction();
                    ft.remove(fragment);
                    ft.commit();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                    // Nothing to do here
                }
            });
            newWorldView.startAnimation(animation);
            newWorldView.setVisibility(View.GONE);

            View viewWithFocus = getCurrentFocus();
            if (viewWithFocus != null) {
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(viewWithFocus.getWindowToken(), 0);
            }
            return true;
        }
        return false;
    }

    /** Toggles the state of the "new world" fragment: if it is currently open then hide it, otherwise show it. */
    synchronized private void toggleNewWorldFragment() {
        if (showNewWorldFragment()) {
            return;
        }
        hideNewWorldFragment();
    }

    /**
     * Return the parameters from which the current world was created
     * @return parameters that created the current world
     */
    @Override
    public WorldParameters getPreviousWorldParameters() {
        if (previousWorldParameters == null) {
            return new WorldParameters();
        }
        return previousWorldParameters;
    }

    /**
     * Creates a new world with the given parameters. Depending on the current desired FPS the simulator thread
     * is started (or not, if the desired FPS is 0).
     * @param worldParameters parameters for the new world.
     */
    @Override
    synchronized public void createWorld(WorldParameters worldParameters) {
        int targetFps = simulatorRunnable != null ? simulatorRunnable.getTargetFps() : -1;
        previousWorldParameters = worldParameters;
        if (simulatorRunnable != null) {
            simulatorRunnable.stopTicking();
        }
        simulator = new Simulator(worldParameters);
        simulatorRunnable = new SimulatorRunnable(simulator);
        if (targetFps >= 0) {
            simulatorRunnable.setTargetFps(targetFps);
        }
        simulatorRunnable.registerSimulatorRunnableObserver(this);
        startSimulatorThread();
        hideNewWorldFragment();
    }

    /** Cancels creating a new world: simply hide the "new world" fragment. */
    @Override
    public void cancelCreateWorld() {
        hideNewWorldFragment();
    }

    /**
     * Saves the state of this instance:
     * <ul>
     *     <li>current state of the simulator: fish positions and age, shark positions and age and hunger</li>
     *     <li>world parameters</li>
     *     <li>desired fps</li>
     * </ul>
     * @param outState Bundle to save the state to
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        Simulator.WorldInspector world = simulator.getWorldToPaint();
        try {
            final int fishCount = world.getFishCount();
            final int sharkCount = world.getSharkCount();
            final short[] fishAge;
            final short[] fishPosX;
            final short[] fishPosY;
            if (fishCount == 0) {
                fishAge = fishPosX = fishPosY = null;
            } else {
                fishAge = new short[fishCount];
                fishPosX = new short[fishCount];
                fishPosY = new short[fishCount];
            }
            final short[] sharkAge;
            final short[] sharkHunger;
            final short[] sharkPosX;
            final short[] sharkPosY;
            if (sharkCount == 0) {
                sharkAge = sharkHunger = sharkPosX = sharkPosY = null;
            } else {
                sharkAge = new short[sharkCount];
                sharkHunger = new short[sharkCount];
                sharkPosX = new short[sharkCount];
                sharkPosY = new short[sharkCount];
            }

            int fishNo = 0;
            int sharkNo = 0;
            do {
                if (world.isFish()) {
                    //noinspection ConstantConditions
                    fishAge[fishNo] = world.getFishAge();
                    //noinspection ConstantConditions
                    fishPosX[fishNo] = world.getCurrentX();
                    //noinspection ConstantConditions
                    fishPosY[fishNo++] = world.getCurrentY();
                } else if (world.isShark()) {
                    //noinspection ConstantConditions
                    sharkAge[sharkNo] = world.getSharkAge();
                    //noinspection ConstantConditions
                    sharkHunger[sharkNo] = world.getSharkHunger();
                    //noinspection ConstantConditions
                    sharkPosX[sharkNo] = world.getCurrentX();
                    //noinspection ConstantConditions
                    sharkPosY[sharkNo++] = world.getCurrentY();
                }
            } while (world.moveToNext() != Simulator.WorldInspector.RESET);
            if (fishCount > 0) {
                outState.putShortArray(WorldKeys.FISH_AGE_KEY, fishAge);
                outState.putShortArray(WorldKeys.FISH_POSITIONS_X_KEY, fishPosX);
                outState.putShortArray(WorldKeys.FISH_POSITIONS_Y_KEY, fishPosY);
            }
            if (sharkCount > 0) {
                outState.putShortArray(WorldKeys.SHARK_AGE_KEY, sharkAge);
                outState.putShortArray(WorldKeys.SHARK_HUNGER_KEY, sharkHunger);
                outState.putShortArray(WorldKeys.SHARK_POSITIONS_X_KEY, sharkPosX);
                outState.putShortArray(WorldKeys.SHARK_POSITIONS_Y_KEY, sharkPosY);
            }
            outState.putShort(WorldKeys.WORLD_WIDTH_KEY, world.getWorldWidth());
            outState.putShort(WorldKeys.WORLD_HEIGHT_KEY, world.getWorldHeight());
            outState.putShort(WorldKeys.FISH_BREED_TIME_KEY, world.getFishBreedTime());
            outState.putShort(WorldKeys.SHARK_BREED_TIME_KEY, world.getSharkBreedTime());
            outState.putShort(WorldKeys.SHARK_STARVE_TIME_KEY, world.getSharkStarveTime());

            if (previousWorldParameters == null) {
                previousWorldParameters = new WorldParameters();
            }
            outState.putInt(WorldKeys.INITIAL_FISH_COUNT_KEY, previousWorldParameters.getInitialFishCount());
            outState.putInt(WorldKeys.INITIAL_SHARK_COUNT_KEY, previousWorldParameters.getInitialSharkCount());

            if (simulatorRunnable != null) {
                outState.putInt(WorldKeys.TARGET_FPS_KEY, simulatorRunnable.getTargetFps());
            }
        } finally {
            world.release();
        }
        super.onSaveInstanceState(outState);
    }

    /**
     * Initializes this activity
     * @param savedInstanceState if this parameter is not {@code null} the activity state is restored to the information
     *                           in this Bundle
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        Toolbar myToolbar = (Toolbar) findViewById(R.id.main_toolbar);
        setSupportActionBar(myToolbar);
        // More info: http://codetheory.in/difference-between-setdisplayhomeasupenabled-sethomebuttonenabled-and-setdisplayshowhomeenabled/
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        newWorldView = findViewById(R.id.new_world_fragment_container);
        fpsOkColor = ContextCompat.getColor(this, R.color.fps_ok_color);
        fpsWarningColor = ContextCompat.getColor(this, R.color.fps_warning_color);
        desiredFpsSlider = (RangeSlider) findViewById(R.id.desired_fps);
        threadsSlider = (RangeSlider) findViewById(R.id.threads);
        desiredFpsSlider.setOnTouchListener(new View.OnTouchListener() {
            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    v.getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_UP:
                    v.getParent().requestDisallowInterceptTouchEvent(false);
                    break;
                }
                v.onTouchEvent(event);
                return true;
            }
        });
        desiredFpsSlider.setOnValueChangeListener(new RangeSlider.OnValueChangeListener() {
            @Override
            public void onValueChange(RangeSlider slider, int oldVal, int newVal, boolean fromUser) {
                synchronized (MainActivity.this) {
                    if (newVal == 0) {
                        if (currentDrawFps != null) {
                            currentDrawFps.setVisibility(View.INVISIBLE);
                        }
                    } else {
                        if (currentDrawFps != null) {
                            currentDrawFps.setVisibility(View.VISIBLE);
                        }
                    }
                    if (fromUser) {
                        if (simulatorRunnable.setTargetFps(newVal)) {
                            startSimulatorThread();
                        }
                    }
                }
            }
        });
        threadsSlider.setOnTouchListener(new View.OnTouchListener() {
            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    v.getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_UP:
                    v.getParent().requestDisallowInterceptTouchEvent(false);
                    break;
                }
                v.onTouchEvent(event);
                return true;
            }
        });
        threadsSlider.setOnValueChangeListener(new RangeSlider.OnValueChangeListener() {
            @Override
            public void onValueChange(RangeSlider slider, int oldVal, int newVal, boolean fromUser) {
                simulatorRunnable.setThreadCount(newVal);
            }
        });

        handler = new Handler();
        updateFpsRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (MainActivity.this) {
                    if (currentSimFps != null) {
                        if (simulatorRunnable.getTargetFps() == 0) {
                            currentSimFps.setText(getString(R.string.paused));
                            currentSimFps.setTextColor(fpsOkColor);
                        } else {
                            int fps = (int) simulatorRunnable.getAvgFps();
                            currentSimFps.setText(getString(R.string.current_simulation_fps, fps));
                            int newTextColor = fps < simulatorRunnable.getTargetFps() ? fpsWarningColor
                                    : fpsOkColor;
                            currentSimFps.setTextColor(newTextColor);
                        }
                    }
                    if (currentDrawFps != null) {
                        float fps = drawingAverageTime.getAverage();
                        if (fps != 0f) {
                            fps = 1000 / fps;
                        }
                        currentDrawFps.setText(getString(R.string.current_drawing_fps, (int) fps));
                        int newTextColor = fps < simulatorRunnable.getTargetFps() ? fpsWarningColor : fpsOkColor;
                        currentDrawFps.setTextColor(newTextColor);
                    }
                }
            }
        };

        if (savedInstanceState == null) {
            simulator = new Simulator(new WorldParameters());
        } else {
            WorldParameters parameters = new WorldParameters()
                    .setWidth(savedInstanceState.getShort(WorldKeys.WORLD_WIDTH_KEY))
                    .setHeight(savedInstanceState.getShort(WorldKeys.WORLD_HEIGHT_KEY))
                    .setFishBreedTime(savedInstanceState.getShort(WorldKeys.FISH_BREED_TIME_KEY))
                    .setSharkBreedTime(savedInstanceState.getShort(WorldKeys.SHARK_BREED_TIME_KEY))
                    .setSharkStarveTime(savedInstanceState.getShort(WorldKeys.SHARK_STARVE_TIME_KEY))
                    .setInitialFishCount(0).setInitialSharkCount(0);
            simulator = new Simulator(parameters);
            short[] fishAge = savedInstanceState.getShortArray(WorldKeys.FISH_AGE_KEY);
            if (fishAge != null) {
                short[] fishPosX = savedInstanceState.getShortArray(WorldKeys.FISH_POSITIONS_X_KEY);
                if (fishPosX != null) {
                    short[] fishPosY = savedInstanceState.getShortArray(WorldKeys.FISH_POSITIONS_Y_KEY);
                    if (fishPosY != null) {
                        for (int fishNo = 0; fishNo < fishAge.length; fishNo++) {
                            simulator.setFish(fishPosX[fishNo], fishPosY[fishNo], fishAge[fishNo]);
                        }
                    }
                }
            }
            short[] sharkAge = savedInstanceState.getShortArray(WorldKeys.SHARK_AGE_KEY);
            if (sharkAge != null) {
                short[] sharkHunger = savedInstanceState.getShortArray(WorldKeys.SHARK_HUNGER_KEY);
                if (sharkHunger != null) {
                    short[] sharkPosX = savedInstanceState.getShortArray(WorldKeys.SHARK_POSITIONS_X_KEY);
                    if (sharkPosX != null) {
                        short[] sharkPosY = savedInstanceState.getShortArray(WorldKeys.SHARK_POSITIONS_Y_KEY);
                        if (sharkPosY != null) {
                            for (int sharkNo = 0; sharkNo < sharkAge.length; sharkNo++) {
                                simulator.setShark(sharkPosX[sharkNo], sharkPosY[sharkNo], sharkAge[sharkNo],
                                        sharkHunger[sharkNo]);
                            }
                        }
                    }
                }
            }

            if (savedInstanceState.containsKey(WorldKeys.INITIAL_FISH_COUNT_KEY)
                    || savedInstanceState.containsKey(WorldKeys.INITIAL_SHARK_COUNT_KEY)) {
                if (savedInstanceState.containsKey(WorldKeys.INITIAL_FISH_COUNT_KEY)) {
                    parameters.setInitialFishCount(savedInstanceState.getInt(WorldKeys.INITIAL_FISH_COUNT_KEY));
                }
                if (savedInstanceState.containsKey(WorldKeys.INITIAL_SHARK_COUNT_KEY)) {
                    parameters.setInitialSharkCount(savedInstanceState.getInt(WorldKeys.INITIAL_SHARK_COUNT_KEY));
                }
                previousWorldParameters = parameters;
            }

        }

        simulatorRunnable = new SimulatorRunnable(simulator);
        if (savedInstanceState != null && savedInstanceState.containsKey(WorldKeys.TARGET_FPS_KEY)) {
            simulatorRunnable.setTargetFps(savedInstanceState.getInt(WorldKeys.TARGET_FPS_KEY));
        }
        simulatorRunnable.registerSimulatorRunnableObserver(this);

        drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        final ListView drawerList = (ListView) findViewById(R.id.drawer_commands);
        drawerList.setAdapter(new DrawerListAdapter(getDrawerCommands()));
        drawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                DrawerCommandItem command = (DrawerCommandItem) drawerList.getItemAtPosition(position);
                if (command != null) {
                    command.execute();
                }
            }
        });
        drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.open_drawer_description,
                R.string.close_drawer_description) {
            @Override
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                supportInvalidateOptionsMenu();
                synchronized (MainActivity.this) {
                    currentSimFps = (TextView) findViewById(R.id.fps_simulator);
                    currentDrawFps = (TextView) findViewById(R.id.fps_drawing);
                }
            }

            @Override
            public void onDrawerClosed(View drawerView) {
                super.onDrawerClosed(drawerView);
                supportInvalidateOptionsMenu();
                synchronized (MainActivity.this) {
                    currentSimFps = null;
                    currentDrawFps = null;
                }
            }
        };
        drawerLayout.addDrawerListener(drawerToggle);
    }

    /**
     * Intercept the back press. We want to not perform the default action if
     * <ul>
     *     <li>the drawer is showing (instead, hide the drawer)</li>
     *     <li>the "new world" fragment is showing (hide it)</li>
     * </ul>
     * Otherwise call through to {@code super}.
     */
    @Override
    public void onBackPressed() {
        if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
            drawerLayout.closeDrawers();
        } else if (!hideNewWorldFragment()) {
            super.onBackPressed();
        }
    }

    /**
     * Called whenever menu item is selected. We need to make sure that we pass this on to our
     * {@link #drawerToggle}.
     * @param item menu item that was selected
     * @return {@code true} if the event was handled; otherwise {@code false}
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Pass the event to ActionBarDrawerToggle
        // If it returns true, then it has handled
        // the nav drawer indicator touch event
        if (drawerToggle.onOptionsItemSelected(item)) {
            return true;
        }

        //TODO: handle own action items

        return super.onOptionsItemSelected(item);
    }

    /**
     * Handle post-create initialization. We cannot find fragments in the activity in {@link #onCreate(Bundle)}.
     * We also need to synchronize the state of the {@link #drawerToggle} to get the hamburger menu.
     * @param savedInstanceState Bundle with state to restore (ignored)
     */
    @Override
    protected void onPostCreate(@Nullable Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        drawerToggle.syncState();
        FragmentManager fm = getSupportFragmentManager();
        Fragment newWorldFragment = fm.findFragmentByTag(NEW_WORLD_FRAGMENT_TAG);
        if (newWorldFragment != null) {
            newWorldView.startAnimation(AnimationUtils.loadAnimation(this, R.anim.slide_in_down));
            newWorldView.setVisibility(View.VISIBLE);
        }
    }

    /**
     * The activitiy is resuming. We need to start our simulator and world updater thread.
     */
    @Override
    protected void onResume() {
        super.onResume();

        drawingAverageTime = new RollingAverage();

        worldUpdateNotifierThread = new Thread(getString(R.string.worldUpdateNotifierThreadName)) {
            @Override
            public void run() {
                if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                    Log.d("Wa-Tor", "Entering world update notifier thread");
                }
                try {
                    long lastUpdateFinished = 0;
                    while (Thread.currentThread() == worldUpdateNotifierThread) {
                        long startUpdate = System.currentTimeMillis();
                        if (Log.isLoggable("Wa-Tor", Log.VERBOSE)) {
                            Log.v("Wa-Tor", "WorldUpdateNotifierThread: Notifying observers of world update");
                        }
                        worldUpdated();
                        if (Log.isLoggable("Wa-Tor", Log.VERBOSE)) {
                            Log.v("Wa-Tor", "WorldUpdateNotifierThread: Notifying observers took "
                                    + (System.currentTimeMillis() - startUpdate) + " ms");
                        }
                        long now = System.currentTimeMillis();
                        if (lastUpdateFinished > 0 && drawingAverageTime != null) {
                            drawingAverageTime.add(now - lastUpdateFinished);
                            if (Log.isLoggable("Wa-Tor", Log.VERBOSE)) {
                                Log.v("Wa-Tor", "WorldUpdateNotifierThread: Time delta since last redraw "
                                        + (now - lastUpdateFinished) + " ms");
                            }
                        }
                        lastUpdateFinished = now;
                        if (Log.isLoggable("Wa-Tor", Log.VERBOSE)) {
                            Log.v("Wa-Tor", "WorldUpdateNotifierThread: Waiting for next update");
                        }
                        synchronized (this) {
                            wait();
                        }
                    }
                } catch (InterruptedException e) {
                    if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                        Log.d("Wa-Tor", "World update notifier thread got interrupted");
                    }
                    synchronized (this) {
                        worldUpdateNotifierThread = null;
                    }
                }
                if (Log.isLoggable("Wa-Tor", Log.DEBUG)) {
                    Log.d("Wa-Tor", "Exiting world update notifier thread");
                }
            }
        };
        worldUpdateNotifierThread.start();

        desiredFpsSlider.setValue(simulatorRunnable.getTargetFps());
        startSimulatorThread();

        synchronized (this) {
            if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
                currentSimFps = (TextView) findViewById(R.id.fps_simulator);
                currentDrawFps = (TextView) findViewById(R.id.fps_drawing);
            }
        }
    }

    /** Starts the simulator thread with our {@link #simulatorRunnable}. */
    private void startSimulatorThread() {
        Thread simulatorThread = new Thread(simulatorRunnable, getString(R.string.simulatorThreadName));
        simulatorThread.start();
    }

    /** The activity is pausing. Stop the simulator thread and the world updater thread. */
    @Override
    protected void onPause() {
        super.onPause();
        synchronized (this) {
            Thread t = worldUpdateNotifierThread;
            worldUpdateNotifierThread = null;
            if (t != null) {
                t.interrupt();
            }
            simulatorRunnable.stopTicking();
        }
    }

    /**
     * Add another {@link WorldObserver} to our list of observers.
     * @param newObserver observer to add
     */
    public void registerSimulatorObserver(WorldObserver newObserver) {
        synchronized (worldObserverMutex) {
            if (worldObservers.length == worldObserverCount - 1) {
                // Time to reallocate
                WorldObserver[] newObservers = new WorldObserver[worldObservers.length * 2];
                System.arraycopy(worldObservers, 0, newObservers, 0, worldObservers.length);
                worldObservers = newObservers;
            }
            worldObservers[worldObserverCount++] = newObserver;
        }
    }

    /**
     * Remove a {@link WorldObserver} from our list of observers.
     * @param goneObserver observer to remove
     */
    @Override
    public void unregisterSimulatorObserver(WorldObserver goneObserver) {
        synchronized (worldObserverMutex) {
            for (int no = 0; no < worldObservers.length; no++) {
                if (worldObservers[no] == goneObserver) {
                    System.arraycopy(worldObservers, no + 1, worldObservers, no, worldObserverCount - 1 - no);
                    worldObservers[worldObserverCount - 1] = null;
                    worldObserverCount--;
                }
            }
        }
    }

    /**
     * Called whenever the {@link SimulatorRunnable} has finished calculating a new world. We need to tell the
     * registered {@link WorldObserver} objects about this fact.
     * @param simulator simulator that has ticked
     */
    @Override
    synchronized public void simulatorUpdated(Simulator simulator) {
        if (worldUpdateNotifierThread != null) {
            //noinspection SynchronizeOnNonFinalField
            synchronized (worldUpdateNotifierThread) {
                worldUpdateNotifierThread.notify();
            }
        }
    }

}