com.google.android.apps.santatracker.doodles.tilt.SwimmingFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.santatracker.doodles.tilt.SwimmingFragment.java

Source

/*
 * Copyright (C) 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.tilt;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Point;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Vibrator;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.ToggleButton;

import com.google.android.apps.santatracker.doodles.R;
import com.google.android.apps.santatracker.doodles.shared.AndroidUtils;
import com.google.android.apps.santatracker.doodles.shared.AnimatedSprite;
import com.google.android.apps.santatracker.doodles.shared.Camera;
import com.google.android.apps.santatracker.doodles.shared.CameraShake;
import com.google.android.apps.santatracker.doodles.shared.DoodleConfig;
import com.google.android.apps.santatracker.doodles.shared.EventBus;
import com.google.android.apps.santatracker.doodles.shared.EventBus.EventBusListener;
import com.google.android.apps.santatracker.doodles.shared.GameFragment;
import com.google.android.apps.santatracker.doodles.shared.GameType;
import com.google.android.apps.santatracker.doodles.shared.HistoryManager;
import com.google.android.apps.santatracker.doodles.shared.HistoryManager.HistoryListener;
import com.google.android.apps.santatracker.doodles.shared.PineappleLogger;
import com.google.android.apps.santatracker.doodles.shared.RectangularInstructionActor;
import com.google.android.apps.santatracker.doodles.shared.SpriteActor;
import com.google.android.apps.santatracker.doodles.shared.Sprites;
import com.google.android.apps.santatracker.doodles.shared.UIUtil;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.shared.sound.SoundManager;
import com.google.android.apps.santatracker.doodles.tilt.SwimmingModel.SwimmingState;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.firebase.analytics.FirebaseAnalytics;

import java.io.File;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.SWIMMING_GAME_TYPE;

/**
 * The main fragment for the swimming game.
 */
public class SwimmingFragment extends GameFragment implements SensorEventListener, EventBusListener {
    private static final String TAG = SwimmingFragment.class.getSimpleName();
    public static final String CURRENT_LEVEL_KEY = "CURRENT_LEVEL";
    private static final float ACCELEROMETER_SCALE = 1.5f / 9.8f; // Scale to range -1.5:1.5.
    private static final int END_VIEW_ON_DEATH_DELAY_MS = 1400;

    public static boolean editorMode = false;

    private SwimmingView swimmingView;
    private DiveView diveView;
    private TextView countdownView;
    private Button saveButton;
    private Button loadButton;
    private Button resetButton;
    private Button deleteButton;
    private ToggleButton collisionModeButton;
    private SwimmingModel model;

    private SwimmingLevelManager levelManager;
    private SensorManager sensorManager;
    private Sensor accelerometerSensor;
    private int displayRotation;

    private int playCount = 0;
    private long titleDurationMs = GameFragment.TITLE_DURATION_MS;
    private SwimmingModel tempLevel;
    private boolean mIsGameOver = false;

    public SwimmingFragment() {
        super();
    }

    public SwimmingFragment(Context context, DoodleConfig doodleConfig, PineappleLogger logger,
            boolean editorMode) {
        super(context, doodleConfig, logger);
        SwimmingFragment.editorMode = editorMode;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (context == null) {
            return null;
        }

        levelManager = new SwimmingLevelManager(context);

        wrapper = new FrameLayout(context);

        titleView = getTitleView(R.drawable.penguin_swim_loadingscreen, R.string.swimming);
        wrapper.addView(titleView);
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                loadGame();
            }
        });
        return wrapper;
    }

    @Override
    protected void firstPassLoadOnUiThread() {
        final FrameLayout.LayoutParams wrapperLP = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);

        final SwimmingFragment that = this;
        scoreView = getScoreView();
        pauseView = getPauseView();

        int diveViewBottomMargin = (int) context.getResources().getDimension(R.dimen.dive_margin_bottom);
        int diveViewStartMargin = (int) context.getResources().getDimension(R.dimen.dive_margin_left);
        int diveViewSize = (int) context.getResources().getDimension(R.dimen.dive_image_size);

        FrameLayout.LayoutParams diveViewLP = new LayoutParams(diveViewSize, diveViewSize);
        diveViewLP.setMargins(diveViewStartMargin, 0, 0, diveViewBottomMargin);
        diveViewLP.gravity = Gravity.BOTTOM | Gravity.LEFT;

        if (VERSION.SDK_INT >= 17) {
            diveViewLP.setMarginStart(diveViewStartMargin);
        }
        diveView = new DiveView(context);

        countdownView = new TextView(context);
        countdownView.setGravity(Gravity.CENTER);
        countdownView.setTextColor(context.getResources().getColor(R.color.ui_text_yellow));
        countdownView.setTypeface(Typeface.DEFAULT_BOLD);
        countdownView.setText("0");
        countdownView.setVisibility(View.INVISIBLE);
        Locale locale = context.getResources().getConfiguration().locale;
        countdownView.setText(NumberFormat.getInstance(locale).format(3));
        Point screenDimens = AndroidUtils.getScreenSize();
        UIUtil.fitToBounds(countdownView, screenDimens.x / 10, screenDimens.y / 10);

        LinearLayout gameView = new LinearLayout(context);
        gameView.setOrientation(LinearLayout.VERTICAL);

        // Add game view.
        swimmingView = new SwimmingView(context);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT, 7);
        gameView.addView(swimmingView, lp);

        if (editorMode) {
            LinearLayout buttonWrapper = new LinearLayout(context);
            buttonWrapper.setOrientation(LinearLayout.HORIZONTAL);
            lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT, 1);
            gameView.addView(buttonWrapper, lp);

            resetButton = getButton(R.string.reset_level, new OnClickListener() {
                @Override
                public void onClick(View v) {
                    SwimmingModel level = levelManager.loadDefaultLevel();
                    initializeLevel(level, false);

                    getActivity().getSharedPreferences(context.getString(R.string.swimming), Context.MODE_PRIVATE)
                            .edit().putString(CURRENT_LEVEL_KEY, null).commit();
                }
            });
            deleteButton = getButton(R.string.delete_levels, new OnClickListener() {
                @Override
                public void onClick(View v) {
                    DialogFragment dialogFragment = new DeleteLevelDialogFragment();
                    dialogFragment.show(getActivity().getFragmentManager(), "delete");
                }
            });
            loadButton = getButton(R.string.load_level, new OnClickListener() {
                @Override
                public void onClick(View v) {
                    DialogFragment dialogFragment = new LoadLevelDialogFragment(that);
                    dialogFragment.show(getActivity().getFragmentManager(), "load");
                }
            });
            saveButton = getButton(R.string.save_level, new OnClickListener() {
                @Override
                public void onClick(View v) {
                    DialogFragment dialogFragment = new SaveLevelDialogFragment(that);
                    dialogFragment.show(getActivity().getFragmentManager(), "save");
                }
            });
            collisionModeButton = new ToggleButton(context);
            collisionModeButton.setText(R.string.scenery_mode);
            collisionModeButton.setTextOff(context.getString(R.string.scenery_mode));
            collisionModeButton.setTextOn(context.getString(R.string.collision_mode));
            collisionModeButton.setOnCheckedChangeListener(new OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    model.collisionMode = isChecked;
                }
            });

            lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.MATCH_PARENT, 1);
            buttonWrapper.addView(deleteButton, lp);
            buttonWrapper.addView(resetButton, lp);
            buttonWrapper.addView(loadButton, lp);
            buttonWrapper.addView(saveButton, lp);
            buttonWrapper.addView(collisionModeButton, lp);
        }

        sensorManager = (SensorManager) getActivity().getSystemService(Context.SENSOR_SERVICE);
        accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (accelerometerSensor == null) {
            // TODO: The game won't be playable without this, so what should we do?
            Log.d(TAG, "Accelerometer sensor is null");
        }
        displayRotation = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
                .getRotation();

        wrapper.addView(gameView, 0, wrapperLP);
        wrapper.addView(countdownView, 1);
        wrapper.addView(diveView, 2, diveViewLP);
        wrapper.addView(scoreView, 3);
        wrapper.addView(pauseView, 4);
    }

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

        tempLevel = new SwimmingModel();
        final SwimmingModel level = tempLevel;
        historyManager = new HistoryManager(context, new HistoryListener() {
            @Override
            public void onFinishedLoading() {
                addBestTimeLine(level);
            }

            @Override
            public void onFinishedSaving() {
            }
        });

        initializeLevel(tempLevel, true);
    }

    @Override
    protected void finalPassLoadOnUiThread() {
        soundManager = SoundManager.getInstance();
        loadSounds();

        onFinishedLoading();
        startHandlers();
        tempLevel = null;
    }

    @Override
    protected void replay() {
        super.replay();
        SwimmingModel level = new SwimmingModel();
        initializeLevel(level, false);
    }

    @Override
    protected void resume() {
        super.resume();
        if (uiRefreshHandler != null) {
            sensorManager.registerListener(this, accelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
            uiRefreshHandler.start(swimmingView);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
        // Clear the path list because it's static and otherwise it hangs around forever.
        if (SwimmingLevelChunk.pathList != null) {
            SwimmingLevelChunk.pathList.clear();
        }
    }

    @Override
    protected void onDestroyHelper() {
        if (swimmingView != null) {
            swimmingView.setModel(null);
        }
        model = null;
        tempLevel = null;
        levelManager = null;

        if (SwimmingLevelChunk.swimmingLevelChunks != null) {
            SwimmingLevelChunk.swimmingLevelChunks.clear();
        }
        if (SwimmingLevelChunk.pathList != null) {
            SwimmingLevelChunk.pathList.clear();
        }
    }

    @Override
    public void update(float deltaMs) {
        if (isPaused) {
            return;
        }
        model.update(deltaMs);
        diveView.update(deltaMs);

        // If we are in editor mode, hide the intro animation right away and skip the camera pan.
        // Otherwise, wait until it has finished playing before fading and then run the camera pan.
        if ((editorMode || model.timeElapsed >= titleDurationMs) && model.getState() == SwimmingState.INTRO) {
            if (editorMode) {
                Log.d(TAG, "Hiding intro animation right away.");

                // Run on UI thread since background threads can't touch the intro view.
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        titleView.setVisibility(View.GONE);
                    }
                });
                model.setState(SwimmingState.WAITING);
            } else {
                Log.d(TAG, "Fading out and hiding intro animation.");

                // Run on UI thread since background threads can't touch the intro view.
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        hideTitle();
                    }
                });
                model.setState(SwimmingState.WAITING);
            }
        }

    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.values == null || model == null) {
            return;
        }
        int sensorType = event.sensor.getType();
        if (sensorType == Sensor.TYPE_ACCELEROMETER) {
            float[] adjustedEventValues = AndroidUtils.getAdjustedAccelerometerValues(displayRotation,
                    event.values);
            float x = -adjustedEventValues[0] * ACCELEROMETER_SCALE;
            float y = -adjustedEventValues[1] * ACCELEROMETER_SCALE;
            // Accelerometer input is very noisy, so we filter it using a (simple) low-pass filter.
            float weight = 0.1f;
            model.tilt.x = model.tilt.x + weight * (x - model.tilt.x);
            model.tilt.y = model.tilt.y + weight * (y - model.tilt.y);
        }
    }

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

    @Override
    public void onEventReceived(int type, final Object data) {
        if (isDestroyed) {
            return;
        }
        switch (type) {
        case EventBus.SCORE_CHANGED:
            int distance = integerValue(data);
            boolean shouldAddStar = false;

            if (model.currentScoreThreshold < SwimmingModel.SCORE_THRESHOLDS.length
                    && distance >= SwimmingModel.SCORE_THRESHOLDS[model.currentScoreThreshold]) {
                // Pop in star.
                shouldAddStar = true;
                model.currentScoreThreshold++;
            }
            updateScoreUi(distance, shouldAddStar);
            break;
        case EventBus.SWIMMING_DIVE:
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    diveView.startCooldown();
                }
            });
            break;

        case EventBus.GAME_STATE_CHANGED:
            SwimmingState state = (SwimmingState) data;
            mIsGameOver = false;
            if (state == SwimmingState.FINISHED) {
                mIsGameOver = true;
                calculateBestDistance();
                calculateStars();
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        pauseView.hidePauseButton();
                    }
                });
                final Handler handler = new Handler(Looper.getMainLooper());
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (model == null) {
                            return;
                        }
                        int bestDistance = historyManager.getBestDistance(GameType.SWIMMING).intValue();
                        String collidedObjectType = model.swimmer.getCollidedObjectType();
                        scoreView.updateBestScore(AndroidUtils.getText(context.getResources(),
                                R.string.swimming_score, bestDistance));
                        scoreView.setShareDrawable(getShareImageDrawable(collidedObjectType));

                        diveView.hide();
                        scoreView.animateToEndState();
                    }
                }, END_VIEW_ON_DEATH_DELAY_MS);
            }

            // [ANALYTICS]
            FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(getActivity());
            MeasurementManager.recordSwimmingEnd(analytics, model.getStarCount(), model.distanceMeters,
                    model.swimmer.getCollidedObjectType());
            break;

        case EventBus.PLAY_SOUND:
            int resId = (int) data;
            if (soundManager != null) {
                soundManager.play(resId);
            }
            break;

        case EventBus.MUTE_SOUNDS:
            boolean shouldMute = (boolean) data;
            if (soundManager != null) {
                if (shouldMute) {
                    soundManager.mute();
                } else {
                    soundManager.unmute();
                }
            }
            break;

        case EventBus.GAME_LOADED:
            long loadTimeMs = (long) data;
            titleDurationMs = Math.max(0, titleDurationMs - loadTimeMs);
            Log.d(TAG, "Waiting " + titleDurationMs + "ms and then hiding title.");
            break;
        }

    }

    @Nullable
    private Drawable getShareImageDrawable(String collidedObjectType) {
        return ContextCompat.getDrawable(getActivity(), R.drawable.winner);
    }

    private void initializeLevel(final SwimmingModel newLevel, boolean shouldPlayIntro) {
        EventBus.getInstance().clearListeners();
        EventBus.getInstance().register(newLevel, EventBus.VIBRATE);
        EventBus.getInstance().register(newLevel, EventBus.SHAKE_SCREEN);
        EventBus.getInstance().register(newLevel, EventBus.GAME_STATE_CHANGED);

        // Initialize Activity-specific stuff.
        EventBus.getInstance().register(this);

        Point size = AndroidUtils.getScreenSize();
        newLevel.screenWidth = size.x;
        newLevel.screenHeight = size.y;
        newLevel.addActor(new Camera(newLevel.screenWidth, newLevel.screenHeight));
        newLevel.addActor(new CameraShake());
        // Center the level horizontally on the screen and scale to fit.
        newLevel.camera.moveImmediatelyTo(Vector2D.get(0, 0), Vector2D.get(SwimmingModel.LEVEL_WIDTH, 0));
        newLevel.playCount = playCount++;
        newLevel.locale = getResources().getConfiguration().locale;

        if (isDestroyed) {
            return;
        }
        SwimmerActor swimmer = SwimmerActor.create(Vector2D.get(0, 0), context.getResources(), this);
        if (swimmer == null) {
            return;
        }
        swimmer.moveTo(SwimmingModel.LEVEL_WIDTH / 2 - swimmer.collisionBody.getWidth() / 2,
                SwimmerActor.KICKOFF_IDLE_Y_OFFSET);
        newLevel.addActor(swimmer);

        DistanceMarkerActor startingBlock = new DistanceMarkerActor(0, ColoredRectangleActor.STARTING_BLOCK, 1000);
        newLevel.addActor(startingBlock);

        SpriteActor banner = new SpriteActor(
                AnimatedSprite.fromFrames(context.getResources(), Sprites.penguin_swim_banner),
                Vector2D.get(SwimmingModel.LEVEL_WIDTH / 2, startingBlock.position.y), Vector2D.get());
        banner.scale = 2;
        banner.zIndex = 2;
        banner.sprite.setAnchor(banner.sprite.frameWidth / 2, banner.sprite.frameHeight - 50);
        newLevel.addActor(banner);

        newLevel.addActor(new DistanceMarkerActor(30, ColoredRectangleActor.DISTANCE_30M));
        newLevel.addActor(new DistanceMarkerActor(50, ColoredRectangleActor.DISTANCE_50M));
        newLevel.addActor(new DistanceMarkerActor(100, ColoredRectangleActor.DISTANCE_100M));
        newLevel.addActor(new DistanceMarkerActor(SwimmingLevelChunk.LEVEL_LENGTH_IN_METERS,
                ColoredRectangleActor.DISTANCE_LEVEL_LENGTH));
        if (historyManager != null) {
            addBestTimeLine(newLevel);
        }

        if (isDestroyed) {
            return;
        }
        ObstacleManager obstacleManager = new ObstacleManager(swimmer, context);
        newLevel.addActor(obstacleManager);

        newLevel.camera.position.y = -newLevel.camera.toWorldScale(newLevel.screenHeight);
        newLevel.clampCameraPosition(); // Clamp camera so that it doesn't jump when we start swimming.

        AnimatedSprite swimmingTutorialSprite = AnimatedSprite.fromFrames(context.getResources(),
                Sprites.tutorial_swimming);
        swimmingTutorialSprite.setFPS(6);
        RectangularInstructionActor instructions = new RectangularInstructionActor(context.getResources(),
                swimmingTutorialSprite);
        instructions.hidden = true;
        instructions.scale = (newLevel.screenWidth * 0.6f) / instructions.rectangle.frameWidth;
        // Put instructions at top right, slightly below pause button.
        instructions.position.set(newLevel.screenWidth / 2 - instructions.getScaledWidth() / 2, 70);
        newLevel.addUiActor(instructions);

        newLevel.setCountdownView(countdownView);

        if (!shouldPlayIntro) {
            // Skip State.INTRO.
            newLevel.setState(SwimmingState.WAITING);
        }
        if (isDestroyed) {
            return;
        }
        model = newLevel;
        swimmingView.setModel(model);

        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                initializeLevelUiThread(newLevel);
            }
        });

        // Hint that this would be a good time to do some garbage collection since we should be able
        // to get rid of any stuff from the previous level at this time.
        System.gc();
    }

    // Initialize level parts that need to happen in UI thread.
    private void initializeLevelUiThread(final SwimmingModel newLevel) {
        if (isDestroyed) {
            return;
        }
        scoreView.updateCurrentScore(AndroidUtils.getText(context.getResources(), R.string.swimming_score, 0),
                false);
        pauseView.showPauseButton();
        diveView.show();
        newLevel.vibrator = (Vibrator) getActivity().getSystemService(Activity.VIBRATOR_SERVICE);
    }

    @Override
    protected void loadSounds() {
        super.loadSounds();
        soundManager.loadShortSound(context, R.raw.swimming_dive_down);
        soundManager.loadShortSound(context, R.raw.swimming_dive_up);
        soundManager.loadShortSound(context, R.raw.swimming_ice_splash_a);
        soundManager.loadShortSound(context, R.raw.swimming_duck_collide);
        soundManager.loadShortSound(context, R.raw.swimming_ice_collide);
        soundManager.loadShortSound(context, R.raw.swimming_grab);
    }

    private void addBestTimeLine(SwimmingModel level) {
        Double bestDistance = historyManager.getBestDistance(GameType.SWIMMING);
        if (bestDistance != null) {
            level.addActor(new DistanceMarkerActor(bestDistance.intValue(), ColoredRectangleActor.DISTANCE_PR));
        }
    }

    private void calculateStars() {
        Integer bestStarCount = historyManager.getBestStarCount(GameType.SWIMMING);
        int modelStarCount = model.getStarCount();
        if (bestStarCount == null || bestStarCount < modelStarCount) {
            historyManager.setBestStarCount(GameType.SWIMMING, modelStarCount);
        }
    }

    private void calculateBestDistance() {
        Double bestDistance = historyManager.getBestDistance(GameType.SWIMMING);
        if (bestDistance == null || bestDistance < model.distanceMeters) {
            historyManager.setBestDistance(GameType.SWIMMING, model.distanceMeters);
        }
    }

    private Button getButton(int textId, OnClickListener onClickListener) {
        Button button = new Button(context);
        button.setText(textId);
        button.setOnClickListener(onClickListener);
        return button;
    }

    private void updateScoreUi(final int score, final boolean shouldShowStars) {
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                scoreView.updateCurrentScore(
                        AndroidUtils.getText(context.getResources(), R.string.swimming_score, score),
                        shouldShowStars);
                if (shouldShowStars) {
                    scoreView.addStar();
                }
            }
        });

    }

    private int integerValue(Object object) {
        if (object instanceof Integer) {
            return (int) object;
        } else {
            throw new IllegalArgumentException("Unknown event data type");
        }
    }

    @Override
    public void onGamePause() {
        if (model != null && model.swimmer != null) {
            model.swimmer.controlsEnabled = false;
        }
        super.onGamePause();
    }

    @Override
    protected void onGameResume() {
        if (model != null && model.swimmer != null) {
            model.swimmer.controlsEnabled = true;
        }
        super.onGameResume();
    }

    @Override
    protected void onGameReplay() {
        if (model != null && model.swimmer != null) {
            model.swimmer.controlsEnabled = true;
        }
        super.onGameReplay();
    }

    @Override
    protected boolean onTitleTapped() {
        if (model != null && model.getState() == SwimmingState.INTRO) {
            // If the user taps the screen while the intro animation is playing, end the intro
            // immediately and transition into the game.
            model.timeElapsed = GameFragment.TITLE_DURATION_MS;
            return true;
        }
        return false;
    }

    @Override
    protected String getGameType() {
        return SWIMMING_GAME_TYPE;
    }

    @Override
    protected float getScore() {
        return model.distanceMeters;
    }

    @Override
    protected int getShareImageId() {
        String collidedObjectType = model.swimmer.getCollidedObjectType();
        if (BoundingBoxSpriteActor.ICE_CUBE.equals(collidedObjectType)) {
            return 0;
        } else if (BoundingBoxSpriteActor.DUCK.equals(collidedObjectType)) {
            return 1;
        } else { // Octopus hand grab.
            return 2;
        }
    }

    /**
     * A dialog fragment for loading a swimming level.
     */
    public static class LoadLevelDialogFragment extends DialogFragment {
        private SwimmingFragment swimmingFragment;

        public LoadLevelDialogFragment(SwimmingFragment swimmingFragment) {
            this.swimmingFragment = swimmingFragment;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final String[] fileList = SwimmingLevelManager.levelsDir.exists()
                    ? SwimmingLevelManager.levelsDir.list()
                    : new String[0];

            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(R.string.load_level).setItems(fileList, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    SwimmingModel level = swimmingFragment.levelManager.loadLevel(fileList[which]);
                    swimmingFragment.initializeLevel(level, false);

                    // Save the current level so we automatically come back to it later.
                    getActivity()
                            .getSharedPreferences(swimmingFragment.context.getString(R.string.swimming),
                                    Context.MODE_PRIVATE)
                            .edit().putString(CURRENT_LEVEL_KEY, fileList[which]).commit();
                }
            });
            return builder.create();
        }
    }

    /**
     * A dialog fragment for saving a swimming level.
     */
    public static class SaveLevelDialogFragment extends DialogFragment {
        private SwimmingFragment swimmingFragment;

        public SaveLevelDialogFragment(SwimmingFragment swimmingFragment) {
            this.swimmingFragment = swimmingFragment;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final EditText editText = new EditText(swimmingFragment.context);
            editText.setText(swimmingFragment.model.levelName);
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(R.string.save_level).setView(editText)
                    .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            swimmingFragment.levelManager.saveLevel(swimmingFragment.model,
                                    editText.getText().toString());
                        }
                    }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            // Do nothing.
                        }
                    });
            return builder.create();
        }
    }

    /**
     * A dialog fragment for deleting a swimming level.
     */
    public static class DeleteLevelDialogFragment extends DialogFragment {
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final List<Integer> selectedItems = new ArrayList<>();
            final String[] fileList = SwimmingLevelManager.levelsDir.exists()
                    ? SwimmingLevelManager.levelsDir.list()
                    : new String[0];

            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(R.string.delete_levels)
                    .setMultiChoiceItems(fileList, null, new DialogInterface.OnMultiChoiceClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                            if (isChecked) {
                                selectedItems.add(which);
                            } else if (selectedItems.contains(which)) {
                                selectedItems.remove(Integer.valueOf(which));
                            }
                        }
                    }).setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            for (Integer selection : selectedItems) {
                                File file = new File(SwimmingLevelManager.levelsDir, fileList[selection]);
                                file.delete();
                            }
                        }
                    }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            // Do nothing.
                        }
                    });
            return builder.create();
        }
    }

    public boolean isGameOver() {
        return mIsGameOver;
    }
}