com.google.android.apps.santatracker.rocketsleigh.RocketSleighActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.santatracker.rocketsleigh.RocketSleighActivity.java

Source

/*
 * Copyright (C) 2015 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.rocketsleigh;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Vibrator;
import android.support.v4.app.FragmentActivity;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AlphaAnimation;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.VideoView;

import com.google.android.apps.santatracker.invites.AppInvitesFragment;
import com.google.android.apps.santatracker.util.AnalyticsManager;
import com.google.android.apps.santatracker.util.ImmersiveModeHelper;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.firebase.analytics.FirebaseAnalytics;

import java.io.IOException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public class RocketSleighActivity extends FragmentActivity
        implements View.OnTouchListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener,
        OnClickListener, SoundPool.OnLoadCompleteListener, MediaPlayer.OnCompletionListener {

    private ImageView mElf;
    private ImageView mThrust;
    private Bitmap mElfBitmap;
    private Bitmap mBurnBitmap;
    private Bitmap mThrustBitmap;
    private Bitmap mSmokeBitmpap;
    private Bitmap mCurrentTrailBitmap;
    private LinearLayout mElfLayout;
    private int mElfState = 0; // 0 - 100% ... 3 - 25% 4 - Parachute elf.
    private boolean mElfIsHit = false;
    private long mElfHitTime = 0;
    private float mElfPosX = 100; // Pixels from the left edge
    private float mElfPosY = 200; // Pixels from the top edge
    private float mElfVelX = 0.3f; // Horizontal speed.
    private float mElfVelY = 0.0f; // Vertical speed.
    private float mElfAccelY = 0.0f; // Acceleration due to thrust if user is touching screen.
    private float mThrustAccelY; // Vertical acceleration in pixel velocity per second of thrust.
    private float mGravityAccelY; // Vertical acceleration due to gravity in pixel velocity per second.
    private long mLastTime;
    private float mElfScale;

    private CountDownTimer mCountDownTimer;

    // Achievments
    private boolean mHit = false;
    private boolean mHitLevel = false;
    private boolean mCleanLevel = false;
    private boolean mPresentBonus = false;

    private boolean mRainingPresents = false;
    private ImageView mPlus100;
    private ImageView mPlus500;
    private AlphaAnimation m100Anim;
    private AlphaAnimation m500Anim;

    private LinearLayout mObstacleLayout;
    private HorizontalScrollView mObstacleScroll;
    private int mSlotWidth;
    // This is the width of an ornament "slot".  Obstacles can span multiple slots.
    private Random mRandom;
    private int mLastTopHeight = 0;
    private int mLastBottomHeight = 0;

    private LinearLayout mBackgroundLayout;
    private HorizontalScrollView mBackgroundScroll;
    private LinearLayout mForegroundLayout;
    private HorizontalScrollView mForegroundScroll;
    private float mScaleY = 1.0f;
    private float mScaleX = 1.0f;

    private TextView mScoreText;
    private String mScoreLabel;
    private ImageView mPlayPauseButton;

    private int mScreenHeight;
    private int mScreenWidth;

    private View mControlView;
    private GestureDetector mGestureDetector;
    private MotionEvent mDownEvent;

    private TextView mCountdown;

    private boolean mIsTv = true;
    private boolean mIsPlaying = false;
    private boolean mMoviePlaying = false;
    private boolean mCountdownStarted = false;
    private int mLevel = 0; // There are six levels.
    private long mScore = 0;
    private int mPresentCount = 0; // 5 in a row gets a bonus...
    private int mBackgroundCount = 0; // 5 copies of backgrounds per level
    private int mTransitionImagesCount = 0; // Some level transitions have transition images.

    private LayoutInflater mInflater;

    private long mLastFrameTime = 0;

    private Vibrator mVibrator;

    private Handler mHandler;

    private VideoView mIntroVideo;
    private View mIntroControl;
    private MediaPlayer mBackgroundPlayer;

    // For sound effects
    private SoundPool mSoundPool;
    private int mCrashSound1;
    private int mCrashSound2;
    private int mCrashSound3;
    private int mGameOverSound;
    private int mJetThrustSound;
    private int mLevelUpSound;
    private int mScoreBigSound;
    private int mScoreSmallSound;
    private int mJetThrustStream;

    private View mBigPlayButtonLayout;
    private ImageButton mBigPlayButton;

    private ImageView mExit;

    private FirebaseAnalytics mMeasurement;
    private AppInvitesFragment mInvitesFragment;

    private static final String LOG_TAG = RocketSleighActivity.class.getSimpleName();
    private static final int SLOTS_PER_SCREEN = 10;

    private static final int[] BACKGROUNDS = { R.drawable.bg_jet_pack_1, R.drawable.bg_jet_pack_2,
            R.drawable.bg_jet_pack_3, R.drawable.bg_jet_pack_4, R.drawable.bg_jet_pack_5,
            R.drawable.bg_jet_pack_6 };

    private Bitmap[] mBackgrounds;
    private Bitmap[] mBackgrounds2;

    private static final int[] FOREGROUNDS = { R.drawable.img_snow_ground_tiles, R.drawable.img_snow_ground_tiles,
            R.drawable.img_snow_ground_tiles, R.drawable.img_snow_ground_tiles, -1, -1 };

    private static final int[] EXIT_TRANSITIONS = { -1, -1, -1, R.drawable.bg_transition_2, -1,
            R.drawable.bg_transition_4, };

    private Bitmap[] mExitTransitions;

    private static final int[] ENTRY_TRANSITIONS = { -1, -1, R.drawable.bg_transition_1, -1,
            R.drawable.bg_transition_3, -1 };

    private Bitmap[] mEntryTransitions;

    private static final int[] ELF_IMAGES = { R.drawable.img_jetelf_100, R.drawable.img_jetelf_75,
            R.drawable.img_jetelf_50, R.drawable.img_jetelf_25, R.drawable.img_jetelf_0 };

    private Bitmap[] mElfImages;

    private static final int[] ELF_HIT_IMAGES = { R.drawable.img_jetelf_100_hit, R.drawable.img_jetelf_75_hit,
            R.drawable.img_jetelf_50_hit, R.drawable.img_jetelf_25_hit, };

    private Bitmap[] mElfHitImages;

    private static final int[] ELF_BURN_IMAGES = { R.drawable.img_jet_burn_100, R.drawable.img_jet_burn_75,
            R.drawable.img_jet_burn_50, R.drawable.img_jet_burn_25 };

    private Bitmap[] mElfBurnImages;

    private static final int[] ELF_THRUST_IMAGES = { R.drawable.img_jet_thrust_100, R.drawable.img_jet_thrust_75,
            R.drawable.img_jet_thrust_50, R.drawable.img_jet_thrust_25 };

    private Bitmap[] mElfThrustImages;

    private static final int[] ELF_SMOKE_IMAGES = { R.drawable.img_jet_smoke_100_hit,
            R.drawable.img_jet_smoke_75_hit, R.drawable.img_jet_smoke_50_hit, R.drawable.img_jet_smoke_25_hit };

    private Bitmap[] mElfSmokeImages;

    private static final int[] GIFT_BOXES = { R.drawable.img_gift_blue_jp, R.drawable.img_gift_green_jp,
            R.drawable.img_gift_yellow_jp, R.drawable.img_gift_purple_jp, R.drawable.img_gift_red_jp };

    private Bitmap[] mGiftBoxes;

    // Top, Bottom, Background
    private static final int[] WOOD_OBSTACLES = { -1, R.drawable.img_pine_1_bottom, R.drawable.img_pine_0, -1,
            R.drawable.img_pine_2_bottom, R.drawable.img_pine_0, -1, R.drawable.img_pine_3_bottom,
            R.drawable.img_pine_0, -1, R.drawable.img_pine_4_bottom, R.drawable.img_pine_0,
            R.drawable.img_pine_1_top, -1, R.drawable.img_pine_0, R.drawable.img_pine_2_top, -1,
            R.drawable.img_pine_0, R.drawable.img_pine_3_top, -1, R.drawable.img_pine_0, R.drawable.img_pine_4_top,
            -1, R.drawable.img_pine_0, -1, R.drawable.img_birch_1_bottom, R.drawable.img_birch_0, -1,
            R.drawable.img_birch_2_bottom, R.drawable.img_birch_0, -1, R.drawable.img_birch_3_bottom,
            R.drawable.img_birch_0, -1, R.drawable.img_birch_4_bottom, R.drawable.img_birch_0,
            R.drawable.img_birch_1_top, -1, R.drawable.img_birch_0, R.drawable.img_birch_2_top, -1,
            R.drawable.img_birch_0, R.drawable.img_birch_3_top, -1, R.drawable.img_birch_0,
            R.drawable.img_birch_4_top, -1, R.drawable.img_birch_0, -1, R.drawable.img_tree_1_bottom,
            R.drawable.img_birch_0, -1, R.drawable.img_tree_2_bottom, R.drawable.img_birch_0, -1,
            R.drawable.img_tree_3_bottom, R.drawable.img_birch_0, -1, R.drawable.img_tree_4_bottom,
            R.drawable.img_birch_0, -1, R.drawable.img_tree_5_bottom, R.drawable.img_birch_0, -1,
            R.drawable.img_tree_6_bottom, R.drawable.img_birch_0, R.drawable.img_tree_1_top, -1,
            R.drawable.img_birch_0, R.drawable.img_tree_2_top, -1, R.drawable.img_birch_0,
            R.drawable.img_tree_3_top, -1, R.drawable.img_birch_0, R.drawable.img_tree_4_top, -1,
            R.drawable.img_birch_0, R.drawable.img_tree_5_top, -1, R.drawable.img_birch_0,
            R.drawable.img_tree_6_top, -1, R.drawable.img_birch_0, -1, R.drawable.img_owl, -1, -1,
            R.drawable.img_log_elf, -1, -1, R.drawable.img_bear_big, -1, R.drawable.img_bear_little };

    private TreeMap<Integer, Bitmap> mWoodObstacles;
    private ArrayList<Integer> mWoodObstacleList;
    private int mWoodObstacleIndex = 0;

    // Top and bottom, no backgrounds
    private static final int[] CAVE_OBSTACLES = { R.drawable.img_icicle_small_3, -1, R.drawable.img_icicle_small_4,
            -1, R.drawable.img_icicle_med_3, -1, R.drawable.img_icicle_med_4, -1, R.drawable.img_icicle_lrg_2, -1,
            -1, R.drawable.img_icicle_small_1, -1, R.drawable.img_icicle_small_2, -1, R.drawable.img_icicle_med_1,
            -1, R.drawable.img_icicle_med_2, -1, R.drawable.img_icicle_lrg_1, R.drawable.img_icicle_small_3,
            R.drawable.img_icicle_small_1, R.drawable.img_icicle_small_3, R.drawable.img_icicle_small_2,
            R.drawable.img_icicle_small_4, R.drawable.img_icicle_small_1, R.drawable.img_icicle_small_4,
            R.drawable.img_icicle_small_2, R.drawable.img_2_bats, -1, R.drawable.img_3_bats, -1,
            R.drawable.img_4_bats, -1, R.drawable.img_5_bats, -1, -1, R.drawable.img_yeti, -1,
            R.drawable.img_mammoth, -1, R.drawable.img_snow_kiss, -1, R.drawable.img_snowman };

    private TreeMap<Integer, Bitmap> mCaveObstacles;
    private ArrayList<Integer> mCaveObstacleList;
    private int mCaveObstacleIndex = 0;

    private final static int[] FACTORY_OBSTACLES = { R.drawable.img_icecream_drop, R.drawable.img_icecream_0,
            R.drawable.img_icecream_drop, R.drawable.img_icecream_1, R.drawable.img_mint_drop_top,
            R.drawable.img_mint_drop_bottom, R.drawable.img_mint_stack_top, R.drawable.img_mint_stack_bottom, -1,
            R.drawable.img_candy_cane_0, R.drawable.img_candy_cane_1, -1, -1, R.drawable.img_lollipops, -1,
            R.drawable.img_choco_fountn, -1, R.drawable.img_candy_buttons, -1, R.drawable.img_mint_gondola, -1,
            R.drawable.img_candy_cane_0, R.drawable.img_candy_cane_1, -1, -1, R.drawable.img_lollipops, -1,
            R.drawable.img_choco_fountn, -1, R.drawable.img_candy_buttons, -1, R.drawable.img_mint_gondola, -1,
            R.drawable.img_candy_cane_0, R.drawable.img_candy_cane_1, -1, -1, R.drawable.img_lollipops, -1,
            R.drawable.img_choco_fountn, -1, R.drawable.img_candy_buttons, -1, R.drawable.img_mint_gondola };

    private TreeMap<Integer, Bitmap> mFactoryObstacles;
    private ArrayList<Integer> mFactoryObstacleList;
    private int mFactoryObstacleIndex = 0;

    private Runnable mGameLoop = new Runnable() {
        @Override
        public void run() {
            processFrame();
        }
    };

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.d(LOG_TAG, "onCreate() : " + savedInstanceState);

        setContentView(R.layout.activity_jet_pack_elf);

        // App Invites
        mInvitesFragment = AppInvitesFragment.getInstance(this);

        // App Measurement
        mMeasurement = FirebaseAnalytics.getInstance(this);
        MeasurementManager.recordScreenView(mMeasurement, getString(R.string.analytics_screen_rocket));

        // [ANALYTICS SCREEN]: Rocket Sleigh
        AnalyticsManager.initializeAnalyticsTracker(this);
        AnalyticsManager.sendScreenView(R.string.analytics_screen_rocket);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            ImmersiveModeHelper.setImmersiveSticky(getWindow());
            ImmersiveModeHelper.installSystemUiVisibilityChangeListener(getWindow());
        }

        mIntroVideo = (VideoView) findViewById(R.id.intro_view);
        mIntroControl = findViewById(R.id.intro_control_view);
        if (savedInstanceState == null) {
            String path = "android.resource://" + getPackageName() + "/" + R.raw.jp_background;
            mBackgroundPlayer = new MediaPlayer();
            try {
                mBackgroundPlayer.setDataSource(this, Uri.parse(path));
                mBackgroundPlayer.setLooping(true);
                mBackgroundPlayer.prepare();
                mBackgroundPlayer.start();
            } catch (IOException e) {
                e.printStackTrace();
            }

            boolean nomovie = false;
            if (getIntent().getBooleanExtra("nomovie", false)) {
                nomovie = true;
            } else if (Build.MANUFACTURER.toUpperCase().contains("SAMSUNG")) {
                //                nomovie = true;
            }
            if (!nomovie) {
                mIntroControl.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        endIntro();
                    }
                });
                path = "android.resource://" + getPackageName() + "/" + R.raw.intro_wipe;
                mIntroVideo.setVideoURI(Uri.parse(path));
                mIntroVideo.setOnCompletionListener(this);
                mIntroVideo.start();
                mMoviePlaying = true;
            } else {
                mIntroControl.setOnClickListener(null);
                mIntroControl.setVisibility(View.GONE);
                mIntroVideo.setVisibility(View.GONE);
            }
        } else {
            mIntroControl.setOnClickListener(null);
            mIntroControl.setVisibility(View.GONE);
            mIntroVideo.setVisibility(View.GONE);
        }

        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // For hit indication.

        mHandler = new Handler(); // Get the main UI handler for posting update events

        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);

        Log.d(LOG_TAG, "Width: " + dm.widthPixels + " Height: " + dm.heightPixels + " Density: " + dm.density);

        mScreenHeight = dm.heightPixels;
        mScreenWidth = dm.widthPixels;
        mSlotWidth = mScreenWidth / SLOTS_PER_SCREEN;

        // Setup the random number generator
        mRandom = new Random();
        mRandom.setSeed(System.currentTimeMillis()); // This is ok.  We are not looking for cryptographically secure random here!

        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Setup the background/foreground
        mBackgroundLayout = (LinearLayout) findViewById(R.id.background_layout);
        mBackgroundScroll = (HorizontalScrollView) findViewById(R.id.background_scroll);
        mForegroundLayout = (LinearLayout) findViewById(R.id.foreground_layout);
        mForegroundScroll = (HorizontalScrollView) findViewById(R.id.foreground_scroll);

        mBackgrounds = new Bitmap[6];
        mBackgrounds2 = new Bitmap[6];
        mExitTransitions = new Bitmap[6];
        mEntryTransitions = new Bitmap[6];

        // Need to vertically scale background to fit the screen.  Checkthe image size
        // compared to screen size and scale appropriately.  We will also use the matrix to translate
        // as we move through the level.
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), BACKGROUNDS[0]);
        Log.d(LOG_TAG, "Bitmap Width: " + bmp.getWidth() + " Height: " + bmp.getHeight() + " Screen Width: "
                + dm.widthPixels + " Height: " + dm.heightPixels);
        mScaleY = (float) dm.heightPixels / (float) bmp.getHeight();
        mScaleX = (float) (dm.widthPixels * 2) / (float) bmp.getWidth(); // Ensure that a single bitmap is 2 screens worth of time.  (Stock xxhdpi image is 3840x1080)

        if ((mScaleX != 1.0f) || (mScaleY != 1.0f)) {
            Bitmap tmp = Bitmap.createScaledBitmap(bmp, mScreenWidth * 2, mScreenHeight, false);
            if (tmp != bmp) {
                bmp.recycle();
                bmp = tmp;
            }
        }
        BackgroundLoadTask.createTwoBitmaps(bmp, mBackgrounds, mBackgrounds2, 0);

        // Load the initial background view
        addNextImages(0);
        addNextImages(0);

        mWoodObstacles = new TreeMap<Integer, Bitmap>();
        mWoodObstacleList = new ArrayList<Integer>();
        mWoodObstacleIndex = 0;
        // We need the bitmaps, so we do pre-load here synchronously.
        initObstaclesAndPreLoad(WOOD_OBSTACLES, 3, mWoodObstacles, mWoodObstacleList);

        mCaveObstacles = new TreeMap<Integer, Bitmap>();
        mCaveObstacleList = new ArrayList<Integer>();
        mCaveObstacleIndex = 0;
        initObstacles(CAVE_OBSTACLES, 2, mCaveObstacleList);

        mFactoryObstacles = new TreeMap<Integer, Bitmap>();
        mFactoryObstacleList = new ArrayList<Integer>();
        mFactoryObstacleIndex = 0;
        initObstacles(FACTORY_OBSTACLES, 2, mFactoryObstacleList);

        // Setup the elf
        mElf = (ImageView) findViewById(R.id.elf_image);
        mThrust = (ImageView) findViewById(R.id.thrust_image);
        mElfLayout = (LinearLayout) findViewById(R.id.elf_container);
        loadElfImages();
        updateElf(false);
        // Elf should be the same height relative to the height of the screen on any platform.
        Matrix scaleMatrix = new Matrix();
        mElfScale = ((float) dm.heightPixels * 0.123f) / (float) mElfBitmap.getHeight(); // On a 1920x1080 xxhdpi screen, this makes the elf 133 pixels which is the height of the drawable.
        scaleMatrix.preScale(mElfScale, mElfScale);
        mElf.setImageMatrix(scaleMatrix);
        mThrust.setImageMatrix(scaleMatrix);
        mElfPosX = (dm.widthPixels * 15) / 100; // 15% Into the screen
        mElfPosY = (dm.heightPixels - ((float) mElfBitmap.getHeight() * mElfScale)) / 2; // About 1/2 way down.
        mElfVelX = (float) dm.widthPixels / 3000.0f; // We start at 3 seconds for a full screen to scroll.
        mGravityAccelY = (float) (2 * dm.heightPixels) / (float) Math.pow((1.2 * 1000.0), 2.0); // a = 2*d/t^2 Where d = height in pixels and t = 1.2 seconds
        mThrustAccelY = (float) (2 * dm.heightPixels) / (float) Math.pow((0.7 * 1000.0), 2.0); // a = 2*d/t^2 Where d = height in pixels and t = 0.7 seconds

        // Setup the control view
        mControlView = findViewById(R.id.control_view);
        mGestureDetector = new GestureDetector(this, this);
        mGestureDetector.setIsLongpressEnabled(true);
        mGestureDetector.setOnDoubleTapListener(this);

        mScoreLabel = getString(R.string.score);
        mScoreText = (TextView) findViewById(R.id.score_text);
        mScoreText.setText("0");

        mPlayPauseButton = (ImageView) findViewById(R.id.play_pause_button);
        mExit = (ImageView) findViewById(R.id.exit);

        // Is Tv?
        mIsTv = TvUtil.isTv(this);
        if (mIsTv) {
            mScoreText.setText(mScoreLabel + ": 0");
            mPlayPauseButton.setVisibility(View.GONE);
            mExit.setVisibility(View.GONE);
            // move scoreLayout position to the Top-Right corner.
            View scoreLayout = findViewById(R.id.score_layout);
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) scoreLayout.getLayoutParams();
            params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
            params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
            params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
            params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);

            final int marginTop = getResources().getDimensionPixelOffset(R.dimen.overscan_margin_top);
            final int marginLeft = getResources().getDimensionPixelOffset(R.dimen.overscan_margin_left);

            params.setMargins(marginLeft, marginTop, 0, 0);
            scoreLayout.setLayoutParams(params);
            scoreLayout.setBackground(null);
            scoreLayout.findViewById(R.id.score_text_seperator).setVisibility(View.GONE);
        } else {
            mPlayPauseButton.setEnabled(false);
            mPlayPauseButton.setOnClickListener(this);
            mExit.setOnClickListener(this);
        }

        mBigPlayButtonLayout = findViewById(R.id.big_play_button_layout);
        mBigPlayButtonLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // No interaction with the screen below this one.
                return true;
            }
        });
        mBigPlayButton = (ImageButton) findViewById(R.id.big_play_button);
        mBigPlayButton.setOnClickListener(this);

        // For showing points when getting presents.
        mPlus100 = (ImageView) findViewById(R.id.plus_100);
        m100Anim = new AlphaAnimation(1.0f, 0.0f);
        m100Anim.setDuration(1000);
        m100Anim.setFillBefore(true);
        m100Anim.setFillAfter(true);
        mPlus500 = (ImageView) findViewById(R.id.plus_500);
        m500Anim = new AlphaAnimation(1.0f, 0.0f);
        m500Anim.setDuration(1000);
        m500Anim.setFillBefore(true);
        m500Anim.setFillAfter(true);

        // Get the obstacle layouts ready.  No obstacles on the first screen of a level.
        // Prime with a screen full of obstacles.
        mObstacleLayout = (LinearLayout) findViewById(R.id.obstacles_layout);
        mObstacleScroll = (HorizontalScrollView) findViewById(R.id.obstacles_scroll);

        // Initialize the present bitmaps.  These are used repeatedly so we keep them loaded.
        mGiftBoxes = new Bitmap[GIFT_BOXES.length];
        for (int i = 0; i < GIFT_BOXES.length; i++) {
            mGiftBoxes[i] = BitmapFactory.decodeResource(getResources(), GIFT_BOXES[i]);
        }

        // Add starting obstacles.  First screen has presents.  Next 3 get obstacles.
        addFirstScreenPresents();
        //        addFinalPresentRun();  // This adds 2 screens of presents
        //        addNextObstacles(0, 1);
        addNextObstacles(0, 3);

        // Setup the sound pool
        mSoundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);
        mSoundPool.setOnLoadCompleteListener(this);
        mCrashSound1 = mSoundPool.load(this, R.raw.jp_crash_1, 1);
        mCrashSound2 = mSoundPool.load(this, R.raw.jp_crash_2, 1);
        mCrashSound3 = mSoundPool.load(this, R.raw.jp_crash_3, 1);
        mGameOverSound = mSoundPool.load(this, R.raw.jp_game_over, 1);
        mJetThrustSound = mSoundPool.load(this, R.raw.jp_jet_thrust, 1);
        mLevelUpSound = mSoundPool.load(this, R.raw.jp_level_up, 1);
        mScoreBigSound = mSoundPool.load(this, R.raw.jp_score_big, 1);
        mScoreSmallSound = mSoundPool.load(this, R.raw.jp_score_small, 1);
        mJetThrustStream = 0;

        if (!mMoviePlaying) {
            doCountdown();
        }
    }

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

        Log.d(LOG_TAG, "onResume()");
    }

    @Override
    public void onPause() {
        Log.d(LOG_TAG, "onPause()");

        if (mMoviePlaying) {
            if (mIntroVideo != null) {
                // We are only here if home or lock is pressed or another app (phone)
                // interrupts.  We just go to the pause screen and start the game when
                // we come back.
                mIntroVideo.stopPlayback();
                mIntroVideo.setVisibility(View.GONE);
                mIntroControl.setOnClickListener(null);
                mIntroControl.setVisibility(View.GONE);
            }
            mMoviePlaying = false;
            mIsPlaying = true; // this will make pause() show the pause button.
        } else if (mCountdownStarted) {
            mCountdown.setVisibility(View.GONE);
            mCountDownTimer.cancel();
            mCountdownStarted = false;
            mIsPlaying = true; // this will make pause() show the pause button.
        }
        pause();

        super.onPause();
    }

    @Override
    public void onStart() {
        super.onStart();
        mInvitesFragment.getInvite(new AppInvitesFragment.GetInvitationCallback() {
            @Override
            public void onInvitation(String invitationId, String deepLink) {
                Log.d(LOG_TAG, "onInvitation:" + deepLink);
            }
        }, false);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(LOG_TAG, "onDestroy()");
        releaseResources();
    }

    private void releaseResources() {
        if (mSoundPool != null) {
            if (mBackgroundPlayer != null) {
                mBackgroundPlayer.stop();
                mBackgroundPlayer.release();
                mBackgroundPlayer = null;
            }
            if (mJetThrustStream > 0) {
                mSoundPool.stop(mJetThrustStream);
                mJetThrustStream = 0;
            }
            mSoundPool.unload(mCrashSound1);
            mSoundPool.unload(mCrashSound2);
            mSoundPool.unload(mCrashSound3);
            mSoundPool.unload(mGameOverSound);
            mSoundPool.unload(mJetThrustSound);
            mSoundPool.unload(mLevelUpSound);
            mSoundPool.unload(mScoreBigSound);
            mSoundPool.unload(mScoreSmallSound);
            mSoundPool.release();
            mSoundPool = null;
        }

        // recylce big bitmaps as soon as possible.
        releaseBitmapArray(mBackgrounds);
        releaseBitmapArray(mBackgrounds2);
        releaseBitmapArray(mEntryTransitions);
        releaseBitmapArray(mExitTransitions);
        releaseBitmapArray(mGiftBoxes);
        releaseBitmapArray(mElfBurnImages);
        releaseBitmapArray(mElfImages);
        releaseBitmapArray(mElfThrustImages);
        releaseBitmapArray(mElfHitImages);
        releaseBitmapArray(mElfSmokeImages);

        releaseIntegerBitmapMap(mWoodObstacles);
        releaseIntegerBitmapMap(mCaveObstacles);
        releaseIntegerBitmapMap(mFactoryObstacles);
    }

    @Override
    public void onConfigurationChanged(Configuration config) {
        // We are eating the config changes so that we don't get destroyed/recreated and again
        // destroyed/recreated when the lock button is pressed!
        Log.e(LOG_TAG, "Config change: " + config);
        super.onConfigurationChanged(config);
    }

    @Override
    public void onBackPressed() {
        if (mMoviePlaying) {
            if (mIntroVideo != null) {
                mIntroVideo.stopPlayback();
                mIntroVideo.setVisibility(View.GONE);
                mIntroControl.setOnClickListener(null);
                mIntroControl.setVisibility(View.GONE);
            }
            mMoviePlaying = false;
            super.onBackPressed();
        } else if (mCountdownStarted) {
            if (mCountDownTimer != null) {
                mCountDownTimer.cancel();
                mCountDownTimer = null;
            }
            mCountdownStarted = false;
            super.onBackPressed();
        } else {
            if (mIsPlaying) {
                pause();
            } else if (mIsTv) {
                finish();
            } else {
                play();
            }
        }
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_CENTER:
            //fall through
        case KeyEvent.KEYCODE_BUTTON_A:
            if (mIsPlaying) {
                mElfAccelY = mThrustAccelY;
                if (!mElfIsHit) {
                    updateElfThrust(1);
                }
                mJetThrustStream = mSoundPool.play(mJetThrustSound, 1.0f, 1.0f, 1, -1, 1.0f);
            } else if (!mCountdownStarted && !mMoviePlaying) {
                //game is paused. resume it.
                mBigPlayButton.setPressed(true);
            }
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_CENTER:
            //fall through
        case KeyEvent.KEYCODE_BUTTON_A:
            if (mIsPlaying) {
                mElfAccelY = 0.0f;
                if (!mElfIsHit) {
                    updateElfThrust(0);
                }
                if (mJetThrustStream > 0) {
                    mSoundPool.stop(mJetThrustStream);
                    mJetThrustStream = 0;
                }
            } else if (mMoviePlaying) {
                endIntro();
            } else if (mBigPlayButton.isPressed()) {
                mBigPlayButton.setPressed(false);
                mBigPlayButton.performClick();
            }
            return true;
        case KeyEvent.KEYCODE_BUTTON_B:
            onBackPressed();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mDownEvent = event;
            mElfAccelY = mThrustAccelY;
            if (!mElfIsHit) {
                updateElfThrust(1);
            }
            mJetThrustStream = mSoundPool.play(mJetThrustSound, 1.0f, 1.0f, 1, -1, 1.0f);
        } else if ((event.getActionMasked() == MotionEvent.ACTION_UP)
                || (event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
            mDownEvent = null;
            mElfAccelY = 0.0f;
            if (!mElfIsHit) {
                updateElfThrust(0);
            }
            if (mJetThrustStream > 0) {
                mSoundPool.stop(mJetThrustStream);
                mJetThrustStream = 0;
            }
        }

        //      return mGestureDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public void onClick(View view) {
        if (view == mPlayPauseButton) {
            if (mIsPlaying) {
                pause();
            } else {
                play();
            }
        } else if (view == mBigPlayButton) {
            if (!mIsPlaying) {
                mBigPlayButtonLayout.setVisibility(View.GONE);
                doCountdown();
            }
        } else if (view == mExit) {
            finish();
        }
    }

    @Override
    public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        endIntro();
    }

    private void endIntro() {
        mMoviePlaying = false;
        mIntroControl.setOnClickListener(null);
        mIntroControl.setVisibility(View.GONE);
        mIntroVideo.setVisibility(View.GONE);
        doCountdown();
    }

    private void processFrame() {
        long newTime = System.currentTimeMillis();
        long time = newTime - mLastTime;

        boolean end = false;

        if (time > 60) {
            Log.e("LONG", "Frame time took too long! Time: " + time + " Last process frame: " + mLastFrameTime
                    + " Count: " + mBackgroundCount + " Level: " + mLevel);
        }

        // We don't want to jump too far so, if real time is > 60 treat it as 33.  On screen will seem to slow
        // down instaead of "jump"
        if (time > 60) {
            time = 33;
        }

        // Score is based on time + presents.  Right now 100 point per second played.  No presents yet
        if (mLevel < 6) {
            mScore += time;
        }

        if (mIsTv) {
            mScoreText.setText(mScoreLabel + ": " + NumberFormat.getNumberInstance().format((mScore / 10)));
        } else {
            mScoreText.setText(NumberFormat.getNumberInstance().format((mScore / 10)));
        }

        float scroll = mElfVelX * time;

        // Do collision detection first...
        // The elf can't collide if it is within 2 seconds of colliding previously.
        if (mElfIsHit) {
            if ((newTime - mElfHitTime) > 2000) {
                // Move to next state.
                if (mElfState < 4) {
                    mElfState++;
                    AnalyticsManager.sendEvent(getString(R.string.analytics_screen_rocket),
                            getString(R.string.analytics_action_rocket_hit), null, mElfState);
                    if (mElfState == 4) {
                        mSoundPool.play(mGameOverSound, 1.0f, 1.0f, 2, 0, 1.0f);
                        // No more control...
                        mControlView.setOnTouchListener(null);
                        mElfAccelY = 0.0f;
                        if (mJetThrustStream != 0) {
                            mSoundPool.stop(mJetThrustStream);
                        }
                    }
                }
                updateElf(false);
                mElfIsHit = false;
            }
        } else if (mElfState == 4) {
            // Don't do any collision detection for parachute elf.  Just let him fall...
        } else {
            // Find the obstacle(s) we might be colliding with.  It can only be one of the first 3 obstacles.
            for (int i = 0; i < 3; i++) {
                View view = mObstacleLayout.getChildAt(i);
                if (view == null) {
                    // No more obstacles...
                    break;
                }

                int[] tmp = new int[2];
                view.getLocationOnScreen(tmp);

                // If the start of this view is past the center of the elf, we are done
                if (tmp[0] > mElfPosX) {
                    break;
                }

                if (RelativeLayout.class.isInstance(view)) {
                    // this is an obstacle layout.
                    View topView = view.findViewById(R.id.top_view);
                    View bottomView = view.findViewById(R.id.bottom_view);
                    if ((topView != null) && topView.getVisibility() == View.VISIBLE) {
                        topView.getLocationOnScreen(tmp);
                        Rect obsRect = new Rect(tmp[0], tmp[1], tmp[0] + topView.getWidth(),
                                tmp[1] + topView.getHeight());
                        if (obsRect.contains((int) mElfPosX, (int) mElfPosY + mElfBitmap.getHeight() / 2)) {
                            handleCollision();
                        }
                    }
                    if (!mElfIsHit) {
                        if ((bottomView != null) && bottomView.getVisibility() == View.VISIBLE) {
                            bottomView.getLocationOnScreen(tmp);
                            Rect obsRect = new Rect(tmp[0], tmp[1], tmp[0] + bottomView.getWidth(),
                                    tmp[1] + bottomView.getHeight());
                            if (obsRect.contains((int) mElfPosX, (int) mElfPosY + mElfBitmap.getHeight() / 2)) {
                                // Special case for the mammoth obstacle...
                                if (bottomView.getTag() != null) {
                                    if (((mElfPosX - tmp[0]) / (float) bottomView.getWidth()) > 0.25f) {
                                        // We are over the mammoth not the spike.  lower the top of the rect and test again.
                                        obsRect.top = (int) (tmp[1] + ((float) bottomView.getHeight() * 0.18f));
                                        if (obsRect.contains((int) mElfPosX,
                                                (int) mElfPosY + mElfBitmap.getHeight() / 2)) {
                                            handleCollision();
                                        }
                                    }
                                } else {
                                    handleCollision();
                                }
                            }
                        }
                    }
                } else if (FrameLayout.class.isInstance(view)) {
                    // Present view
                    FrameLayout frame = (FrameLayout) view;
                    if (frame.getChildCount() > 0) {
                        ImageView presentView = (ImageView) frame.getChildAt(0);
                        presentView.getLocationOnScreen(tmp);
                        Rect presentRect = new Rect(tmp[0], tmp[1], tmp[0] + presentView.getWidth(),
                                tmp[1] + presentView.getHeight());
                        mElfLayout.getLocationOnScreen(tmp);
                        Rect elfRect = new Rect(tmp[0], tmp[1], tmp[0] + mElfLayout.getWidth(),
                                tmp[1] + mElfLayout.getHeight());
                        if (elfRect.intersect(presentRect)) {
                            // We got a present!
                            mPresentCount++;
                            if (mPresentCount < 4) {
                                mSoundPool.play(mScoreSmallSound, 1.0f, 1.0f, 2, 0, 1.0f);
                                mScore += 1000; // 100 points.  Score is 10x displayed score.
                                mPlus100.setVisibility(View.VISIBLE);
                                if (mElfPosY > (mScreenHeight / 2)) {
                                    mPlus100.setY(mElfPosY - (mElfLayout.getHeight() + mPlus100.getHeight()));
                                } else {
                                    mPlus100.setY(mElfPosY + mElfLayout.getHeight());
                                }
                                mPlus100.setX(mElfPosX);
                                if (m100Anim.hasStarted()) {
                                    m100Anim.reset();
                                }
                                mPlus100.startAnimation(m100Anim);
                            } else {
                                mSoundPool.play(mScoreBigSound, 1.0f, 1.0f, 2, 0, 1.0f);
                                mScore += 5000; // 500 points.  Score is 10x displayed score.
                                if (!mRainingPresents) {
                                    mPresentCount = 0;
                                }
                                mPlus500.setVisibility(View.VISIBLE);
                                if (mElfPosY > (mScreenHeight / 2)) {
                                    mPlus500.setY(mElfPosY - (mElfLayout.getHeight() + mPlus100.getHeight()));
                                } else {
                                    mPlus500.setY(mElfPosY + mElfLayout.getHeight());
                                }
                                mPlus500.setX(mElfPosX);
                                if (m500Anim.hasStarted()) {
                                    m500Anim.reset();
                                }
                                mPlus500.startAnimation(m500Anim);
                                mPresentBonus = true;
                            }
                            frame.removeView(presentView);
                        } else if (elfRect.left > presentRect.right) {
                            mPresentCount = 0;
                        }
                    }
                }
            }
        }

        if (mForegroundLayout.getChildCount() > 0) {
            int currentX = mForegroundScroll.getScrollX();
            View view = mForegroundLayout.getChildAt(0);
            int newX = currentX + (int) scroll;
            if (newX > view.getWidth()) {
                newX -= view.getWidth();
                mForegroundLayout.removeViewAt(0);
            }
            mForegroundScroll.setScrollX(newX);
        }

        // Scroll obstacle views
        if (mObstacleLayout.getChildCount() > 0) {
            int currentX = mObstacleScroll.getScrollX();
            View view = mObstacleLayout.getChildAt(0);
            int newX = currentX + (int) scroll;
            if (newX > view.getWidth()) {
                newX -= view.getWidth();
                mObstacleLayout.removeViewAt(0);
            }
            mObstacleScroll.setScrollX(newX);
        }

        // Scroll the background and foreground
        if (mBackgroundLayout.getChildCount() > 0) {
            int currentX = mBackgroundScroll.getScrollX();
            View view = mBackgroundLayout.getChildAt(0);
            int newX = currentX + (int) scroll;
            if (newX > view.getWidth()) {
                newX -= view.getWidth();
                mBackgroundLayout.removeViewAt(0);
                if (view.getTag() != null) {
                    Pair<Integer, Integer> pair = (Pair<Integer, Integer>) view.getTag();
                    int type = pair.first;
                    int level = pair.second;
                    if (type == 0) {
                        if (mBackgrounds[level] != null) {
                            mBackgrounds[level].recycle();
                            mBackgrounds[level] = null;
                        } else if (mBackgrounds2[level] != null) {
                            mBackgrounds2[level].recycle();
                            mBackgrounds2[level] = null;
                        }
                    } else if (type == 1) {
                        if (mExitTransitions[level] != null) {
                            mExitTransitions[level].recycle();
                            mExitTransitions[level] = null;
                        }
                    } else if (type == 2) {
                        if (mEntryTransitions[level] != null) {
                            mEntryTransitions[level].recycle();
                            mEntryTransitions[level] = null;
                        }
                    }
                }
                if (mBackgroundCount == 5) {
                    if (mLevel < 6) {
                        // Pre-fetch next levels backgrounds
                        // end level uses the index 1 background...
                        int level = (mLevel == 5) ? 1 : (mLevel + 1);
                        BackgroundLoadTask task = new BackgroundLoadTask(getResources(), mLevel + 1,
                                BACKGROUNDS[level], EXIT_TRANSITIONS[mLevel],
                                // Exit transitions are for the current level...
                                ENTRY_TRANSITIONS[level], mScaleX, mScaleY, mBackgrounds, mBackgrounds2,
                                mExitTransitions, mEntryTransitions, mScreenWidth, mScreenHeight);
                        task.execute();
                        addNextImages(mLevel, true);
                        addNextObstacles(mLevel, 2);
                    }
                    // Fetch first set of obstacles if the next level changes from woods to cave or cave to factory
                    if (mLevel == 1) {
                        // Next level will be caves.  Get bitmaps for the first 20 obstacles.
                        ObstacleLoadTask task = new ObstacleLoadTask(getResources(), CAVE_OBSTACLES, mCaveObstacles,
                                mCaveObstacleList, 0, 2, mScaleX, mScaleY);
                        task.execute();
                    } else if (mLevel == 3) {
                        // Next level will be factory.  Get bitmaps for the first 20 obstacles.
                        ObstacleLoadTask task = new ObstacleLoadTask(getResources(), FACTORY_OBSTACLES,
                                mFactoryObstacles, mFactoryObstacleList, 0, 2, mScaleX, mScaleY);
                        task.execute();
                    }
                    mBackgroundCount++;
                } else if (mBackgroundCount == 7) {
                    // Add transitions and/or next level
                    if (mLevel < 5) {
                        addNextTransitionImages(mLevel + 1);
                        if (mTransitionImagesCount > 0) {
                            addNextObstacleSpacer(mTransitionImagesCount);
                        }
                        addNextImages(mLevel + 1);
                        // First screen of each new level has no obstacles
                        if ((mLevel % 2) == 1) {
                            addNextObstacleSpacer(1);
                            addNextObstacles(mLevel + 1, 1);
                        } else {
                            addNextObstacles(mLevel + 1, 2);
                        }
                    } else if (mLevel == 5) {
                        addNextTransitionImages(mLevel + 1);
                        if (mTransitionImagesCount > 0) {
                            addNextObstacleSpacer(mTransitionImagesCount);
                        }
                        addFinalImages();
                    }
                    mBackgroundCount++;
                } else if (mBackgroundCount == 9) {
                    // Either the transition or the next level is showing
                    if (this.mTransitionImagesCount > 0) {
                        mTransitionImagesCount--;
                    } else {
                        if (mLevel == 1) {
                            // Destroy the wood obstacle bitmaps
                            Thread thread = new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    synchronized (mWoodObstacles) {
                                        for (Bitmap bmp : mWoodObstacles.values()) {
                                            bmp.recycle();
                                        }
                                        mWoodObstacles.clear();
                                    }
                                }
                            });
                            thread.start();
                        } else if (mLevel == 3) {
                            // Destroy the cave obstacle bitmaps
                            Thread thread = new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    synchronized (mCaveObstacles) {
                                        for (Bitmap bmp : mCaveObstacles.values()) {
                                            bmp.recycle();
                                        }
                                        mCaveObstacles.clear();
                                    }
                                }
                            });
                            thread.start();
                        } else if (mLevel == 5) {
                            // Destroy the factory obstacle bitmaps
                            Thread thread = new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    synchronized (mFactoryObstacles) {
                                        for (Bitmap bmp : mFactoryObstacles.values()) {
                                            bmp.recycle();
                                        }
                                        mFactoryObstacles.clear();
                                    }
                                }
                            });
                            thread.start();
                        }
                        mLevel++;

                        // Add an event for clearing this level - note we don't increment mLevel as
                        // it's 0-based and we're tracking the previous level.
                        AnalyticsManager.sendEvent(getString(R.string.analytics_screen_rocket),
                                getString(R.string.analytics_action_rocket_level), null, mLevel);

                        // Achievements
                        if (!mHitLevel) {
                            mCleanLevel = true;
                        }
                        mHitLevel = false;
                        if (mLevel == 5) {
                            mPlus100.setSelected(true);
                            mPlus500.setSelected(true);
                        } else if (mLevel == 6) {
                            mPlus100.setSelected(false);
                            mPlus500.setSelected(false);
                        }
                        if (mLevel < 6) {
                            mSoundPool.play(mLevelUpSound, 1.0f, 1.0f, 2, 0, 1.0f);
                            addNextImages(mLevel);
                            addNextObstacles(mLevel, 2);
                        }
                        mBackgroundCount = 0;
                    }
                } else {
                    if ((mBackgroundCount % 2) == 1) {
                        if (mLevel < 6) {
                            addNextImages(mLevel);
                            addNextObstacles(mLevel, 2);
                        }
                    }
                    mBackgroundCount++;
                }
            }
            int current = mBackgroundScroll.getScrollX();
            mBackgroundScroll.setScrollX(newX);
            if ((mLevel == 6) && (mBackgroundScroll.getScrollX() == current)) {
                end = true;
            }
        }

        // Check on the elf
        boolean hitBottom = false;
        boolean hitTop = false;

        float deltaY = mElfVelY * time;
        mElfPosY = mElfLayout.getY() + deltaY;
        if (mElfPosY < 0.0f) {
            mElfPosY = 0.0f;
            mElfVelY = 0.0f;
            hitTop = true;
        } else if (mElfPosY > (mScreenHeight - mElfLayout.getHeight())) {
            mElfPosY = mScreenHeight - mElfLayout.getHeight();
            mElfVelY = 0.0f;
            hitBottom = true;
        } else {
            // Remember -Y is up!
            mElfVelY += (mGravityAccelY * time - mElfAccelY * time);
        }
        mElfLayout.setY(mElfPosY);

        // Rotate the elf to indicate thrust, dive.
        float rot = (float) (Math.atan(mElfVelY / mElfVelX) * 120.0 / Math.PI);
        mElfLayout.setRotation(rot);

        mElf.invalidate();

        // Update the time and spawn the next call to processFrame.
        mLastTime = newTime;
        mLastFrameTime = System.currentTimeMillis() - newTime;
        if (!end) {
            if ((mElfState < 4) || !hitBottom) {
                if (mLastFrameTime < 16) {
                    mHandler.postDelayed(mGameLoop, 16 - mLastFrameTime);
                } else {
                    mHandler.post(mGameLoop);
                }
            } else {
                endGame();
            }
        } else {
            // Whatever the final stuff is, do it here.
            mPlayPauseButton.setEnabled(false);
            mPlayPauseButton.setVisibility(View.INVISIBLE);
            endGame();
        }
    }

    private void handleCollision() {
        // Achievements
        mHit = true;
        mHitLevel = true;

        // Collision!
        mElfIsHit = true;
        mElfHitTime = System.currentTimeMillis();
        updateElf(true);
        mVibrator.vibrate(500);
        if (mElfState == 0) {
            mSoundPool.play(mCrashSound1, 1.0f, 1.0f, 2, 0, 1.0f);
        } else if (mElfState == 1) {
            mSoundPool.play(mCrashSound2, 1.0f, 1.0f, 2, 0, 1.0f);
        } else if (mElfState == 2) {
            mSoundPool.play(mCrashSound3, 1.0f, 1.0f, 2, 0, 1.0f);
        } else if (mElfState == 3) {
            mSoundPool.play(mCrashSound3, 1.0f, 1.0f, 2, 0, 1.0f);
        }
    }

    private void doCountdown() {
        mCountdownStarted = true;
        mPlayPauseButton.setEnabled(false);
        // Start the countdown
        if (mCountdown == null) {
            mCountdown = (TextView) findViewById(R.id.countdown_text);
        }
        mCountdown.setVisibility(View.VISIBLE);
        mCountdown.setTextColor(Color.WHITE);
        mCountdown.setText("3");
        mCountDownTimer = new CountDownTimer(3500, 100) {
            @Override
            public void onTick(long millisUntilFinished) {
                int time = (int) ((millisUntilFinished + 500) / 1000);
                if (time == 3) {
                    mCountdown.setText("3");
                } else if (time == 2) {
                    mCountdown.setText("2");
                } else if (time == 1) {
                    mCountdown.setText("1");
                } else if (time == 0) {
                    mCountdown.setText("Go!");
                }
            }

            @Override
            public void onFinish() {
                mCountdownStarted = false;
                mPlayPauseButton.setEnabled(true);
                play();
                mCountdown.setVisibility(View.GONE);
            }
        };
        mCountDownTimer.start();
    }

    private int mLastObstacle = 0;
    // 0 - spacer, 1 - upper obstacle, 2 - lower obstacle.  These are flags so top + bottom is 3.

    private void addFirstScreenPresents() {
        // First 4 slots have no nothing.
        for (int i = 0; i < Math.min(4, SLOTS_PER_SCREEN); i++) {
            View view = new View(this);
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mSlotWidth, mScreenHeight);
            mObstacleLayout.addView(view, lp);
        }

        // Generate a SIN like pattern;
        float center = (float) ((mScreenHeight - mGiftBoxes[0].getHeight()) / 2);
        float presentHeight = (float) mGiftBoxes[0].getHeight();
        float[] heights = new float[] { center, center - presentHeight, center - (1.5f * presentHeight),
                center - presentHeight, center, center + presentHeight, center + (1.5f * presentHeight),
                center + presentHeight, center };
        // Add presents to the end
        if (SLOTS_PER_SCREEN > 4) {
            for (int i = 0; i < (SLOTS_PER_SCREEN - 4); i++) {
                // Which one?
                Bitmap bmp = mGiftBoxes[mRandom.nextInt(mGiftBoxes.length)];
                ImageView iv = new ImageView(this);
                iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                iv.setImageBitmap(bmp);

                // Position the present
                float left = (mSlotWidth - bmp.getWidth()) / 2;
                float top = heights[(i % heights.length)];

                FrameLayout frame = new FrameLayout(this);
                LayoutParams flp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                frame.addView(iv, flp);
                iv.setTranslationX(left);
                iv.setTranslationY(top);

                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mSlotWidth,
                        LinearLayout.LayoutParams.MATCH_PARENT);
                mObstacleLayout.addView(frame, lp);
            }
        }

        // Account for rounding errors in mSlotWidth
        int extra = (mScreenWidth - (SLOTS_PER_SCREEN * mSlotWidth));
        if (extra > 0) {
            // Add filler to ensure sync with background/foreground scrolls!
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(extra,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            View view = new View(this);
            mObstacleLayout.addView(view, lp);
        }

        mLastObstacle = 0;
    }

    private void addFinalPresentRun() {
        // Two spacers at the begining.
        View view = new View(this);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mSlotWidth, mScreenHeight);
        mObstacleLayout.addView(view, lp);
        view = new View(this);
        mObstacleLayout.addView(view, lp);

        // All of these presents are 500 points (but only if you're awesome)
        if (mElfState == 0) {
            mRainingPresents = true;
        }

        // SIN wave of presents in the middle
        float center = (float) (mScreenHeight / 2);
        float amplitude = (float) (mScreenHeight / 4);

        int count = (3 * SLOTS_PER_SCREEN) - 4;

        for (int i = 0; i < count; i++) {
            float x = (float) ((mSlotWidth - mGiftBoxes[0].getWidth()) / 2);
            float y = center + (amplitude * (float) Math.sin(2.0 * Math.PI * (double) i / (double) count));
            Bitmap bmp = mGiftBoxes[mRandom.nextInt(mGiftBoxes.length)];
            ImageView iv = new ImageView(this);
            iv.setImageBitmap(bmp);
            iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);

            FrameLayout frame = new FrameLayout(this);
            LayoutParams flp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            frame.addView(iv, flp);
            iv.setTranslationX(x);
            iv.setTranslationY(y);
            mObstacleLayout.addView(frame, lp);
        }

        // Two spacers at the end.
        view = new View(this);
        mObstacleLayout.addView(view, lp);
        view = new View(this);
        mObstacleLayout.addView(view, lp);

        // Account for rounding errors in mSlotWidth
        int extra = ((3 * mScreenWidth) - (3 * SLOTS_PER_SCREEN * mSlotWidth));
        if (extra > 0) {
            // Add filler to ensure sync with background/foreground scrolls!
            lp = new LinearLayout.LayoutParams(extra, LinearLayout.LayoutParams.MATCH_PARENT);
            view = new View(this);
            mObstacleLayout.addView(view, lp);
        }
    }

    // Pre-populate the random list of obstacles so we can background fetch them.
    private void initObstacles(int[] resources, int stride, ArrayList<Integer> list) {
        // Select 200 obstacles randomly.  This should be enough for each type of obstacles.
        // We will wrap if we need more.
        for (int i = 0; i < 200; i++) {
            list.add(mRandom.nextInt(resources.length / stride));
        }
    }

    // Pre-populate the random list of obstacles and pre-load some of the bitmaps.
    private void initObstaclesAndPreLoad(int[] resources, int stride, TreeMap<Integer, Bitmap> map,
            ArrayList<Integer> list) {
        initObstacles(resources, stride, list);
        // Load the bitmaps for the first 20 obstacles.
        for (int i = 0; i < 20; i++) {
            int obstacle = list.get(i);
            for (int j = (obstacle * stride); j < ((obstacle + 1) * stride); j++) {
                // Check just in case something is wonky
                if (j < resources.length) {
                    int id = resources[j];
                    if (id != -1) {
                        // Only need to load it once...
                        if (!map.containsKey(id)) {
                            Bitmap bmp = BitmapFactory.decodeResource(getResources(), id);
                            if ((mScaleX != 1.0f) || (mScaleY != 1.0f)) {
                                Bitmap tmp = Bitmap.createScaledBitmap(bmp,
                                        (int) ((float) bmp.getWidth() * mScaleX),
                                        (int) ((float) bmp.getHeight() * mScaleY), false);
                                if (tmp != bmp) {
                                    bmp.recycle();
                                }
                                map.put(id, tmp);
                            } else {
                                map.put(id, bmp);
                            }
                        }
                    }
                }
            }
        }
    }

    private void addNextObstacleSpacer(int screens) {
        if (screens > 0) {
            View view = new View(this);
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mScreenWidth * screens, mScreenHeight);
            mObstacleLayout.addView(view, lp);
            mLastObstacle = 0;
        }
    }

    private void addNextObstacles(int level, int screens) {
        if (level < 2) {
            addWoodObstacles(screens);
        } else if (level < 4) {
            addCaveObstacles(screens);
        } else {
            addFactoryObstacles(screens);
        }
    }

    private void addWoodObstacles(int screens) {
        int totalSlots = screens * SLOTS_PER_SCREEN;
        for (int i = 0; i < totalSlots;) {
            // Any given "slot" has a 1 in 3 chance of having an obstacle
            if (mRandom.nextInt(3) == 0) {
                View view = mInflater.inflate(R.layout.obstacle_layout, null);
                ImageView top = (ImageView) view.findViewById(R.id.top_view);
                ImageView bottom = (ImageView) view.findViewById(R.id.bottom_view);
                ImageView back = (ImageView) view.findViewById(R.id.back_view);

                // Which obstacle?
                int width = 0;
                //            int obstacle = mRandom.nextInt((WOOD_OBSTACLES.length/3));
                if ((mWoodObstacleIndex % 20) == 0) {
                    ObstacleLoadTask task = new ObstacleLoadTask(getResources(), WOOD_OBSTACLES, mWoodObstacles,
                            mWoodObstacleList, mWoodObstacleIndex + 20, 3, mScaleX, mScaleY);
                    task.execute();
                }
                int obstacle = mWoodObstacleList.get(mWoodObstacleIndex++);
                if (mWoodObstacleIndex >= mWoodObstacleList.size()) {
                    mWoodObstacleIndex = 0;
                }
                int topIndex = obstacle * 3;
                int bottomIndex = topIndex + 1;
                int backIndex = topIndex + 2;
                if (WOOD_OBSTACLES[backIndex] != -1) {
                    Bitmap bmp = null;
                    synchronized (mWoodObstacles) {
                        bmp = mWoodObstacles.get(WOOD_OBSTACLES[backIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mWoodObstacles) {
                            bmp = mWoodObstacles.get(WOOD_OBSTACLES[backIndex]);
                            if (bmp == null) {
                                try {
                                    mWoodObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    width = bmp.getWidth();
                    back.setImageBitmap(bmp);
                } else {
                    back.setVisibility(View.GONE);
                }

                int currentObstacle = 0; // Same values as mLastObstacle
                if (WOOD_OBSTACLES[topIndex] != -1) {
                    currentObstacle |= 1;
                    Bitmap bmp = null;
                    synchronized (mWoodObstacles) {
                        bmp = mWoodObstacles.get(WOOD_OBSTACLES[topIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mWoodObstacles) {
                            bmp = mWoodObstacles.get(WOOD_OBSTACLES[topIndex]);
                            if (bmp == null) {
                                try {
                                    mWoodObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    width = bmp.getWidth();
                    top.setImageBitmap(bmp);
                } else {
                    top.setVisibility(View.GONE);
                }

                if (WOOD_OBSTACLES[bottomIndex] != -1) {
                    currentObstacle |= 2;
                    Bitmap bmp = null;
                    synchronized (mWoodObstacles) {
                        bmp = mWoodObstacles.get(WOOD_OBSTACLES[bottomIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mWoodObstacles) {
                            bmp = mWoodObstacles.get(WOOD_OBSTACLES[bottomIndex]);
                            if (bmp == null) {
                                try {
                                    mWoodObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    if (bmp.getWidth() > width) {
                        width = bmp.getWidth();
                    }
                    bottom.setImageBitmap(bmp);
                } else {
                    bottom.setVisibility(View.GONE);
                }
                int slots = (width / mSlotWidth) + 2;

                // If last obstacle had a top and this is a bottom or vice versa, insert a space
                if ((mLastObstacle & 0x1) > 0) {
                    if ((currentObstacle & 0x2) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                } else if ((mLastObstacle & 0x2) > 0) {
                    if ((currentObstacle & 0x1) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                }

                // If the new obstacle is too wide for the remaining space, skip it and fill spacer instead
                if ((i + slots) > totalSlots) {
                    addSpaceOrPresent(mSlotWidth * (totalSlots - i));
                    i = totalSlots;
                } else {
                    mLastObstacle = currentObstacle;
                    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(slots * mSlotWidth,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    view.setLayoutParams(lp);
                    mObstacleLayout.addView(view);
                    i += slots;
                }
            } else {
                addSpaceOrPresent(mSlotWidth);
                i++;
            }
        }

        // Account for rounding errors in mSlotWidth
        int extra = ((screens * mScreenWidth) - (totalSlots * mSlotWidth));
        if (extra > 0) {
            // Add filler to ensure sync with background/foreground scrolls!
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(extra,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            View view = new View(this);
            mObstacleLayout.addView(view, lp);
        }
    }

    private void addCaveObstacles(int screens) {
        int totalSlots = screens * SLOTS_PER_SCREEN;
        for (int i = 0; i < totalSlots;) {
            // Any given "slot" has a 1 in 3 chance of having an obstacle
            if (mRandom.nextInt(2) == 0) {
                View view = mInflater.inflate(R.layout.obstacle_layout, null);
                ImageView top = (ImageView) view.findViewById(R.id.top_view);
                ImageView bottom = (ImageView) view.findViewById(R.id.bottom_view);
                ImageView back = (ImageView) view.findViewById(R.id.back_view);

                // Which obstacle?
                int width = 0;
                if ((mCaveObstacleIndex % 20) == 0) {
                    ObstacleLoadTask task = new ObstacleLoadTask(getResources(), CAVE_OBSTACLES, mCaveObstacles,
                            mCaveObstacleList, mCaveObstacleIndex + 20, 2, mScaleX, mScaleY);
                    task.execute();
                }
                int obstacle = mCaveObstacleList.get(mCaveObstacleIndex++);
                if (mCaveObstacleIndex >= mCaveObstacleList.size()) {
                    mCaveObstacleIndex = 0;
                }
                //                int obstacle = mRandom.nextInt((CAVE_OBSTACLES.length/2));
                int topIndex = obstacle * 2;
                int bottomIndex = topIndex + 1;
                back.setVisibility(View.GONE);

                int currentObstacle = 0; // Same values as mLastObstacle
                if (CAVE_OBSTACLES[topIndex] != -1) {
                    currentObstacle |= 1;
                    Bitmap bmp = null;
                    synchronized (mCaveObstacles) {
                        bmp = mCaveObstacles.get(CAVE_OBSTACLES[topIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mCaveObstacles) {
                            bmp = mCaveObstacles.get(CAVE_OBSTACLES[topIndex]);
                            if (bmp == null) {
                                try {
                                    mCaveObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    width = bmp.getWidth();
                    top.setImageBitmap(bmp);
                } else {
                    top.setVisibility(View.GONE);
                }

                if (CAVE_OBSTACLES[bottomIndex] != -1) {
                    currentObstacle |= 2;
                    Bitmap bmp = null;
                    synchronized (mCaveObstacles) {
                        bmp = mCaveObstacles.get(CAVE_OBSTACLES[bottomIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mCaveObstacles) {
                            bmp = mCaveObstacles.get(CAVE_OBSTACLES[bottomIndex]);
                            if (bmp == null) {
                                try {
                                    mCaveObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    if (bmp.getWidth() > width) {
                        width = bmp.getWidth();
                    }
                    bottom.setImageBitmap(bmp);
                    if (CAVE_OBSTACLES[bottomIndex] == R.drawable.img_mammoth) {
                        // Special case...
                        bottom.setTag(true);
                    }
                } else {
                    bottom.setVisibility(View.GONE);
                }
                int slots = (width / mSlotWidth);
                slots += 2;

                // If last obstacle had a top and this is a bottom or vice versa, insert a space
                if ((mLastObstacle & 0x1) > 0) {
                    if ((currentObstacle & 0x2) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                } else if ((mLastObstacle & 0x2) > 0) {
                    if ((currentObstacle & 0x1) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                }

                // If the new obstacle is too wide for the remaining space, skip it and fill spacer instead
                if ((i + slots) > totalSlots) {
                    addSpaceOrPresent(mSlotWidth * (totalSlots - i));
                    i = totalSlots;
                } else {
                    mLastObstacle = currentObstacle;
                    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(slots * mSlotWidth,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    view.setLayoutParams(lp);
                    mObstacleLayout.addView(view);
                    i += slots;
                }
            } else {
                addSpaceOrPresent(mSlotWidth);
                i++;
            }
        }

        // Account for rounding errors in mSlotWidth
        int extra = ((screens * mScreenWidth) - (totalSlots * mSlotWidth));
        if (extra > 0) {
            // Add filler to ensure sync with background/foreground scrolls!
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(extra,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            View view = new View(this);
            mObstacleLayout.addView(view, lp);
        }
    }

    private void addFactoryObstacles(int screens) {
        int totalSlots = screens * SLOTS_PER_SCREEN;
        for (int i = 0; i < totalSlots;) {
            // Any given "slot" has a 1 in 3 chance of having an obstacle
            if (mRandom.nextInt(2) == 0) {
                View view = mInflater.inflate(R.layout.obstacle_layout, null);
                ImageView top = (ImageView) view.findViewById(R.id.top_view);
                ImageView bottom = (ImageView) view.findViewById(R.id.bottom_view);
                ImageView back = (ImageView) view.findViewById(R.id.back_view);

                // Which obstacle?
                int width = 0;
                //                int obstacle = mRandom.nextInt((FACTORY_OBSTACLES.length/2));
                if ((mFactoryObstacleIndex % 20) == 0) {
                    ObstacleLoadTask task = new ObstacleLoadTask(getResources(), FACTORY_OBSTACLES,
                            mFactoryObstacles, mFactoryObstacleList, mFactoryObstacleIndex + 20, 2, mScaleX,
                            mScaleY);
                    task.execute();
                }
                int obstacle = mFactoryObstacleList.get(mFactoryObstacleIndex++);
                if (mFactoryObstacleIndex >= mFactoryObstacleList.size()) {
                    mFactoryObstacleIndex = 0;
                }
                int topIndex = obstacle * 2;
                int bottomIndex = topIndex + 1;
                back.setVisibility(View.GONE);

                int currentObstacle = 0; // Same values as mLastObstacle
                if (FACTORY_OBSTACLES[topIndex] != -1) {
                    currentObstacle |= 1;
                    Bitmap bmp = null;
                    synchronized (mFactoryObstacles) {
                        bmp = mFactoryObstacles.get(FACTORY_OBSTACLES[topIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mFactoryObstacles) {
                            bmp = mFactoryObstacles.get(FACTORY_OBSTACLES[topIndex]);
                            if (bmp == null) {
                                try {
                                    mFactoryObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    width = bmp.getWidth();
                    top.setImageBitmap(bmp);
                } else {
                    top.setVisibility(View.GONE);
                }

                if (FACTORY_OBSTACLES[bottomIndex] != -1) {
                    currentObstacle |= 2;
                    Bitmap bmp = null;
                    synchronized (mFactoryObstacles) {
                        bmp = mFactoryObstacles.get(FACTORY_OBSTACLES[bottomIndex]);
                    }
                    while (bmp == null) {
                        synchronized (mFactoryObstacles) {
                            bmp = mFactoryObstacles.get(FACTORY_OBSTACLES[bottomIndex]);
                            if (bmp == null) {
                                try {
                                    mFactoryObstacles.wait();
                                } catch (InterruptedException e) {
                                }
                            }
                        }
                    }
                    if (bmp.getWidth() > width) {
                        width = bmp.getWidth();
                    }
                    bottom.setImageBitmap(bmp);
                } else {
                    bottom.setVisibility(View.GONE);
                }
                int slots = (width / mSlotWidth);
                if ((width % mSlotWidth) != 0) {
                    slots++;
                }

                // If last obstacle had a top and this is a bottom or vice versa, insert a space
                if ((mLastObstacle & 0x1) > 0) {
                    if ((currentObstacle & 0x2) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                } else if ((mLastObstacle & 0x2) > 0) {
                    if ((currentObstacle & 0x1) > 0) {
                        addSpaceOrPresent(mSlotWidth);
                        i++;
                    }
                }

                // If the new obstacle is too wide for the remaining space, skip it and fill spacer instead
                if ((i + slots) > totalSlots) {
                    addSpaceOrPresent(mSlotWidth * (totalSlots - i));
                    i = totalSlots;
                } else {
                    mLastObstacle = currentObstacle;
                    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(slots * mSlotWidth,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    view.setLayoutParams(lp);
                    mObstacleLayout.addView(view);
                    i += slots;
                }
            } else {
                addSpaceOrPresent(mSlotWidth);
                i++;
            }
        }

        // Account for rounding errors in mSlotWidth
        int extra = ((screens * mScreenWidth) - (totalSlots * mSlotWidth));
        if (extra > 0) {
            // Add filler to ensure sync with background/foreground scrolls!
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(extra,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            View view = new View(this);
            view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            mObstacleLayout.addView(view, lp);
        }
    }

    private void addSpaceOrPresent(int width) {
        if (width > 0) {
            mLastObstacle = 0;
            // 1/3 chance of a present.
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(width,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            if (mRandom.nextInt(3) == 0) {
                // Present!

                // Which one?
                Bitmap bmp = mGiftBoxes[mRandom.nextInt(mGiftBoxes.length)];
                ImageView iv = new ImageView(this);
                iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                iv.setImageBitmap(bmp);

                // Position the present
                int left = mRandom.nextInt(width / 2) + (width / 4)
                        - ((int) ((float) bmp.getWidth() * mScaleX) / 2);
                int top = mRandom.nextInt(mScreenHeight / 2) + (mScreenHeight / 4)
                        - ((int) ((float) bmp.getHeight() * mScaleY) / 2);

                FrameLayout frame = new FrameLayout(this);
                LayoutParams flp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                frame.addView(iv, flp);
                iv.setTranslationX(left);
                iv.setTranslationY(top);

                mObstacleLayout.addView(frame, lp);
            } else {
                // Space
                View view = new View(this);
                mObstacleLayout.addView(view, lp);
            }
        }
    }

    private void addNextImages(int level) {
        addNextImages(level, false);
    }

    private void addNextImages(int level, boolean recycle) {
        if (level < BACKGROUNDS.length) {
            // Add the background image
            ImageView iv = new ImageView(this);
            iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            // This is being background loaded.  Should already be loaded, but if not, wait.
            while (mBackgrounds[level] == null) {
                synchronized (mBackgrounds) {
                    if (mBackgrounds[level] == null) {
                        try {
                            mBackgrounds.wait();
                        } catch (InterruptedException e) {
                        }
                    }
                }
            }
            iv.setImageBitmap(mBackgrounds[level]);
            if (recycle) {
                iv.setTag(new Pair<Integer, Integer>(0, level));
            }
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            mBackgroundLayout.addView(iv, lp);
            iv = new ImageView(this);
            iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);

            if (recycle) {
                iv.setTag(new Pair<Integer, Integer>(0, level));
            }
            iv.setImageBitmap(mBackgrounds2[level]);
            mBackgroundLayout.addView(iv, lp);

            // Add the foreground image
            if (FOREGROUNDS[level] == -1) {
                View view = new View(this);
                lp = new LinearLayout.LayoutParams(mScreenWidth * 2, 10);
                mForegroundLayout.addView(view, lp);
            } else {
                iv = new ImageView(this);
                iv.setBackgroundResource(R.drawable.img_snow_ground_tiles);
                if (recycle) {
                    iv.setTag(level);
                }
                lp = new LinearLayout.LayoutParams(mScreenWidth * 2, LinearLayout.LayoutParams.WRAP_CONTENT);
                mForegroundLayout.addView(iv, lp);
                iv = new ImageView(this);
                if (recycle) {
                    iv.setTag(level);
                }
                iv.setBackgroundResource(R.drawable.img_snow_ground_tiles);
                mForegroundLayout.addView(iv, lp);
            }
        }
    }

    // This is the level we are moving TO.
    private void addNextTransitionImages(int level) {
        mTransitionImagesCount = 0;
        if ((level > 0) && ((level - 1) < EXIT_TRANSITIONS.length)) {
            if (EXIT_TRANSITIONS[level - 1] != -1) {
                // Add the exit transition image
                ImageView iv = new ImageView(this);
                iv.setTag(new Pair<Integer, Integer>(1, (level - 1)));
                // This is being background loaded.  Should already be loaded, but if not, wait.
                while (mExitTransitions[level - 1] == null) {
                    synchronized (mExitTransitions) {
                        if (mExitTransitions[level - 1] == null) {
                            try {
                                mExitTransitions.wait();
                            } catch (InterruptedException e) {
                                continue;
                            }
                        }
                    }
                }
                iv.setImageBitmap(mExitTransitions[level - 1]);
                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT);
                mBackgroundLayout.addView(iv, lp);

                // No foreground on transistions.  Transition images are a single screen long
                View view = new View(this);
                lp = new LinearLayout.LayoutParams(mScreenWidth, 10);
                mForegroundLayout.addView(view, lp);
                mTransitionImagesCount++;
            }
        }
        if ((level > 0) && (level < ENTRY_TRANSITIONS.length)) {
            if (ENTRY_TRANSITIONS[level] != -1) {
                // Add the exit transition image
                ImageView iv = new ImageView(this);
                iv.setTag(new Pair<Integer, Integer>(2, level));
                // This is being background loaded.  Should already be loaded, but if not, wait.
                while (mEntryTransitions[level] == null) {
                    synchronized (mEntryTransitions) {
                        if (mEntryTransitions[level] == null) {
                            try {
                                mEntryTransitions.wait();
                            } catch (InterruptedException e) {
                                continue;
                            }
                        }
                    }
                }
                iv.setImageBitmap(mEntryTransitions[level]);
                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT);
                mBackgroundLayout.addView(iv, lp);
                // No foreground on transistions.  Transition images are a single screen long
                View view = new View(this);
                lp = new LinearLayout.LayoutParams(mScreenWidth, 10);
                mForegroundLayout.addView(view, lp);
                mTransitionImagesCount++;
            }
        }
    }

    private void addFinalImages() {
        addNextImages(1);
        addNextImages(1);

        // Add presents
        addFinalPresentRun();

        // Add final screen.  This is a two screen background.
        ImageView iv = new ImageView(this);
        iv.setTag(true);
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.bg_finish);
        if ((mScaleX != 1.0f) || (mScaleY != 1.0f)) {
            Bitmap tmp = Bitmap.createScaledBitmap(bmp, mScreenWidth * 2, mScreenHeight, false);
            if (bmp != tmp) {
                bmp.recycle();
            }
            bmp = tmp;
        }
        iv.setImageBitmap(bmp);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        mBackgroundLayout.addView(iv, lp);
        View view = new View(this);
        lp = new LinearLayout.LayoutParams(mScreenWidth * 2, 10);
        mForegroundLayout.addView(view, lp);
        addNextObstacleSpacer(2);
    }

    // Load the level 1 images right now since we need to display them.
    // Load the rest on a thread.
    // We preload all of these because the transitions can be quick.
    // They are not very big, relatively speaking.
    private void loadElfImages() {
        mElfImages = new Bitmap[ELF_IMAGES.length];
        mElfHitImages = new Bitmap[ELF_HIT_IMAGES.length];
        mElfBurnImages = new Bitmap[ELF_BURN_IMAGES.length];
        mElfThrustImages = new Bitmap[ELF_THRUST_IMAGES.length];
        mElfSmokeImages = new Bitmap[ELF_SMOKE_IMAGES.length];
        mElfImages[0] = BitmapFactory.decodeResource(getResources(), ELF_IMAGES[0]);
        mElfHitImages[0] = BitmapFactory.decodeResource(getResources(), ELF_HIT_IMAGES[0]);
        mElfBurnImages[0] = BitmapFactory.decodeResource(getResources(), ELF_BURN_IMAGES[0]);
        mElfThrustImages[0] = BitmapFactory.decodeResource(getResources(), ELF_THRUST_IMAGES[0]);
        mElfSmokeImages[0] = BitmapFactory.decodeResource(getResources(), ELF_SMOKE_IMAGES[0]);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < ELF_IMAGES.length; i++) {
                    mElfImages[i] = BitmapFactory.decodeResource(getResources(), ELF_IMAGES[i]);
                }
                for (int i = 1; i < ELF_HIT_IMAGES.length; i++) {
                    mElfHitImages[i] = BitmapFactory.decodeResource(getResources(), ELF_HIT_IMAGES[i]);
                }
                for (int i = 1; i < ELF_BURN_IMAGES.length; i++) {
                    mElfBurnImages[i] = BitmapFactory.decodeResource(getResources(), ELF_BURN_IMAGES[i]);
                }
                for (int i = 1; i < ELF_THRUST_IMAGES.length; i++) {
                    mElfThrustImages[i] = BitmapFactory.decodeResource(getResources(), ELF_THRUST_IMAGES[i]);
                }
                for (int i = 1; i < ELF_SMOKE_IMAGES.length; i++) {
                    mElfSmokeImages[i] = BitmapFactory.decodeResource(getResources(), ELF_SMOKE_IMAGES[i]);
                }
            }
        });
        thread.start();
    }

    private void updateElf(boolean hit) {
        float thrustWidth = 0.0f;
        if (hit) {
            // Just update the elf drawable
            mElf.setImageDrawable(null);
            mElfBitmap.recycle();
            mElfBitmap = mElfHitImages[mElfState];
            mElf.setImageBitmap(mElfBitmap);
            updateElfThrust(2);
            thrustWidth = (float) mCurrentTrailBitmap.getWidth() * mElfScale;
        } else {
            // New state for elf recycle, reload and reset.
            mElf.setImageDrawable(null);
            if (mElfBitmap != null) {
                mElfBitmap.recycle();
            }
            mThrust.setImageDrawable(null);
            if (mBurnBitmap != null) {
                mBurnBitmap.recycle();
            }
            if (mThrustBitmap != null) {
                mThrustBitmap.recycle();
            }
            if (mSmokeBitmpap != null) {
                mSmokeBitmpap.recycle();
            }
            if (mElfState < 4) {
                mBurnBitmap = mElfBurnImages[mElfState];
                mThrustBitmap = mElfThrustImages[mElfState];
                mSmokeBitmpap = mElfSmokeImages[mElfState];
                mElfBitmap = mElfImages[mElfState];
                if (mElfAccelY > 0.0f) {
                    updateElfThrust(1);
                } else {
                    updateElfThrust(0);
                }
                thrustWidth = (float) mCurrentTrailBitmap.getWidth() * mElfScale;
            } else {
                mElfBitmap = mElfImages[4];
                mThrust.setVisibility(View.GONE);
            }
            mElf.setImageBitmap(mElfBitmap);
        }
        float offset = thrustWidth + ((float) mElfBitmap.getWidth() / 2.0f);
        mElfLayout.setX(mElfPosX - offset);
        mElfLayout.setY(mElfPosY);
        mElfLayout.setPivotX(offset);
        mElfLayout.setPivotY((float) mElfBitmap.getHeight() * 3.0f / 4.0f);
        float rot = (float) (Math.atan(mElfVelY / mElfVelX) * 120.0 / Math.PI);
        mElfLayout.setRotation(rot);
        mElfLayout.invalidate();
    }

    // 0 - burn, 1 - thrust, 2 - smoke, 3 - gone
    private void updateElfThrust(int type) {
        switch (type) {
        case 0:
            mCurrentTrailBitmap = mBurnBitmap;
            break;
        case 1:
            mCurrentTrailBitmap = mThrustBitmap;
            break;
        case 2:
            mCurrentTrailBitmap = mSmokeBitmpap;
            break;
        case 3:
        default:
            mCurrentTrailBitmap = null;
            break;
        }
        if (mCurrentTrailBitmap != null) {
            mThrust.setImageBitmap(mCurrentTrailBitmap);
        }
    }

    private void pause() {

        if (mIsPlaying) {
            mBigPlayButtonLayout.setVisibility(View.VISIBLE);
            if (!mIsTv) {
                mExit.setVisibility(View.VISIBLE);
            }
        }

        mIsPlaying = false;
        mHandler.removeCallbacks(mGameLoop);
        mControlView.setOnTouchListener(null);
        mPlayPauseButton.setImageResource(R.drawable.play_button_jp);

        if (mJetThrustStream > 0) {
            mSoundPool.stop(mJetThrustStream);
            mJetThrustStream = 0;
        }
        if (mBackgroundPlayer != null) {
            mBackgroundPlayer.pause();
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            ImmersiveModeHelper.setImmersiveStickyWithActionBar(getWindow());
        }
    }

    private void play() {
        mIsPlaying = true;
        mBigPlayButtonLayout.setVisibility(View.GONE);
        mLastTime = System.currentTimeMillis();
        mControlView.setOnTouchListener(this);
        mHandler.post(mGameLoop);
        mPlayPauseButton.setImageResource(R.drawable.pause_button_jp);
        mExit.setVisibility(View.GONE);
        if (mBackgroundPlayer != null) {
            mBackgroundPlayer.start();
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            ImmersiveModeHelper.setImmersiveSticky(getWindow());
        }
    }

    private void endGame() {
        mIsPlaying = false;
        Intent intent = new Intent(this.getApplicationContext(), EndGameActivity.class);
        intent.putExtra("score", (long) (mScore / 10));

        AnalyticsManager.sendEvent(getString(R.string.analytics_screen_rocket),
                getString(R.string.analytics_action_rocket_final_score), null, mScore / 10);

        if (mCleanLevel) {
            intent.putExtra(getString(R.string.achievement_safe_tapper), true);
        }
        if (!mHit) {
            intent.putExtra(getString(R.string.achievement_untouchable), true);
        }
        if (mPresentBonus) {
            intent.putExtra(getString(R.string.achievement_hidden_presents), true);
        }
        if ((mScore / 10) > 10000) {
            intent.putExtra(getString(R.string.achievement_rocket_junior_score_10000), true);
        }
        if ((mScore / 10) > 30000) {
            intent.putExtra(getString(R.string.achievement_rocket_intermediate_score_30000), true);
        }
        if ((mScore / 10) > 50000) {
            intent.putExtra(getString(R.string.achievement_rocket_pro_score_50000), true);
        }
        startActivity(intent);
        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
        finish();
    }

    private void releaseIntegerBitmapMap(Map<Integer, Bitmap> map) {

        Iterator<Map.Entry<Integer, Bitmap>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, Bitmap> entry = it.next();
            Bitmap bitmap = entry.getValue();
            if (bitmap != null) {
                bitmap.recycle();
            }
        }
    }

    private void releaseBitmapArray(Bitmap[] bitmapArray) {
        if (bitmapArray != null) {
            for (Bitmap bitmap : bitmapArray) {
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                }
            }
        }
    }
}