com.google.android.apps.santatracker.launch.TvStartupActivity.java Source code

Java tutorial

Introduction

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

import android.Manifest;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.FocusHighlight;
import android.support.v17.leanback.widget.ItemBridgeAdapter;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.DecodeFormat;
import com.google.android.apps.santatracker.AudioPlayer;
import com.google.android.apps.santatracker.BuildConfig;
import com.google.android.apps.santatracker.R;
import com.google.android.apps.santatracker.data.SantaPreferences;
import com.google.android.apps.santatracker.invites.AppInvitesFragment;
import com.google.android.apps.santatracker.service.SantaService;
import com.google.android.apps.santatracker.service.SantaServiceMessages;
import com.google.android.apps.santatracker.util.AccessibilityUtil;
import com.google.android.apps.santatracker.util.AnalyticsManager;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.android.apps.santatracker.util.SantaLog;
import com.google.android.apps.santatracker.village.Village;
import com.google.android.apps.santatracker.village.VillageView;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.firebase.analytics.FirebaseAnalytics;

import java.lang.ref.WeakReference;

/**
 * Launch activity for the app. Handles loading of the village, the state of the markers (based on
 * the date/time) and incoming voice intents.
 */
public class TvStartupActivity extends FragmentActivity implements View.OnClickListener, Village.VillageListener,
        SantaContext, LaunchCountdown.LaunchCountdownContext {

    protected static final String TAG = "SantaStart";
    private static final String VILLAGE_TAG = "VillageFragment";

    private AppInvitesFragment mInvitesFragment;
    private AudioPlayer mAudioPlayer;

    private boolean mResumed = false;

    private boolean mIsDebug = false;

    private Village mVillage;
    private VillageView mVillageView;
    private ImageView mVillageBackdrop;
    private View mLaunchButton;
    private View mCountdownView;

    private VerticalGridView mMarkers;
    private TvCardAdapter mCardAdapter;

    private LaunchCountdown mCountdown;

    // Load these values from resources when an instance of this activity is initialised.
    private static long OFFLINE_SANTA_DEPARTURE;
    private static long OFFLINE_SANTA_FINALARRIVAL;
    private static long UNLOCK_JETPACK;
    private static long UNLOCK_ROCKET;
    private static long UNLOCK_SNOWDOWN;
    private static long UNLOCK_VIDEO_1;
    private static long UNLOCK_VIDEO_15;
    private static long UNLOCK_VIDEO_23;

    // Server controlled flags
    private long mOffset = 0;
    private boolean mFlagSwitchOff = false;
    private boolean mFlagDisableJetpack = false;
    private boolean mFlagDisableRocket = false;
    private boolean mFlagDisableSnowdown = false;

    private String[] mVideoList = new String[] { null, null, null };

    private boolean mHaveGooglePlayServices = false;
    private long mFirstDeparture;
    private long mFinalArrival;

    // Handler for scheduled UI updates
    private Handler mHandler = new Handler();

    // Waiting for data from the API (no data or data is outdated)
    private boolean mWaitingForApi = true;

    // Service integration
    private Messenger mService = null;

    private boolean mIsBound = false;
    private Messenger mMessenger;

    // request code for games Activities
    private static final int RC_STARTUP = 1111;

    // Permission request codes
    private static final int RC_DEBUG_PERMS = 1;

    private FirebaseAnalytics mMeasurement;
    private boolean mLaunchingChild = false;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestPermissionsIfDebugModeEnabled();

        // Glide's pretty aggressive at caching images, so get the 8888 preference in early.
        if (!Glide.isSetup()) {
            Glide.setup(new GlideBuilder(getActivityContext()).setDecodeFormat(DecodeFormat.PREFER_ARGB_8888));
        }

        setContentView(R.layout.layout_startup_tv);
        loadResourceFields(getResources());

        mIsDebug = BuildConfig.DEBUG;

        mMessenger = new Messenger(new IncomingHandler(this));

        mCountdown = new LaunchCountdown(this);
        mCountdownView = findViewById(R.id.countdown_container);
        mAudioPlayer = new AudioPlayer(getApplicationContext());

        mVillageView = (VillageView) findViewById(R.id.villageView);
        mVillage = (Village) getSupportFragmentManager().findFragmentByTag(VILLAGE_TAG);
        if (mVillage == null) {
            mVillage = new Village();
            getSupportFragmentManager().beginTransaction().add(mVillage, VILLAGE_TAG).commit();
        }
        mVillageBackdrop = (ImageView) findViewById(R.id.villageBackground);
        mLaunchButton = findViewById(R.id.launch_button);
        mLaunchButton.setOnClickListener(this);

        mMarkers = (VerticalGridView) findViewById(R.id.markers);
        initialiseViews();

        mHaveGooglePlayServices = checkGooglePlayServicesAvailable();

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

        // Initialize measurement
        mMeasurement = FirebaseAnalytics.getInstance(this);
        MeasurementManager.recordScreenView(mMeasurement, getString(R.string.analytics_screen_village));

        // [ANALYTICS SCREEN]: Village
        AnalyticsManager.sendScreenView(R.string.analytics_screen_village);
        // set the initial states
        resetLauncherStates();
        // See if it was a voice action which triggered this activity and handle it
        onNewIntent(getIntent());
    }

    private void loadResourceFields(Resources res) {
        final long ms = 1000L;
        OFFLINE_SANTA_DEPARTURE = res.getInteger(R.integer.santa_takeoff) * ms;
        OFFLINE_SANTA_FINALARRIVAL = res.getInteger(R.integer.santa_arrival) * ms;
        mFinalArrival = OFFLINE_SANTA_FINALARRIVAL;
        mFirstDeparture = OFFLINE_SANTA_DEPARTURE;

        // Game unlock
        UNLOCK_JETPACK = res.getInteger(R.integer.unlock_jetpack) * ms;
        UNLOCK_ROCKET = res.getInteger(R.integer.unlock_rocket) * ms;
        UNLOCK_SNOWDOWN = res.getInteger(R.integer.unlock_snowdown) * ms;

        // Video unlock
        UNLOCK_VIDEO_1 = res.getInteger(R.integer.unlock_video1) * ms;
        UNLOCK_VIDEO_15 = res.getInteger(R.integer.unlock_video15) * ms;
        UNLOCK_VIDEO_23 = res.getInteger(R.integer.unlock_video23) * ms;
    }

    void initialiseViews() {
        mVillageView.setVillage(mVillage);

        // Initialize ListRowPresenter
        ListRowPresenter listRowPresenter = new ListRowPresenter(FocusHighlight.ZOOM_FACTOR_SMALL);
        listRowPresenter.setShadowEnabled(true);
        final int rowHeight = getResources().getDimensionPixelOffset(R.dimen.tv_marker_height_and_shadow);
        listRowPresenter.setRowHeight(rowHeight);

        // Initialize ListRow
        TvCardPresenter presenter = new TvCardPresenter(this);
        mCardAdapter = new TvCardAdapter(this, presenter);
        ListRow listRow = new ListRow(mCardAdapter);

        // Initialize ObjectAdapter for ListRow
        ArrayObjectAdapter arrayObjectAdapter = new ArrayObjectAdapter(listRowPresenter);
        arrayObjectAdapter.add(listRow);

        // Initialized Debug menus only for debug build.
        if (mIsDebug) {
            addDebugMenuListRaw(arrayObjectAdapter);
        }

        // set ItemBridgeAdapter to RecyclerView
        ItemBridgeAdapter adapter = new ItemBridgeAdapter(arrayObjectAdapter);
        mMarkers.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);
        mMarkers.setAdapter(adapter);
    }

    private void addDebugMenuListRaw(ArrayObjectAdapter objectAdapter) {

        mMarkers.setPadding(mMarkers.getPaddingLeft(), mMarkers.getPaddingTop() + 150, mMarkers.getPaddingRight(),
                mMarkers.getPaddingBottom());

        Presenter debugMenuPresenter = new Presenter() {
            @Override
            public ViewHolder onCreateViewHolder(ViewGroup parent) {
                TextView tv = new TextView(parent.getContext());
                ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(200, 150);
                tv.setLayoutParams(params);
                tv.setGravity(Gravity.CENTER);
                tv.setBackgroundColor(getResources().getColor(R.color.SantaBlueDark));
                tv.setFocusableInTouchMode(false);
                tv.setFocusable(true);
                tv.setClickable(true);
                return new ViewHolder(tv);
            }

            @Override
            public void onBindViewHolder(ViewHolder viewHolder, Object item) {
                ((TextView) viewHolder.view).setText((String) item);
                viewHolder.view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        final String text = ((TextView) v).getText().toString();
                        if (text.contains("Enable Tracker")) {
                            enableTrackerMode(true);
                        } else if (text.contains("Enable CountDown")) {
                            startCountdown(SantaPreferences.getCurrentTime());
                        } else {
                            mIsDebug = false;
                            initialiseViews();
                            resetLauncherStates();
                        }
                    }
                });
            }

            @Override
            public void onUnbindViewHolder(ViewHolder viewHolder) {

            }
        };

        ObjectAdapter debugMenuAdapter = new ObjectAdapter(debugMenuPresenter) {

            private final String[] mMenuString = { "Enable Tracker", "Enable CountDown", "Hide DebugMenu" };

            @Override
            public int size() {
                return mMenuString.length;
            }

            @Override
            public Object get(int position) {
                return mMenuString[position];
            }
        };

        ListRow debugMenuListRow = new ListRow(debugMenuAdapter);
        objectAdapter.add(debugMenuListRow);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void requestPermissionsIfDebugModeEnabled() {
        // If debug mode is enabled in debug_settings.xml, and we don't yet have storage perms, ask.
        if (getResources().getBoolean(R.bool.prompt_for_sdcard_perms) && ContextCompat.checkSelfPermission(this,
                Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
                    RC_DEBUG_PERMS);
        }
    }

    // see http://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances/
    @Override
    protected void onNewIntent(Intent intent) {
        setIntent(intent);
    }

    @Override
    protected void onResume() {
        super.onResume();
        showColorMask(false);
        mResumed = true;
    }

    @Override
    protected void onPause() {
        super.onPause();
        mResumed = false;
        mAudioPlayer.stopAll();

        cancelUIUpdate();
    }

    @Override
    protected void onStart() {
        super.onStart();
        registerWithService();

        // Check for App Invites
        mInvitesFragment.getInvite(new AppInvitesFragment.GetInvitationCallback() {
            @Override
            public void onInvitation(String invitationId, String deepLink) {
                Log.d(TAG, "onInvitation: " + deepLink);
            }
        }, true);

        initialiseViews();
        resetLauncherStates();
    }

    private void resetLauncherStates() {
        // Start only if play services are available
        if (mHaveGooglePlayServices) {
            stateNoData();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        unregisterFromService();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (mResumed && hasFocus && !AccessibilityUtil.isTouchAccessiblityEnabled(this)) {
            mAudioPlayer.playTrackExclusive(R.raw.village_music, true);
        }
    }

    /**
     * Move to 'no valid data' state ("offline"). No further locations, rely on local offline data
     * only.
     */
    private void stateNoData() {
        Log.d(TAG, "Santa is offline.");

        // Enable/disable pins and nav drawer
        updateNavigation();

        // Schedule UI Updates
        scheduleUIUpdate();

        // Note that in the "no data" state, this may or may not include the TIME_OFFSET, depending
        // on whether we've had a successful API call and still have the data. We can't use
        // System.currentTimeMillis() as it *will* ignore TIME_OFFSET.
        final long time = SantaPreferences.getCurrentTime();

        AbstractLaunch launchSanta = mCardAdapter.getLauncher(CardAdapter.SANTA);

        if (time < OFFLINE_SANTA_DEPARTURE) {
            // Santa hasn't departed yet, show countdown
            launchSanta.setState(AbstractLaunch.STATE_LOCKED);
            startCountdown(OFFLINE_SANTA_DEPARTURE);
        } else if (time >= OFFLINE_SANTA_DEPARTURE && time < OFFLINE_SANTA_FINALARRIVAL) {
            // Santa should have already left, but no data yet, hide countdown and show message
            stopCountdown();
            enableTrackerMode(false);
            launchSanta.setState(AbstractLaunch.STATE_DISABLED);
        } else {
            // Post Christmas
            stopCountdown();
            enableTrackerMode(false);
            launchSanta.setState(AbstractLaunch.STATE_FINISHED);
        }
    }

    /**
     * Move to 'data' (online) state.
     */
    private void stateData() {
        Log.d(TAG, "Santa is online.");

        // Enable/disable pins and nav drawer
        updateNavigation();

        // Schedule next UI update
        scheduleUIUpdate();

        long time = SantaPreferences.getCurrentTime();

        AbstractLaunch launchSanta = mCardAdapter.getLauncher(CardAdapter.SANTA);
        // Is Santa finished?
        if (time > mFirstDeparture && time < OFFLINE_SANTA_FINALARRIVAL) {
            // Santa should be travelling, enable map and hide countdown
            enableTrackerMode(true);

            if (mFlagSwitchOff) {
                // Kill-switch triggered, disable button
                launchSanta.setState(AbstractLaunch.STATE_DISABLED);
            } else if (time > mFinalArrival) {
                // No data
                launchSanta.setState(AbstractLaunch.STATE_DISABLED);
            } else {
                launchSanta.setState(AbstractLaunch.STATE_READY);
            }

        } else if (time < mFirstDeparture) {
            // Santa hasn't taken off yet, start count-down and schedule
            // notification to first departure, hide buttons

            startCountdown(mFirstDeparture);
            launchSanta.setState(AbstractLaunch.STATE_LOCKED);

        } else {
            // Post Christmas, hide countdown and buttons
            launchSanta.setState(AbstractLaunch.STATE_FINISHED);
            stopCountdown();
            enableTrackerMode(false);
        }

    }

    public void enableTrackerMode(boolean showLaunchButton) {
        mCountdown.cancel();
        mVillageBackdrop.setImageResource(R.drawable.village_bg_launch);
        mVillage.setPlaneEnabled(false);
        mLaunchButton.setVisibility(showLaunchButton ? View.VISIBLE : View.GONE);
        mCountdownView.setVisibility(View.GONE);
    }

    public void startCountdown(long time) {
        mCountdown.startTimer(time - SantaPreferences.getCurrentTime());
        mVillageBackdrop.setImageResource(R.drawable.village_bg_countdown);
        mVillage.setPlaneEnabled(true);
        mLaunchButton.setVisibility(View.GONE);
        mCountdownView.setVisibility(View.VISIBLE);
    }

    public void stopCountdown() {
        mCountdown.cancel();
        mCountdownView.setVisibility(View.GONE);
    }

    /*
     * Village Markers
     */
    private void updateNavigation() {
        // Games
        mCardAdapter.getLauncher(TvCardAdapter.JETPACK)
                .setState(getGamePinState(mFlagDisableJetpack, UNLOCK_JETPACK));
        mCardAdapter.getLauncher(TvCardAdapter.ROCKET).setState(getGamePinState(mFlagDisableRocket, UNLOCK_ROCKET));
        mCardAdapter.getLauncher(TvCardAdapter.SNOWDOWN)
                .setState(getGamePinState(mFlagDisableSnowdown, UNLOCK_SNOWDOWN));

        ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO01)).setVideo(mVideoList[0], UNLOCK_VIDEO_1);
        ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO15)).setVideo(mVideoList[1], UNLOCK_VIDEO_15);
        ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO23)).setVideo(mVideoList[2], UNLOCK_VIDEO_23);
    }

    private int getGamePinState(boolean disabledFlag, long unlockTime) {
        if (disabledFlag) {
            return AbstractLaunch.STATE_HIDDEN;
        } else if (!disabledFlag && SantaPreferences.getCurrentTime() < unlockTime) {
            return AbstractLaunch.STATE_LOCKED;
        } else {
            return AbstractLaunch.STATE_READY;
        }
    }

    /*
     * Scheduled UI update
     */

    /**
     * Schedule a call to {@link #stateData()} or {@link #stateNoData()} at the next time at which
     * the UI should be updated (games become available, Santa takes off, Santa is finished).
     */
    private void scheduleUIUpdate() {
        // cancel scheduled update
        cancelUIUpdate();

        final long delay = calculateNextUiUpdateDelay();
        if (delay > 0 && delay < Long.MAX_VALUE) {
            // schedule if delay is in the future
            mHandler.postDelayed(mUpdateUiRunnable, delay);
        }
    }

    private long calculateNextUiUpdateDelay() {

        final long time = SantaPreferences.getCurrentTime();

        final long departureDelay = mFirstDeparture - time;
        final long arrivalDelay = mFinalArrival - time;

        // if disable flag is toggled, exclude from calculation
        final long[] delays = new long[] { mFlagDisableJetpack ? Long.MAX_VALUE : UNLOCK_JETPACK - time,
                mFlagDisableRocket ? Long.MAX_VALUE : UNLOCK_ROCKET - time,
                mFlagDisableSnowdown ? Long.MAX_VALUE : UNLOCK_SNOWDOWN - time, departureDelay, arrivalDelay };

        // find lowest delay, but only count positive values or zero (ie. that are in the future)
        long delay = Long.MAX_VALUE;
        for (final long x : delays) {
            if (x >= 0) {
                delay = Math.min(delay, x);
            }
        }

        return delay;
    }

    private void cancelUIUpdate() {
        mHandler.removeCallbacksAndMessages(null);
    }

    private Runnable mUpdateUiRunnable = new Runnable() {
        @Override
        public void run() {
            if (!mWaitingForApi) {
                stateData();
            } else {
                stateNoData();
            }
        }
    };

    /*
     * Google Play Services - from
     * http://code.google.com/p/google-api-java-client/source/browse/tasks-android-sample/src/main/
     *     java/com/google/api/services/samples/tasks/android/TasksSample.java?repo=samples
     */

    /**
     * Check that Google Play services APK is installed and up to date.
     */
    private boolean checkGooglePlayServicesAvailable() {
        final int connectionStatusCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) {
            showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode);
            return false;
        }
        return (connectionStatusCode == ConnectionResult.SUCCESS);
    }

    private void showGooglePlayServicesAvailabilityErrorDialog(final int connectionStatusCode) {

        Dialog dialog = GooglePlayServicesUtil.getErrorDialog(connectionStatusCode, this, 0);
        dialog.show();
        dialog.setOnDismissListener(new Dialog.OnDismissListener() {

            public void onDismiss(DialogInterface dialog) {
                finish();
            }
        });

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.launch_button:
            launchTracker();
            break;
        }
    }

    @Override
    public void playSoundOnce(int resSoundId) {
        mAudioPlayer.playTrack(resSoundId, false);
    }

    @Override
    public Context getActivityContext() {
        return this;
    }

    @Override
    public void launchActivity(Intent intent) {
        launchActivityInternal(intent, null, 0);
    }

    @Override
    public void launchActivityDelayed(final Intent intent, final View v) {
        launchActivityInternal(intent, v, 200);
    }

    @Override
    public View getCountdownView() {
        return findViewById(R.id.countdown_container);
    }

    @Override
    public void onCountdownFinished() {
        if (!mWaitingForApi) {
            stateData();
        } else {
            stateNoData();
        }
    }

    /** Attempt to launch the tracker, if available. */
    public void launchTracker() {
        AbstractLaunch launch = mCardAdapter.getLauncher(CardAdapter.SANTA);
        if (launch instanceof LaunchSanta) {
            LaunchSanta tracker = (LaunchSanta) launch;

            AnalyticsManager.sendEvent(R.string.analytics_event_category_launch,
                    R.string.analytics_launch_action_village);

            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch),
                    getString(R.string.analytics_launch_action_village));

            tracker.onClick(mLaunchButton);
        }
    }

    /*
     * Service communication
     */

    private static class IncomingHandler extends Handler {

        private final WeakReference<TvStartupActivity> mActivityRef;

        public IncomingHandler(TvStartupActivity activity) {
            mActivityRef = new WeakReference<>(activity);
        }

        /*
        Order in which messages are received: Data updates, State of Service [Idle, Error]
         */
        @Override
        public void handleMessage(Message msg) {
            SantaLog.d(TAG, "message=" + msg.what);
            final TvStartupActivity activity = mActivityRef.get();
            if (activity == null) {
                return;
            }

            switch (msg.what) {
            case SantaServiceMessages.MSG_SERVICE_STATUS:
                // Current state of service, received once when connecting
                activity.onSantaServiceStateUpdate(msg.arg1);
                break;
            case SantaServiceMessages.MSG_INPROGRESS_UPDATE_ROUTE:
                // route is about to be updated
                activity.onRouteUpdateStart();
                break;
            case SantaServiceMessages.MSG_UPDATED_ROUTE:
                // route data has been updated
                activity.onRouteDataUpdateFinished();
                break;
            case SantaServiceMessages.MSG_UPDATED_ONOFF:
                activity.mFlagSwitchOff = (msg.arg1 == SantaServiceMessages.SWITCH_OFF);
                activity.onDataUpdate();
                break;
            case SantaServiceMessages.MSG_UPDATED_TIMES:
                Bundle b = (Bundle) msg.obj;
                activity.mOffset = b.getLong(SantaServiceMessages.BUNDLE_OFFSET);
                SantaPreferences.cacheOffset(activity.mOffset);
                activity.mFinalArrival = b.getLong(SantaServiceMessages.BUNDLE_FINAL_ARRIVAL);
                activity.mFirstDeparture = b.getLong(SantaServiceMessages.BUNDLE_FIRST_DEPARTURE);
                activity.onDataUpdate();
                break;
            case SantaServiceMessages.MSG_UPDATED_GAMES:
                final int arg = msg.arg1;
                activity.mFlagDisableJetpack = (arg
                        & SantaServiceMessages.MSG_FLAG_GAME_JETPACK) == SantaServiceMessages.MSG_FLAG_GAME_JETPACK;
                activity.mFlagDisableRocket = (arg
                        & SantaServiceMessages.MSG_FLAG_GAME_ROCKET) == SantaServiceMessages.MSG_FLAG_GAME_ROCKET;
                activity.mFlagDisableSnowdown = (arg
                        & SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN) == SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN;
                activity.onDataUpdate();
                break;
            case SantaServiceMessages.MSG_UPDATED_VIDEOS:
                Bundle data = msg.getData();
                activity.mVideoList = data.getStringArray(SantaServiceMessages.BUNDLE_VIDEOS);
                activity.onDataUpdate();
                break;
            case SantaServiceMessages.MSG_ERROR:
                // Error accessing the API, ignore because there is data.
                activity.onApiSuccess();
                break;
            case SantaServiceMessages.MSG_ERROR_NODATA:
                activity.stateNoData();
                break;
            case SantaServiceMessages.MSG_SUCCESS:
                activity.onApiSuccess();
                break;
            default:
                super.handleMessage(msg);
                break;
            }

        }
    }

    /**
     * Handle the state of the SantaService when first connecting to it.
     */
    private void onSantaServiceStateUpdate(int state) {
        switch (state) {
        case SantaServiceMessages.STATUS_IDLE:
            // Service is idle, data should be uptodate
            mWaitingForApi = false;
            stateData();
            break;
        case SantaServiceMessages.STATUS_IDLE_NODATA:
            mWaitingForApi = true;
            stateNoData();
            break;
        case SantaServiceMessages.STATUS_ERROR_NODATA:
            // Service is in error state and there is no valid data
            mWaitingForApi = true;
            stateNoData();
        case SantaServiceMessages.STATUS_ERROR:
            // Service is in error state and waiting for another attempt to access API
            mWaitingForApi = true;
            stateNoData();
        case SantaServiceMessages.STATUS_PROCESSING:
            // Service is busy processing an update, wait for success and ignore this state
            mWaitingForApi = true;
            break;

        }
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);

            //reply with local Messenger to establish bi-directional communication
            Message msg = Message.obtain(null, SantaServiceMessages.MSG_SERVICE_REGISTER_CLIENT);
            msg.replyTo = mMessenger;
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                // Could not connect to Service, connection will be terminated soon.
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            mIsBound = false;
        }
    };

    private void onApiSuccess() {

        if (mWaitingForApi) {
            mWaitingForApi = false;
            stateData();
        }
    }

    private void onRouteDataUpdateFinished() {
        // switch to 'online' mode, data has been loaded
        if (!mWaitingForApi) {
            stateData();
        }
    }

    private void onRouteUpdateStart() {
        // temporarily switch back to offline mode until route update has finished
        if (!mWaitingForApi) {
            stateNoData();
        }
    }

    private void onDataUpdate() {
        if (!mWaitingForApi) {
            stateData();
        }
    }

    private void registerWithService() {
        bindService(new Intent(this, SantaService.class), mConnection, Context.BIND_AUTO_CREATE);
        mIsBound = true;
    }

    private void unregisterFromService() {
        if (mIsBound) {
            if (mService != null) {
                Message msg = Message.obtain(null, SantaServiceMessages.MSG_SERVICE_UNREGISTER_CLIENT);
                msg.replyTo = mMessenger;
                try {
                    mService.send(msg);
                } catch (RemoteException e) {
                    // ignore if service is not available
                }
            }
            unbindService(mConnection);
            mIsBound = false;
        }
    }

    private synchronized void launchActivityInternal(final Intent intent, View srcView, long delayMs) {

        if (!mLaunchingChild) {
            mLaunchingChild = true;

            // stop timer
            if (mCountdown != null) {
                mCountdown.cancel();
            }

            if (srcView != null) {
                playCircularRevealTransition(srcView);
            }

            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    startActivityForResult(intent, RC_STARTUP);
                    mLaunchingChild = false;
                }
            }, delayMs);
        }
    }

    final Rect mSrcRect = new Rect();

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void playCircularRevealTransition(View srcView) {

        showColorMask(true);
        View mask = findViewById(R.id.content_mask);
        srcView.getGlobalVisibleRect(mSrcRect);
        Animator anim = ViewAnimationUtils.createCircularReveal(mask, mSrcRect.centerX(), mSrcRect.centerY(), 0.f,
                mask.getWidth());
        anim.start();
    }

    private void showColorMask(boolean show) {
        int visibility = show ? View.VISIBLE : View.INVISIBLE;
        (findViewById(R.id.content_mask)).setVisibility(visibility);
    }
}