com.google.android.apps.santatracker.games.gumball.TiltGameFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.santatracker.games.gumball.TiltGameFragment.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.games.gumball;

import com.google.android.apps.santatracker.R;
import com.google.android.apps.santatracker.games.common.ImmersiveModeHelper;
import com.google.android.apps.santatracker.games.common.PlayGamesActivity;
import com.google.android.apps.santatracker.games.matching.CircleView;
import com.google.android.apps.santatracker.games.matching.MatchingGameConstants;
import com.google.android.apps.santatracker.games.matching.LevelTextView;

import org.jbox2d.callbacks.ContactImpulse;
import org.jbox2d.callbacks.ContactListener;
import org.jbox2d.collision.Manifold;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.contacts.Contact;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.AnimationDrawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Gumball game fragment.
 */
public class TiltGameFragment extends Fragment
        implements SensorEventListener, ContactListener, AnimationListener, OnClickListener {

    /**
     * Bounce rate of objects in the physics world.
     */
    public static final float WORLD_OBJECT_BOUNCE = 0.2f;
    /**
     * Density of objects in the physics world.
     */
    public static final float WORLD_OBJECT_DENSITY = 185.77f;
    /**
     * Friction of objects in the physics world.
     */
    public static final float WORLD_OBJECT_FRICTION = 0.2f;
    /**
     * Friction of floor objects in the physics world.
     */
    public static final float WORLD_FLOOR_FRICTION = 0.8f;
    /**
     * Initial X position of the floor and pipes in the physics world.
     */
    public static final float WORLD_FLOOR_X = 3.37f;
    /**
     * Initial Y position of the floor and pipes in the physics world.
     */
    public static final float WORLD_FLOOR_Y = 0f;

    /** View that contains the main game. */
    private TiltGameView mGameView;

    /**
     * Box2D physics world for this game.
     */
    private PhysicsWorld mWorld;

    /**
     * Current rotation of the device. Used to adjust sensor readings if the screen is rotate in
     * portrait or landscape.
     *
     * @see android.view.Display#getRotation()
     */
    private int mRotation;

    /**
     * Main game thread.
     */
    private Runnable mGameThread;

    /**
     * Previous value of the sensor's Y reading. Used to calculate the rotational offset between
     * sensor events.
     */
    private float mPreviousSensorY = 0f;

    /**
     * MediaPlayer that plays the background music.
     */
    private MediaPlayer mBackgroundMusic;

    /**
     * Index of loaded sound effect in sound pool for small bounce.
     */
    private int mSoundBounceSmall = -1;

    /**
     * Index of loaded sound effect in sound pool for medium bounce.
     */
    private int mSoundBounceMed = -1;

    /**
     * Index of loaded sound effect in sound pool for large bounce.
     */
    private int mSoundBounceLarge = -1;

    /**
     * Index of loaded sound effect in sound pool for ball in machine.
     */
    private int mSoundBallInMachine = -1;

    /**
     * Index of loaded sound effect in sound pool for failed ball.
     */
    private int mSoundBallFail = -1;

    /**
     * Index of loaded sound effect in sound pool for dropped ball.
     */
    private int mSoundBallDrop = -1;

    /**
     * Index of loaded sound effect in sound pool for game over.
     */
    private int mSoundGameOver = -1;

    /**
     * Scale down animation for level.
     */
    private Animation mAnimationScaleLevelDown;

    /**
     * Fading out animation for level.
     */
    private Animation mAnimationLevelFadeOut;

    /**
     * Scaling up animation for level.
     */
    private Animation mAnimationLevelScaleUp;

    /**
     * Outlet animation for balls.
     */
    private Animation mAnimationOutlet;

    /**
     * Alpha animation for timer updates.
     */
    private Animation mAnimationTimerAlpha;

    /**
     * View for end of level circle overlay.
     */
    private CircleView mEndLevelCircle;

    /**
     * View that shows the current level number.
     */
    private LevelTextView mLevelNumberText;

    /**
     * Sound pool from which all sounds are played back.
     */
    private SoundPool mSoundPool;

    /**
     * Holder for sound pool id to handle playbacks, connects and disconnects.
     */
    private final HashMap<UUID, Boolean> mSoundPoolId = new HashMap<>();

    /**
     * Number of balls left in the game.
     */
    private int mGameBallsLeft = 2;

    /**
     * Current play level. Zero indexed, first level is 0.
     */
    private int mCurrentLevelNum = 0;

    /**
     * View for the ball outlet at the top of the screen.
     */
    private View mGameOutlet;

    /**
     * Root view of the game layout.
     */
    private View mRootView;

    /**
     * Gumballs that are queued to be dropped through the outlet.
     */
    private Queue<Gumball> mGumballQueue;

    /**
     * The current, active gumball on screen.
     */
    private Gumball mCurrentGumball;

    /**
     * X position of outlet in the last animation.
     */
    private float mOutletPreviousXPos = 0;

    /**
     * Array of the ball indicator views at the bottom of the screen.
     */
    private ImageView mViewIndicators[] = new ImageView[6];

    /**
     * Number of gumballs collected in the current game.
     */
    private int mNumberCollected = 0;

    /**
     * Refresh rate for the game countdown timer.
     *
     * @see com.google.android.apps.santatracker.games.gumball.TiltGameFragment.GameCountdown
     */
    private int mFramesPerSecond = 60;

    /**
     * Time left in the current game. Value in milliseconds.
     */
    private long mTimeLeftInMillis = MatchingGameConstants.GUMBALL_INIT_TIME;

    /**
     * Countdown timer for the current game.
     */
    private GameCountdown mCountDownTimer = null;

    /**
     * Countdown timer text.
     */
    private TextView mViewCountdown;

    /**
     * Score text.
     */
    private TextView mViewScore;

    /**
     * Total score of current game.
     */
    private int mMatchScore = 0;

    /**
     * Number of balls that have respawned in the current level. Used to calculate the total
     * game score.
     */
    private int mCountLevelBallRespawns = 0;

    /**
     * Flag indicating if the game is paused.
     */
    private boolean wasPaused = false;

    private ImageView mViewPlayButton;

    private ImageView mViewPauseButton;

    private ImageButton mViewBigPlayButton;

    private ImageView mViewCancelBar;

    private View mViewMatchPauseOverlay;

    private View mViewPlayAgainBackground;

    private View mViewPlayAgainMain;

    private Button mViewPlayAgainButton;

    private TextView mViewPlayAgainScore;

    private TextView mViewPlayAgainLevel;

    private Animation mAnimationPlayAgainBackground;

    private Animation mAnimationPlayAgainMain;

    /**
     * Display offset on X axis for outlet in pixels.
     */
    private int mOutletOffset;

    /**
     * View that displays the instructions from {@link #mDrawableTransition}
     */
    private ImageView mViewInstructions;

    /**
     * Drawable that contains all images for the instructions.
     */
    private AnimationDrawable mDrawableTransition;

    private SharedPreferences mSharedPreferences;

    private ImageView mViewGPlusSignIn;

    private View mViewGPlusLayout;

    private ImageButton mViewMainMenuButton;

    /**
     * Gets an instance of this fragment
     */
    public static TiltGameFragment newInstance() {
        TiltGameFragment fragment = new TiltGameFragment();
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = inflater.inflate(R.layout.fragment_gumball, container, false);
        mRootView.setKeepScreenOn(true);

        // Use a lower resolution background image to conserve memory below ICS
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            View matchScoreLayout = mRootView.findViewById(R.id.tilt_score_layout);
            matchScoreLayout.setBackgroundResource(R.drawable.score_background_gingerbread);
        }

        mViewPlayAgainScore = (TextView) mRootView.findViewById(R.id.play_again_score);
        mViewPlayAgainScore.setText(String.valueOf(mMatchScore));
        mViewPlayAgainLevel = (TextView) mRootView.findViewById(R.id.play_again_level);
        mViewPlayAgainLevel.setText(String.valueOf(mCurrentLevelNum));
        mViewPlayAgainBackground = mRootView.findViewById(R.id.play_again_bkgrd);
        mViewPlayAgainMain = mRootView.findViewById(R.id.play_again_main);
        mViewPlayAgainButton = (Button) mRootView.findViewById(R.id.play_again_btn);
        mViewPlayAgainButton.setOnClickListener(this);

        mViewGPlusSignIn = (ImageView) mRootView.findViewById(R.id.gplus_button);
        mViewGPlusSignIn.setOnClickListener(this);
        mViewGPlusLayout = mRootView.findViewById(R.id.play_again_gplus);
        mViewGPlusLayout.setVisibility(View.GONE);

        // Initialise all animations
        // Construct an animation to blink the timer indefinitely
        mAnimationTimerAlpha = new AlphaAnimation(0.0f, 1.0f);
        mAnimationTimerAlpha.setDuration(1000);
        mAnimationTimerAlpha.setRepeatMode(Animation.REVERSE);
        mAnimationTimerAlpha.setRepeatCount(Animation.INFINITE);

        // Load all other animations
        mAnimationPlayAgainBackground = AnimationUtils.loadAnimation(getActivity(), R.anim.play_again_bkgrd_anim);
        mAnimationPlayAgainBackground.setFillAfter(true);
        mAnimationPlayAgainBackground.setAnimationListener(this);
        mAnimationPlayAgainMain = AnimationUtils.loadAnimation(getActivity(), R.anim.play_again_main_anim);
        mAnimationPlayAgainMain.setFillAfter(true);
        mAnimationPlayAgainMain.setAnimationListener(this);
        mAnimationScaleLevelDown = AnimationUtils.loadAnimation(getActivity(), R.anim.scale_level_anim_down);
        mAnimationScaleLevelDown.setAnimationListener(this);
        mAnimationLevelFadeOut = AnimationUtils.loadAnimation(getActivity(), R.anim.level_fade_out_anim);
        mAnimationLevelFadeOut.setAnimationListener(this);
        mAnimationLevelScaleUp = AnimationUtils.loadAnimation(getActivity(), R.anim.scale_up_level_anim);
        mAnimationLevelScaleUp.setAnimationListener(this);

        mViewMainMenuButton = (ImageButton) mRootView.findViewById(R.id.main_menu_button);
        mViewMainMenuButton.setVisibility(View.GONE);
        mViewMainMenuButton.setOnClickListener(this);

        mGameOutlet = mRootView.findViewById(R.id.tiltGameOutlet);
        mOutletOffset = getResources().getInteger(R.integer.outlet_offset);

        mViewIndicators[0] = (ImageView) mRootView.findViewById(R.id.indicator1);
        mViewIndicators[1] = (ImageView) mRootView.findViewById(R.id.indicator2);
        mViewIndicators[2] = (ImageView) mRootView.findViewById(R.id.indicator3);
        mViewIndicators[3] = (ImageView) mRootView.findViewById(R.id.indicator4);
        mViewIndicators[4] = (ImageView) mRootView.findViewById(R.id.indicator5);
        mViewIndicators[5] = (ImageView) mRootView.findViewById(R.id.indicator6);
        mViewCountdown = (TextView) mRootView.findViewById(R.id.tiltTimer);

        mLevelNumberText = (LevelTextView) mRootView.findViewById(R.id.tilt_end_level_number);
        mLevelNumberText.setVisibility(View.GONE);
        mEndLevelCircle = (CircleView) mRootView.findViewById(R.id.tilt_end_level_circle);
        mEndLevelCircle.setVisibility(View.GONE);

        mViewPlayButton = (ImageView) mRootView.findViewById(R.id.tilt_play_button);
        mViewPlayButton.setOnClickListener(this);
        mViewPlayButton.setVisibility(View.GONE);
        mViewPauseButton = (ImageView) mRootView.findViewById(R.id.tilt_pause_button);
        mViewPauseButton.setOnClickListener(this);
        mViewPauseButton.setVisibility(View.VISIBLE);
        mViewMatchPauseOverlay = mRootView.findViewById(R.id.tilt_pause_overlay);
        mViewMatchPauseOverlay.setVisibility(View.GONE);
        mViewBigPlayButton = (ImageButton) mRootView.findViewById(R.id.tilt_big_play_button);
        mViewBigPlayButton.setOnClickListener(this);
        mViewCancelBar = (ImageView) mRootView.findViewById(R.id.tilt_cancel_bar);
        mViewCancelBar.setOnClickListener(this);
        mViewCancelBar.setVisibility(View.GONE);

        mViewScore = (TextView) mRootView.findViewById(R.id.tilt_score);
        mViewScore.setText(String.valueOf(mMatchScore));

        mGameView = (TiltGameView) mRootView.findViewById(R.id.tiltGameView);

        // Create the Box2D physics world.
        mWorld = new PhysicsWorld();
        Vec2 gravity = new Vec2(0.0f, 0.0f);
        mWorld.create(gravity);
        mGameView.setModel(mWorld);
        mWorld.getWorld().setContactListener(this);

        mGumballQueue = new LinkedList<>();

        // Initialise the sound pool and audio playback
        mSoundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
        mSoundBounceSmall = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_1, 1);
        mSoundBounceMed = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_2, 1);
        mSoundBounceLarge = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_3, 1);
        mSoundBallInMachine = mSoundPool.load(getActivity(), R.raw.gbg_ball_into_machine, 1);
        mSoundBallFail = mSoundPool.load(getActivity(), R.raw.gbg_ball_fall_out, 1);
        mSoundBallDrop = mSoundPool.load(getActivity(), R.raw.gbg_new_ball_bounce_drop, 1);
        mSoundGameOver = mSoundPool.load(getActivity(), R.raw.gameover, 1);

        // Display the instructions if they haven't been seen before
        mSharedPreferences = getActivity().getSharedPreferences(MatchingGameConstants.PREFERENCES_FILENAME,
                Context.MODE_PRIVATE);
        if (!mSharedPreferences.getBoolean(MatchingGameConstants.GUMBALL_INSTRUCTIONS_VIEWED, false)) {
            mDrawableTransition = new AnimationDrawable();
            mDrawableTransition.addFrame(getResources().getDrawable(R.drawable.instructions_shake_1), 300);
            mDrawableTransition.addFrame(getResources().getDrawable(R.drawable.instructions_shake_2), 300);
            mDrawableTransition.addFrame(getResources().getDrawable(R.drawable.instructions_shake_3), 300);
            mDrawableTransition.setOneShot(false);
            mViewInstructions = (ImageView) mRootView.findViewById(R.id.instructions);

            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                mViewInstructions.setImageResource(R.drawable.instructions_shake_1);
            } else {
                mViewInstructions.setImageDrawable(mDrawableTransition);
                mViewInstructions.post(new Runnable() {
                    public void run() {
                        mDrawableTransition.start();
                    }
                });
            }

            // Hide the instructions after 2 seconds
            mViewInstructions.postDelayed(new HideInstructionsRunnable(), 2200);
        }

        return mRootView;
    }

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

        // Resume the game play if the game was not paused
        if (!wasPaused) {
            mRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
            SensorManager sensorManager = (SensorManager) getActivity().getSystemService(Activity.SENSOR_SERVICE);
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            if (sensor != null) {
                sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);
            }
            mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis);
            mCountDownTimer.start();
            mGameView.setGameCountDown(mCountDownTimer);
        }

        // Start the game loop if it is not initialised yet
        if (mGameThread == null) {
            mGameThread = new Runnable() {
                public void run() {
                    synchronized (mWorld) {
                        if (!wasPaused) {
                            if (mCurrentLevelNum == 0) {
                                mCurrentLevelNum++;
                                loadLevel(mCurrentLevelNum);
                            }
                            mWorld.update();
                            mGameView.invalidate();
                        }
                    }
                    getActivity().getWindow().getDecorView().postDelayed(mGameThread, 10);
                }
            };
        }
        getActivity().getWindow().getDecorView().postDelayed(mGameThread, 1000);

        loadBackgroundMusic();
        updateSignInButtonVisibility();
    }

    @Override
    public void onPause() {
        super.onPause();
        pauseGame();
        if (mBackgroundMusic != null) {
            mBackgroundMusic.stop();
            mBackgroundMusic.release();
        }
        getActivity().getWindow().getDecorView().removeCallbacks(mGameThread);
    }

    private void loadBackgroundMusic() {
        mBackgroundMusic = MediaPlayer.create(getActivity(), R.raw.santatracker_musicloop);
        mBackgroundMusic.setLooping(true);
        mBackgroundMusic.setVolume(.2f, .2f);
        mBackgroundMusic.start();
    }

    /**
     * Hide the sign in button if sign in was successful.
     */
    public void onSignInSucceeded() {
        setSignInButtonVisibility(false);
    }

    public void onSignInFailed() {
    }

    @Override
    public void onClick(View view) {
        if (view.equals(mViewPauseButton)) {
            // Pause the game
            pauseGame();
        } else if (view.equals(mViewPlayButton) || view.equals(mViewBigPlayButton)) {
            // Continue the game
            unPauseGame();
        } else if (view.equals(mViewPlayAgainButton)) {
            // Reload the background music for a new game
            if (mBackgroundMusic != null) {
                mBackgroundMusic.stop();
                mBackgroundMusic.release();
            }
            loadBackgroundMusic();

            // Reset the game variables
            mCurrentLevelNum = 0;
            mTimeLeftInMillis = MatchingGameConstants.GUMBALL_INIT_TIME;
            mMatchScore = 0;
            mViewScore.setText(String.valueOf(mMatchScore));
            wasPaused = false;

            // Hide the pause screen
            mViewPlayAgainBackground.clearAnimation();
            mViewPlayAgainMain.clearAnimation();
            mViewPlayAgainBackground.setVisibility(View.GONE);
            mViewPlayAgainMain.setVisibility(View.GONE);
            mViewGPlusLayout.setVisibility(View.GONE);
            mViewMainMenuButton.setVisibility(View.GONE);
        } else if (view.equals(mViewGPlusSignIn)) {
            // Start sign-in flow.
            PlayGamesActivity act = Utils.getPlayGamesActivity(this);
            if (act != null) {
                act.startSignIn();
            }
        } else if (view.equals(mViewCancelBar) || (view.equals(mViewMainMenuButton))) {
            // Exit and return to previous Activity.
            returnToBackClass();
        }

    }

    private void returnToBackClass() {
        getActivity().finish();
    }

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

    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        float x, y;
        if (getActivity() != null) {
            // Store the current screen rotation (used to offset the readings of the sensor).
            mRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
        }

        // Handle screen rotations by interpreting the sensor readings here
        if (mRotation == Surface.ROTATION_0) {
            x = -event.values[0];
            y = -event.values[1];
        } else if (mRotation == Surface.ROTATION_90) {
            x = event.values[1];
            y = -event.values[0];
        } else if (mRotation == Surface.ROTATION_180) {
            x = event.values[0];
            y = event.values[1];
        } else {
            x = -event.values[1];
            y = event.values[0];
        }
        // keep y low to simulate gravity
        if (mPreviousSensorY == 0f) {
            mPreviousSensorY = -9;
        } else if (mPreviousSensorY > y) {
            mPreviousSensorY = y;
        }
        // restrict x to ~+-45 degrees
        if (x > 1.7) {
            x = 2;
        } else if (x < -1.7) {
            x = -2;
        }
        mWorld.getWorld().setGravity(new Vec2(x, mPreviousSensorY));
    }

    @Override
    public void beginContact(Contact contact) {

    }

    /**
     * Handle contact with objects in the Box 2D world.
     * Here the main game logic is implemented: When a ball hits the bottom pipe, it is removed
     * and the next level or ball is started.
     * When the ball goes over the edge, it is removed and a new ball is dropped from the pipe
     * again.
     */
    @Override
    public void endContact(Contact contact) {
        // If the gumball goes in the pipe, remove it from the scene (Case 1/2)
        if (contact.getFixtureA().getBody().getUserData() != null
                && !(contact.getFixtureA().getBody().getUserData() instanceof Gumball)
                && (contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_BOTTOM)
                        || contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_SIDES))) {
            mWorld.mBodiesToBeRemoved.add(contact.getFixtureB().getBody());
            mSoundPoolId.remove(((Gumball) contact.getFixtureB().getBody().getUserData()).mSoundPoolId);
            onBallInPipe();
        } else if (contact.getFixtureB().getBody().getUserData() != null
                && !(contact.getFixtureB().getBody().getUserData() instanceof Gumball) && (
                // If the gumball goes in the pipe, remove it from the scene (Case 2/2)
                contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_BOTTOM)
                        || contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_SIDES))) {
            mWorld.mBodiesToBeRemoved.add(contact.getFixtureA().getBody());
            mSoundPoolId.remove(((Gumball) contact.getFixtureA().getBody().getUserData()).mSoundPoolId);
            onBallInPipe();
        } else if (contact.getFixtureA().getBody().getUserData() != null
                && !(contact.getFixtureA().getBody().getUserData() instanceof Gumball)
                && contact.getFixtureA().getBody().getUserData().equals(TiltGameView.GAME_FLOOR)) {
            // If the gumball goes over the edge, remove it and respawn (Case 1/2)
            Gumball gumball = ((Gumball) contact.getFixtureB().getBody().getUserData());
            mWorld.mBodiesToBeRemoved.add(contact.getFixtureB().getBody());
            mSoundPoolId.remove(gumball.mSoundPoolId);
            mSoundPool.play(mSoundBallFail, 1, 1, 0, 0, 1.0f);
            mWorld.getWorld().step(1.0f / 60.0f, 10, 10);
            moveOutlet((mCurrentGumball.mXInitPos));
            mCountLevelBallRespawns++;
        } else if (contact.getFixtureB().getBody().getUserData() != null
                && !(contact.getFixtureB().getBody().getUserData() instanceof Gumball)
                && contact.getFixtureB().getBody().getUserData().equals(TiltGameView.GAME_FLOOR)) {
            // If the gumball goes over the edge, remove it and respawn (Case 2/2)
            Gumball gumball = ((Gumball) contact.getFixtureB().getBody().getUserData());
            mWorld.mBodiesToBeRemoved.add(contact.getFixtureA().getBody());
            mSoundPoolId.remove(gumball.mSoundPoolId);
            mSoundPool.play(mSoundBallFail, 1, 1, 0, 0, 1.0f);
            mWorld.getWorld().step(1.0f / 60.0f, 10, 10);
            moveOutlet((mCurrentGumball.mXInitPos));
            mCountLevelBallRespawns++;
        }
    }

    /**
     * Successfully dropped a ball in the pipe.
     * Add the next ball and go to the next level if no balls are left in this level.
     */
    private void onBallInPipe() {
        mSoundPool.play(mSoundBallInMachine, 1, 1, 0, 0, 1.0f);
        mGameBallsLeft--;
        mNumberCollected++;
        changeIndicator();
        mMatchScore += 50 * Math.max(1f, (mCurrentLevelNum - mCountLevelBallRespawns));
        mViewScore.setText(String.valueOf(mMatchScore));
        if (mGameBallsLeft == 0 && mViewPlayAgainBackground.getVisibility() != View.VISIBLE) {
            // No balls are left in this level, go to the next one
            mCurrentLevelNum++;
            mLevelNumberText.setLevelNumber(mCurrentLevelNum);
            mLevelNumberText.startAnimation(mAnimationLevelScaleUp);
            mEndLevelCircle.startAnimation(mAnimationScaleLevelDown);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.jbox2d.callbacks.ContactListener#postSolve(org.jbox2d.dynamics.contacts
     * .Contact, org.jbox2d.callbacks.ContactImpulse)
     */

    /**
     * Play a sound on impact (when a ball is dropped).
     * The sound depends on the severity of the impact.
     *
     * @see #playBounceSound(float)
     */
    @Override
    public void postSolve(Contact contact, ContactImpulse impulse) {
        // Get both collision objects
        Object dataA = contact.getFixtureA().getBody().getUserData();
        Object dataB = contact.getFixtureB().getBody().getUserData();

        // Check if one of the objects is NOT a gumball, but a candy cane.
        boolean hitCane = false;
        if (dataA != null && !(dataA instanceof Gumball) && (Integer) dataA > TiltGameView.GUMBALL_PURPLE) {
            hitCane = true;
        } else if (dataB != null && !(dataB instanceof Gumball) && (Integer) dataB > TiltGameView.GUMBALL_PURPLE) {
            hitCane = true;
        }

        if (hitCane && impulse.normalImpulses[0] > 80) {
            playBounceSound(impulse.normalImpulses[0]);
        }
    }

    /**
     * Plays a 'bounce' sound through the sound pool, depending on the impulse.
     */
    private void playBounceSound(float impulse) {
        if (impulse > 80) {
            mSoundPool.play(mSoundBounceLarge, 1, 1, 0, 0, 1.0f);
        } else if (impulse > 60) {
            mSoundPool.play(mSoundBounceMed, 1, 1, 0, 0, 1.0f);
        } else if (impulse > 30) {
            mSoundPool.play(mSoundBounceSmall, 1, 1, 0, 0, 1.0f);
        }
    }

    @Override
    public void preSolve(Contact contact, Manifold arg1) {

    }

    /**
     * Add a gumball to the game and play the ball drop sound.
     */
    private void addGumball(float xPos, float yPos) {
        Gumball gumball = new Gumball();
        gumball.mXInitPos = xPos;
        gumball.mYInitPos = yPos;
        gumball.mSoundPoolId = UUID.randomUUID();
        mSoundPoolId.put(gumball.mSoundPoolId, false);
        mGameView.addGumball(gumball);
        mSoundPool.play(mSoundBallDrop, 1, 1, 0, 0, 1);
    }

    private JSONObject readLevelFile(int levelNumber) throws IOException, JSONException {
        // load the appropriate levels file from a raw resource.
        InputStream is = getResources().openRawResource(Utils.getLevelRawFile(mCurrentLevelNum));
        int size = is.available();
        byte[] buffer = new byte[size];
        is.read(buffer);
        is.close();
        String json = new String(buffer, "UTF-8");
        JSONObject level = new JSONObject(json);

        return level;
    }

    /**
     * Loads a level from the levels json file and sets up the game world.
     */
    private void loadLevel(int levelNumber) {

        // Reset the current game state
        if (mCountDownTimer != null) {
            mCountDownTimer.cancel();
        }
        mCountLevelBallRespawns = 0;
        mNumberCollected = 0;
        mViewPlayAgainLevel.setText(String.valueOf(levelNumber));
        Body body = mWorld.getWorld().getBodyList();
        while (body != null) {
            if (body.m_userData == null) {
                body = body.getNext();
                continue;
            }
            mWorld.mBodiesToBeRemoved.add(body);
            body = body.getNext();
        }
        mWorld.getWorld().step(1.0f / 60.0f, 10, 10);

        try {
            // Read the level file and extract the candy cane positions
            JSONObject level = readLevelFile(levelNumber);
            JSONArray canes = level.getJSONArray("candycanes");

            for (int i = 0; i < canes.length(); i++) {
                JSONObject canePart = canes.getJSONObject(i);
                int type = canePart.getInt("type");
                float xPos = (float) canePart.getDouble("xPos");
                float yPos = (float) canePart.getDouble("yPos");
                // Add the candy cane to the game world, the values represent the
                mWorld.addItem(xPos, yPos, Edges.getEdges(type), WORLD_OBJECT_BOUNCE, type, WORLD_OBJECT_DENSITY,
                        WORLD_OBJECT_FRICTION, BodyType.STATIC);
            }

            // Add the sides and floor to the game world to catch dropped balls.
            // Note that the WORLD_FRICTION is used as the bounce rate of the floors.
            mWorld.addItem(WORLD_FLOOR_X, WORLD_FLOOR_Y, Edges.getPipeSideEdges(), WORLD_OBJECT_BOUNCE,
                    TiltGameView.PIPE_SIDES, WORLD_OBJECT_DENSITY, WORLD_OBJECT_FRICTION, BodyType.STATIC);
            mWorld.addFloor(WORLD_FLOOR_X, WORLD_FLOOR_Y, TiltGameView.GAME_FLOOR, WORLD_OBJECT_DENSITY,
                    WORLD_OBJECT_FRICTION, WORLD_FLOOR_FRICTION, BodyType.STATIC);
            mWorld.addPipeBottom(WORLD_FLOOR_X, WORLD_FLOOR_Y, TiltGameView.PIPE_BOTTOM, WORLD_OBJECT_DENSITY,
                    WORLD_OBJECT_FRICTION, WORLD_FLOOR_FRICTION, BodyType.STATIC);

            // Add the gumballs
            JSONArray gumballs = level.getJSONArray("gumballs");
            mGameBallsLeft = gumballs.length();
            setIndicators(mGameBallsLeft);
            for (int j = 0; j < gumballs.length(); j++) {
                JSONObject gumball = gumballs.getJSONObject(j);
                float xPos = (float) gumball.getDouble("xPos");
                float yPos = (float) gumball.getDouble("yPos");
                Gumball gumballObject = new Gumball();
                gumballObject.mXInitPos = xPos;
                gumballObject.mYInitPos = yPos;
                mGumballQueue.add(gumballObject);
            }
            mCurrentGumball = mGumballQueue.poll();

            // Start the timer
            if (mCurrentGumball != null) {
                if (mCurrentLevelNum > 1) {
                    // Do not include gumball dropping time in countdown calculation.
                    mTimeLeftInMillis += MatchingGameConstants.GUMBALL_ADDED_TIME;
                }
                mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis);
                mCountDownTimer.start();
                mGameView.setGameCountDown(mCountDownTimer);

                // Move the outlet to its initial position
                moveOutlet((mCurrentGumball.mXInitPos));
            }
        } catch (IOException e) {
        } catch (JSONException e) {
        }

    }

    /**
     * Update the state of the indicators at the bottom of the screen to the number of balls
     * collected.
     */
    private void setIndicators(int numGumballs) {
        for (int i = 0; i < mViewIndicators.length; i++) {
            int stateResource = R.drawable.gbg_gumball_indicator_collected_disabled;
            if (i + 1 <= numGumballs) {
                stateResource = R.drawable.gbg_gumball_indicator_pending;
            }
            mViewIndicators[i].setImageResource(stateResource);
        }
    }

    /**
     * Mark the last indicator for which a ball was collected in the 'collected' state.
     */
    private void changeIndicator() {
        mViewIndicators[mNumberCollected - 1].setImageResource(R.drawable.gbg_gumball_indicator_collected);
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        if (animation == mAnimationScaleLevelDown) {
            // After the level scale down animation, fade out the level number and end circle
            mLevelNumberText.startAnimation(mAnimationLevelFadeOut);
            mEndLevelCircle.startAnimation(mAnimationLevelFadeOut);
        } else if (animation == mAnimationLevelFadeOut) {
            // After the level fade out animation reset and hide all other end level views
            mEndLevelCircle.clearAnimation();
            mLevelNumberText.clearAnimation();
            mLevelNumberText.setVisibility(View.GONE);
            mEndLevelCircle.setVisibility(View.GONE);
        } else if (animation == mAnimationOutlet) {
            // After the outlet has moved to the correct position, add gumball
            addGumball(mCurrentGumball.mXInitPos, mCurrentGumball.mYInitPos);
            if (mGumballQueue.peek() != null) {
                // Move it to the next position if there is a gumball left in the queue
                mCurrentGumball = mGumballQueue.poll();
                moveOutlet(mCurrentGumball.mXInitPos);
            }
        }
    }

    @Override
    public void onAnimationRepeat(Animation arg0) {
        // do nothing

    }

    @Override
    public void onAnimationStart(Animation animation) {
        if (animation == mAnimationScaleLevelDown) {
            // Show the circle level end and level text views when the animation starts
            mEndLevelCircle.setVisibility(View.VISIBLE);
            mLevelNumberText.setVisibility(View.VISIBLE);
        } else if (animation == mAnimationLevelFadeOut) {
            // Load the next level after the end level animation is over
            loadLevel(mCurrentLevelNum);
        } else if (animation == mAnimationPlayAgainBackground) {
            // Show the 'play again' screen when the animation starts and cancel the timer
            mViewPlayAgainBackground.setVisibility(View.VISIBLE);
            if (mCountDownTimer != null) {
                mCountDownTimer.cancel();
            }
        } else if (animation == mAnimationPlayAgainMain) {
            mViewPlayAgainMain.setVisibility(View.VISIBLE);
            setSignInButtonVisibility(true);
        }
    }

    /**
     * Set the visibility of the sign in button if the user is not already signed in.
     */
    private void setSignInButtonVisibility(boolean show) {
        mViewGPlusLayout.setVisibility(show && !Utils.isSignedIn(this) ? View.VISIBLE : View.GONE);
    }

    /**
     * Hide the sign in button when the user signs in and the button is still visible on screen.
     */
    private void updateSignInButtonVisibility() {
        if (mViewGPlusLayout.getVisibility() == View.VISIBLE && Utils.isSignedIn(this)) {
            setSignInButtonVisibility(false);
        }
    }

    /**
     * Start an animation to move the outlet to the x position in pixels.
     */
    private void moveOutlet(float xPos) {
        float scale = mRootView.getWidth() / 10.0f;
        mAnimationOutlet = new TranslateAnimation(mOutletPreviousXPos, (scale * xPos) - mOutletOffset, 0, 0);
        mAnimationOutlet.setDuration(700);
        mAnimationOutlet.setFillAfter(true);
        mAnimationOutlet.setStartOffset(400);
        mAnimationOutlet.setAnimationListener(this);
        mGameOutlet.startAnimation(mAnimationOutlet);
        mOutletPreviousXPos = (scale * xPos) - mOutletOffset;
    }

    /**
     * Countdown for the main game.
     * Updates the countdown on screen and stops the game when the timer runs out.
     */
    public class GameCountdown {

        private Boolean animationStarted = false;

        private final long mMillisDuration;

        private final long mMillisTickDuration;

        private long mTicksLeft;

        private boolean mStarted = false;

        private long mSecondsTextValue = -1;

        /**
         * @param framesPerSecond assumed frame rate
         * @param millisInFuture  duration of game at this frame rate
         */
        public GameCountdown(int framesPerSecond, long millisInFuture) {
            mMillisDuration = millisInFuture;
            mMillisTickDuration = 1000 / framesPerSecond;
            mTicksLeft = mMillisDuration / mMillisTickDuration;
        }

        /**
         * Stop the timer.
         */
        public void cancel() {
            mTicksLeft = 0;
            mStarted = false;
        }

        /**
         * Starts the timer.
         */
        public void start() {
            mStarted = true;
            mSecondsTextValue = -1;
            long seconds = TimeUnit.MILLISECONDS.toSeconds(mTicksLeft * mMillisTickDuration);
            if (seconds >= 6) {
                animationStarted = false;
                mViewCountdown.clearAnimation();
                mViewCountdown.setTextColor(Color.WHITE);
                mViewCountdown.setTypeface(Typeface.DEFAULT);
            }
        }

        /**
         * Update the displayed timer.
         * When the timer is below 6s the text color changes to red.
         */
        public void tick() {
            if (mStarted) {
                --mTicksLeft;
                mTimeLeftInMillis = mTicksLeft * mMillisTickDuration;
                if (mTimeLeftInMillis < 6000 && !animationStarted) {
                    animationStarted = true;
                    mViewCountdown.setTextColor(Color.RED);
                    mViewCountdown.setTypeface(Typeface.DEFAULT_BOLD);
                    mViewCountdown.clearAnimation();
                    mViewCountdown.startAnimation(mAnimationTimerAlpha);
                }
                if (mSecondsTextValue != mTimeLeftInMillis / 1000) {
                    mViewCountdown
                            .setText(String.format("%d:%02d", TimeUnit.MILLISECONDS.toMinutes(mTimeLeftInMillis),
                                    TimeUnit.MILLISECONDS.toSeconds(mTimeLeftInMillis)));
                    mSecondsTextValue = mTimeLeftInMillis / 1000;
                }
                if (mTimeLeftInMillis == 0) {
                    finished();
                }
            }
        }

        /**
         * Shut down the count down timer.
         * Cancel all pending animations and display the 'play again' screen.
         */
        private void finished() {
            mViewCountdown.clearAnimation();
            animationStarted = false;
            mViewCountdown.setTextColor(Color.WHITE);
            mViewCountdown.setTypeface(Typeface.DEFAULT);
            if (mViewPlayAgainBackground.getVisibility() != View.VISIBLE && !wasPaused) {
                wasPaused = true;
                submitScore(MatchingGameConstants.LEADERBOARDS_GUMBALL, mMatchScore);
                if (mBackgroundMusic != null) {
                    mBackgroundMusic.stop();
                    mBackgroundMusic.release();
                    mBackgroundMusic = null;
                }
                mViewPlayAgainScore.setText(String.valueOf(mMatchScore));
                mViewPlayAgainBackground.startAnimation(mAnimationPlayAgainBackground);
                mViewPlayAgainMain.startAnimation(mAnimationPlayAgainMain);
                mViewPlayAgainBackground.setVisibility(View.VISIBLE);
                mViewPlayAgainMain.setVisibility(View.VISIBLE);
                mViewMainMenuButton.setVisibility(View.VISIBLE);
                setSignInButtonVisibility(true);
                mSoundPool.play(mSoundGameOver, .2f, .2f, 0, 0, 1.0f);
            }

            cancel();
        }
    }

    /**
     * Pause the game when the back key is pressed.
     */
    public void onBackKeyPressed() {
        if (mViewPlayAgainMain.getVisibility() == View.VISIBLE) {
            returnToBackClass();
        } else {
            if (mViewPauseButton.getVisibility() != View.GONE) {// check if already handled
                pauseGame();
            } else {
                unPauseGame();
            }
        }
    }

    /**
     * Pause the game and display the pause game screen.
     */
    private void pauseGame() {
        mViewPauseButton.setVisibility(View.GONE);
        mViewPlayButton.setVisibility(View.VISIBLE);
        if (mCountDownTimer != null) {
            mCountDownTimer.cancel();
            wasPaused = true;
        }
        mViewMatchPauseOverlay.setVisibility(View.VISIBLE);
        mViewCancelBar.setVisibility(View.VISIBLE);
        SensorManager sensorManager = (SensorManager) getActivity().getSystemService(Activity.SENSOR_SERVICE);
        sensorManager.unregisterListener(this);
        if (Utils.hasKitKat()) {
            ImmersiveModeHelper.setImmersiveStickyWithActionBar(getActivity().getWindow());
        }
    }

    /**
     * Continue the paused game.
     * Restart the countdown timer and hide the pause game screen.
     */
    private void unPauseGame() {
        mViewPauseButton.setVisibility(View.VISIBLE);
        mViewPlayButton.setVisibility(View.GONE);
        mViewMatchPauseOverlay.setVisibility(View.GONE);
        mViewCancelBar.setVisibility(View.GONE);
        mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis);
        mCountDownTimer.start();
        mGameView.setGameCountDown(mCountDownTimer);
        wasPaused = false;
        SensorManager sensorManager = (SensorManager) getActivity().getSystemService(Activity.SENSOR_SERVICE);
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (sensor != null) {
            sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);
        }
        if (Utils.hasKitKat()) {
            ImmersiveModeHelper.setImmersiveSticky(getActivity().getWindow());
        }
    }

    /**
     * Submit score to play games services
     */
    private void submitScore(int resId, int score) {
        PlayGamesActivity act = Utils.getPlayGamesActivity(this);
        if (act != null) {
            act.postSubmitScore(resId, score);
        }
    }

    /**
     * Hide the instructions and mark them as viewed.
     */
    private class HideInstructionsRunnable implements Runnable {

        @Override
        public void run() {
            mDrawableTransition.stop();
            wasPaused = false;
            mViewInstructions.setVisibility(View.GONE);
            Editor edit = mSharedPreferences.edit();
            edit.putBoolean(MatchingGameConstants.GUMBALL_INSTRUCTIONS_VIEWED, true);
            edit.apply();
        }

    }
}