com.google.android.apps.santatracker.map.SantaMapActivity.java Source code

Java tutorial

Introduction

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

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.ImageButton;
import android.widget.Toast;

import com.google.android.apps.santatracker.R;
import com.google.android.apps.santatracker.SantaApplication;
import com.google.android.apps.santatracker.cast.NotificationDataCastManager;
import com.google.android.apps.santatracker.data.AllDestinationCursorLoader;
import com.google.android.apps.santatracker.data.Destination;
import com.google.android.apps.santatracker.data.DestinationCursor;
import com.google.android.apps.santatracker.data.PresentCounter;
import com.google.android.apps.santatracker.data.SantaPreferences;
import com.google.android.apps.santatracker.data.StreamCursor;
import com.google.android.apps.santatracker.data.StreamCursorLoader;
import com.google.android.apps.santatracker.data.StreamEntry;
import com.google.android.apps.santatracker.map.cardstream.CardAdapter;
import com.google.android.apps.santatracker.map.cardstream.DashboardFormats;
import com.google.android.apps.santatracker.map.cardstream.DashboardViewHolder;
import com.google.android.apps.santatracker.map.cardstream.SeparatorDecoration;
import com.google.android.apps.santatracker.map.cardstream.TrackerCard;
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.Intents;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.android.apps.santatracker.util.SantaLog;
import com.google.firebase.analytics.FirebaseAnalytics;

import java.lang.ref.WeakReference;

/**
 * Map Activity that shows Santa's destinations and his path on and after
 * Christmas.
 */
public class SantaMapActivity extends AppCompatActivity implements SantaMapFragment.SantaMapInterface {

    private static String ARRIVING_IN, DEPARTING_IN, NO_NEXT_DESTINATION, CURRENT_LOCATION, NEXT_LOCATION;

    // countdown update frequency (in ms)
    private static final int DESTINATION_COUNTDOWN_UPDATEINTERVAL = 1000;
    // countdown is shown every 10 seconds
    private static final int DESTINATION_COUNTDOWN_DISPLAY_INTERVAL = 1000 * 10;

    // Percentage of presents to hand out when travelling between destinations
    // (the rest is handed out when the destination is reached)
    public static final double FACTOR_PRESENTS_TRAVELLING = 0.3;

    // time to allow the screen to stay active
    private static final long SCREEN_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5m

    protected static final String TAG = "SantaActivity";

    private static final int LOADER_DESTINATIONS = 1;
    private static final int LOADER_STREAM = 2;

    private CountDownTimer mTimer;
    private PresentCounter mPresents = new PresentCounter();
    private SantaCamTimeout mSantaCamTimeout;
    protected DestinationCursor mDestinations;

    private Handler mScreenLock = new Handler();
    private Runnable mScreenUnlock = new Runnable() {
        @Override
        public void run() {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
    };

    // Fragments
    protected SantaMapFragment mMapFragment;

    // Activity State
    private boolean mHasDataLoaded = false;
    private boolean mIsLive = false;
    private boolean mResumed = false;
    private boolean mIgnoreNextUpdate = false;

    // Resource Strings
    private static String LOST_CONTACT_STRING;

    private static String ANNOUNCE_TRAVEL_TO;
    private static String ANNOUNCE_ARRIVED_AT;

    // Server controlled data
    protected boolean mSwitchOff = true;
    protected long mOffset = 0L;
    protected long mFirstDeparture = 0L;
    protected long mFinalArrival = 0L;
    protected long mFinalDeparture = 0L;
    protected boolean mFlagDisableCast = true;

    // Toggle when error accessing API and need to return to Village with error message when out of
    // locations
    private boolean mHaveApiError = false;

    // Service integration
    private Messenger mService = null;
    private boolean mIsBound = false;
    private final Messenger mMessenger = new Messenger(new IncomingHandler(this));

    // Stream
    private StreamEntry mNextStreamEntry = null;
    protected StreamCursor mStream;

    private CardAdapter mAdapter;

    private RecyclerView mRecyclerView;

    // Support for StreetView intent on device
    private boolean mSupportStreetView = false;

    private AccessibilityManager mAccessibilityManager;

    private SantaCamButton mSantaCamButton;
    private BottomSheetBehavior mBottomSheetBehavior;

    private NotificationDataCastManager mCastManager;

    private FirebaseAnalytics mMeasurement;
    private LinearLayoutManager mLayoutManager;

    private ImageButton mButtonTop;

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

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

        // [ANALYTICS SCREEN]: Tracker
        AnalyticsManager.sendScreenView(R.string.analytics_screen_tracker);

        // Needs to be called before setting the content view
        supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);

        setContentView(R.layout.activity_map);

        // Set up timer to remove screen lock
        resetScreenTimer();

        mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        Resources resources = getResources();
        if (actionBar != null) {
            // set visibility flags *AFTER* values have been set,
            // otherwise nothing is displayed on Galaxy devices
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeButtonEnabled(true);
            actionBar.setDisplayShowTitleEnabled(false);
        }

        LOST_CONTACT_STRING = resources.getString(R.string.lost_contact_with_santa);
        ANNOUNCE_ARRIVED_AT = resources.getString(R.string.santa_is_now_arriving_in_x);
        ARRIVING_IN = resources.getString(R.string.arriving_in);
        DEPARTING_IN = resources.getString(R.string.departing_in);
        NO_NEXT_DESTINATION = resources.getString(R.string.no_next_destination);
        CURRENT_LOCATION = resources.getString(R.string.current_location);
        NEXT_LOCATION = resources.getString(R.string.next_destination);

        // Concatenate String for 'travel to' announcement
        StringBuilder sb = new StringBuilder();
        sb.append(resources.getString(R.string.in_transit));
        sb.append(" ");
        sb.append(resources.getString(R.string.next_destination));
        sb.append(" %s");
        ANNOUNCE_TRAVEL_TO = sb.toString();
        sb.setLength(0);

        // Get all fragments
        mMapFragment = (SantaMapFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_map);

        mButtonTop = (ImageButton) findViewById(R.id.top);
        mButtonTop.setOnClickListener(mOnClickListener);
        mRecyclerView = (RecyclerView) findViewById(R.id.stream);
        mSupportStreetView = Intents.canHandleStreetView(this);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.addItemDecoration(new SeparatorDecoration(this));
        mRecyclerView.addOnScrollListener(mOnScrollListener);
        mAdapter = new CardAdapter(getApplicationContext(), mCardAdapterListener);
        mAdapter.setHasStableIds(true);
        mRecyclerView.setAdapter(mAdapter);

        if (NotificationDataCastManager.checkGooglePlayServices(this)) {
            mCastManager = SantaApplication.getCastManager(this);
        }

        // Santacam button
        mSantaCamButton = (SantaCamButton) findViewById(R.id.santacam);
        mSantaCamButton.setOnClickListener(mOnClickListener);
        if (mMapFragment.isInSantaCam()) {
            mSantaCamButton.setVisibility(View.GONE);
        }

        View bottomSheet = findViewById(R.id.bottom_sheet);
        if (bottomSheet != null) {
            mBottomSheetBehavior = (BottomSheetBehavior) ((CoordinatorLayout.LayoutParams) bottomSheet
                    .getLayoutParams()).getBehavior();
            mBottomSheetBehavior.setBottomSheetListener(mBottomSheetListener);
        }

        findViewById(R.id.main_touchinterceptor).setOnTouchListener(mInterceptorListener);
    }

    private void initialiseOnChristmas() {
        mSantaCamTimeout = new SantaCamTimeout(mMapFragment, mSantaCamButton);
    }

    private OnTouchListener mInterceptorListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mMapFragment.disableSantaCam();
            notifyCamInteraction();
            return false; // propagate touch event to map
        }
    };

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (mResumed && hasFocus) {
            mMapFragment.resumeAudio();
        }
    }

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

        resetScreenTimer();

        if (mCastManager == null && NotificationDataCastManager.checkGooglePlayServices(this)) {
            mCastManager = SantaApplication.getCastManager(this);
        }

        if (mCastManager != null) {
            mCastManager.incrementUiCounter();
        }

        if (mBottomSheetBehavior != null) {
            adjustMapPaddings(mBottomSheetBehavior.getState());
        }

    }

    @Override
    protected void onStart() {
        super.onStart();
        bindService(new Intent(this, SantaService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

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

        // unregister and unbind from Services
        unregisterFromService();
    }

    @Override
    protected void onPause() {
        mResumed = false;

        // stop the countdown timer if running
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }

        cancelScreenTimer();

        // stop santa cam
        onSantacamStateChange(false);

        if (mCastManager != null) {
            mCastManager.decrementUiCounter();
        }

        // Reset state
        mHasDataLoaded = false;
        mIsLive = false;
        mDestinations = null;
        mStream = null;

        super.onPause();
    }

    @Override
    public void onUserInteraction() {
        resetScreenTimer();
    }

    private void resetScreenTimer() {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        mScreenLock.removeCallbacks(mScreenUnlock);
        mScreenLock.postDelayed(mScreenUnlock, SCREEN_IDLE_TIMEOUT_MS);
    }

    private void cancelScreenTimer() {
        mScreenLock.removeCallbacks(mScreenUnlock);
    }

    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
                }
                mService = null;
            }
            unbindService(mConnection);
            mIsBound = false;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_map, menu);

        // Add cast button
        if (mCastManager != null) {
            mCastManager.addMediaRouterButton(menu, R.id.media_route_menu_item);
        }

        return super.onCreateOptionsMenu(menu);
    }

    private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() {

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            switch (id) {
            case LOADER_DESTINATIONS: {
                return new AllDestinationCursorLoader(SantaMapActivity.this);
            }
            case LOADER_STREAM: {
                return new StreamCursorLoader(getApplicationContext(), false);
            }
            }
            return null;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
            final int id = loader.getId();
            if (id == LOADER_DESTINATIONS) {
                // loader finished loading cursor, setup the helper
                mDestinations = new DestinationCursor(cursor);
                start();
            } else if (id == LOADER_STREAM) {
                mStream = new StreamCursor(cursor);
                addPastStream();
            }
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            switch (loader.getId()) {
            case LOADER_DESTINATIONS:
                mDestinations = null;
                break;
            case LOADER_STREAM:
                mStream = null;
                break;
            }
        }
    };

    @Override
    public boolean onSupportNavigateUp() {
        returnToStartupActivity();
        return true;
    }

    @Override
    public void onBackPressed() {
        // Close the bottom sheet if it is open.
        if (mBottomSheetBehavior != null && mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
            if (mRecyclerView != null) {
                mRecyclerView.smoothScrollToPosition(0);
            }
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        } else {
            super.onBackPressed();
        }
    }

    /**
     * Finishes the current activity and starts the startup activity.
     */
    protected void returnToStartupActivity() {
        finish();
    }

    /**
     * Call when the map or destinations are ready. Checks if both are initialised and calls
     * startTracking if ready.
     */
    private void start() {
        // check that the cursor and map have been initialised
        if (mDestinations == null || !mMapFragment.isInitialised()) {
            return;
        }
        if (!mIsLive) {
            startTracking();
        }
    }

    /**
     * Moves the destination cursor from to the current destination and adds all visited locations
     * to the map.
     */
    protected void addVisitedLocations() {
        // add all visited destinations from the cursors current position to the map
        while (mDestinations.hasNext() && mDestinations.isInPast(SantaPreferences.getCurrentTime())) {
            Destination destination = mDestinations.getCurrent();
            mMapFragment.addLocation(destination);
            mAdapter.addDestination(false, destination, mSupportStreetView);
            mDestinations.moveToNext();
        }
        mAdapter.notifyDataSetChanged();
    }

    /**
     * Displays a friendly toast and returns to the startup activity with the given message.
     */
    private void handleErrorFinish() {
        Log.d(TAG, "Lost contact, returning to village.");
        Toast.makeText(getApplicationContext(), LOST_CONTACT_STRING, Toast.LENGTH_LONG).show();
        returnToStartupActivity();
    }

    /**
     * Called when the map has been initialised and is ready to be used.
     */
    public void onMapInitialised() {
        // map initialised, start tracking
        start();
    }

    /**
     * Start tracking Santa. If Santa is already finished, return to the main launcher. All
     * destinations from the cursor's current position to the current time are added to the map and
     * the map is restored to its
     */
    protected void startTracking() {
        mIsLive = true;

        final long time = SantaPreferences.getCurrentTime();
        // Return to launch activity if Santa hasn't left yet or has already left for the next year
        if (time >= mFirstDeparture && time < mFinalArrival) {
            // It's Christmas and Santa is travelling
            startOnChristmas();
        } else {
            // Any other state, return back to Village
            returnToStartupActivity();
        }

    }

    private void startOnChristmas() {
        SantaLog.d(TAG, "start on christmas");
        initialiseOnChristmas();
        addVisitedLocations();
        // Load the stream data once all past locations have been added, based on the last visited
        // location
        getSupportLoaderManager().restartLoader(LOADER_STREAM, null, mLoaderCallbacks);
        // determine santa's status - visiting or travelling?
        if (!mDestinations.hasNext()) {
            // sanity check - already finished, no destinations left
            returnToStartupActivity();
        } else if (mDestinations.isVisiting(SantaPreferences.getCurrentTime())) {
            // currently visiting a location
            Destination d = mDestinations.getCurrent();
            // move santa marker
            visitDestination(d, false);
            setNextDestination(d, mSupportStreetView);
            // enable santa cam and center on santa
            mMapFragment.enableSantaCam(true);
        } else {
            // not currently visiting a location, en route to next destination
            // enable santacam, but do not move camera - this is done
            // through a callback once the santa animation has started
            mMapFragment.enableSantaCam(true);
            // get the destination and animate santa
            Destination d = mDestinations.getCurrent();
            // animate to next destination
            // marker at origin has already been set above, does not need to be
            // added again.
            travelToDestination(null, d);
        }
    }

    /**
     * Call when Santa is en route to the given destination.
     */
    private void travelToDestination(final Destination origin, final Destination nextDestination) {

        if (origin != null) {
            // add marker at origin position to map.
            mMapFragment.addLocation(origin);
        }

        // check if finished
        if (mDestinations.isFinished() || nextDestination == null) {
            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_tracker),
                    getString(R.string.analytics_tracker_action_finished),
                    getString(R.string.analytics_tracker_error_nodata));

            // [ANALYTICS EVENT]: Error NoData after API error
            AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                    R.string.analytics_tracker_action_finished, R.string.analytics_tracker_error_nodata);

            // No more destinations left, return to village
            returnToStartupActivity();
            return;
        }

        if (mHaveApiError) {
            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_tracker),
                    getString(R.string.analytics_tracker_action_error),
                    getString(R.string.analytics_tracker_error_nodata));

            // [ANALYTICS EVENT]: Error NoData after API error
            AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                    R.string.analytics_tracker_action_error, R.string.analytics_tracker_error_nodata);
            handleErrorFinish();
            return;
        }

        final String nextString = DashboardFormats.formatDestination(nextDestination);
        setNextLocation(nextString);
        setNextDestination(nextDestination, mSupportStreetView);
        setCurrentLocation(null);

        // get the previous position
        Destination previous = mDestinations.getPrevious();

        SantaLog.d(TAG, "Travel: " + (origin != null ? origin.identifier : "null") + " -> "
                + nextDestination.identifier + " prev=" + (previous != null ? previous.identifier : "null"));

        // if this is the very first location, move santa directly
        if (previous == null) {
            mMapFragment.setSantaVisiting(nextDestination, false);
            mPresents.init(0, nextDestination.presentsDelivered, nextDestination.arrival,
                    nextDestination.departure);
        } else {
            mMapFragment.setSantaTravelling(previous, nextDestination, false);
            // only hand out X% of presents during travel
            long presentsEnd = previous.presentsDelivered
                    + Math.round((nextDestination.presentsDeliveredAtDestination) * FACTOR_PRESENTS_TRAVELLING);
            mPresents.init(previous.presentsDelivered, presentsEnd, previous.departure, nextDestination.arrival);
        }

        // Notify dashboard to send accessibility event
        AccessibilityUtil.announceText(String.format(ANNOUNCE_TRAVEL_TO, nextString), mRecyclerView,
                mAccessibilityManager);

        // cancel the countdown if it is already running
        if (mTimer != null) {
            mTimer.cancel();
        }
        mTimer = new CountDownTimer(nextDestination.arrival - SantaPreferences.getCurrentTime(),
                DESTINATION_COUNTDOWN_UPDATEINTERVAL) {

            @Override
            public void onTick(long millisUntilFinished) {
                countdownTick(millisUntilFinished);
            }

            @Override
            public void onFinish() {
                // reached destination - visit destination
                visitDestination(nextDestination, true);
            }
        };
        if (mResumed) {
            mTimer.start();
        }
    }

    private DashboardViewHolder getDashboardViewHolder() {
        return (DashboardViewHolder) mRecyclerView.findViewHolderForItemId(mAdapter.getDashboardId());
    }

    private void setNextLocation(final String s) {
        final String nextLocation = s == null ? NO_NEXT_DESTINATION : s;
        mAdapter.setNextLocation(nextLocation);
        final DashboardViewHolder holder = getDashboardViewHolder();
        if (null == holder) {
            return;
        }
        holder.location.post(new Runnable() {
            @Override
            public void run() {
                holder.locationLabel.setText(NEXT_LOCATION);
                holder.location.setText(nextLocation);
            }
        });
    }

    private void setNextDestination(Destination next, boolean showStreetView) {
        mAdapter.addDestination(false, next, showStreetView);
        mAdapter.notifyDataSetChanged();
    }

    /**
     * Call when Santa is to visit a location.
     */
    private void visitDestination(final Destination destination, boolean playSound) {

        // Only visit this location if there is a following destination
        // Otherwise out of data or at North Pole
        if (mDestinations.isLast()) {
            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_tracker),
                    getString(R.string.analytics_tracker_action_error),
                    getString(R.string.analytics_tracker_error_nodata));

            // [ANALYTICS EVENT]: Error NoData
            AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                    R.string.analytics_tracker_action_error, R.string.analytics_tracker_error_nodata);

            Toast.makeText(this, R.string.lost_contact_with_santa, Toast.LENGTH_LONG).show();
            returnToStartupActivity();
            return;
        }

        Destination nextDestination = mDestinations.getPeekNext();
        SantaLog.d(TAG, "Arrived: " + destination.identifier + " current=" + mDestinations.getCurrent().identifier
                + " next = " + nextDestination + " next id=" + nextDestination);

        // hand out the remaining presents for this location, explicit to ensure counter is always
        // in correct state and does not depend on anything else at runtime.
        final long presentsStart = destination.presentsDelivered - destination.presentsDeliveredAtDestination
                + Math.round((destination.presentsDeliveredAtDestination) * (1.0f - FACTOR_PRESENTS_TRAVELLING));
        mPresents.init(presentsStart, destination.presentsDelivered, destination.arrival, destination.departure);

        final String destinationString = DashboardFormats.formatDestination(destination);
        setCurrentLocation(destinationString);

        mMapFragment.setSantaVisiting(destination, playSound);

        // Notify dashboard to send accessibility event
        AccessibilityUtil.announceText(String.format(ANNOUNCE_ARRIVED_AT, destination.getPrintName()),
                mRecyclerView, mAccessibilityManager);

        // cancel the countdown if it is already running
        if (mTimer != null) {
            mTimer.cancel();
        }

        // Count down until departure
        mTimer = new CountDownTimer(destination.departure - SantaPreferences.getCurrentTime(),
                DESTINATION_COUNTDOWN_UPDATEINTERVAL) {

            @Override
            public void onTick(long millisUntilFinished) {
                countdownTick(millisUntilFinished);
            }

            @Override
            public void onFinish() {
                // finished at this destination, move to the next one
                travelToDestination(mDestinations.getCurrent(), mDestinations.getNext());
            }

        };
        if (mResumed) {
            mTimer.start();
        }
    }

    private void setDestinationPhotoDisabled(boolean disablePhoto) {
        mAdapter.setDestinationPhotoDisabled(disablePhoto);
    }

    private void setPresentsDelivered(final String presentsDelivered) {
        DashboardViewHolder holder = getDashboardViewHolder();
        if (holder == null) {
            return;
        }
        holder.presents.setText(presentsDelivered);
    }

    private void setCountdown(String countdown) {
        DashboardViewHolder holder = getDashboardViewHolder();
        if (holder == null) {
            return;
        }
        holder.countdown.setText(countdown);
    }

    private void setCurrentLocation(String location) {
        final DashboardViewHolder holder = getDashboardViewHolder();
        if (holder == null) {
            return;
        }
        if (TextUtils.isEmpty(location)) {
            holder.countdownLabel.setText(ARRIVING_IN);
        } else {
            holder.countdownLabel.setText(DEPARTING_IN);
            holder.locationLabel.setText(CURRENT_LOCATION);
            holder.location.setText(location);
        }
    }

    private void countdownTick(long millisUntilFinished) {
        final long presents = mPresents.getPresents(SantaPreferences.getCurrentTime());
        final String presentsString = DashboardFormats.formatPresents(presents);
        setPresentsDelivered(presentsString);
        final DashboardViewHolder holder = getDashboardViewHolder();
        if (holder != null) {
            if ((millisUntilFinished / DESTINATION_COUNTDOWN_DISPLAY_INTERVAL) % 2 == 1) {
                if (holder.presentsContainer.getVisibility() != View.VISIBLE) {
                    holder.presentsContainer.setVisibility(View.VISIBLE);
                    holder.countdownContainer.setVisibility(View.INVISIBLE);
                }
            } else {
                setCountdown(DashboardFormats.formatCountdown(millisUntilFinished));
                if (holder.countdownContainer.getVisibility() != View.VISIBLE) {
                    holder.presentsContainer.setVisibility(View.INVISIBLE);
                    holder.countdownContainer.setVisibility(View.VISIBLE);
                }
            }
        }
        // Check if next stream card should be displayed
        if (mNextStreamEntry != null && mStream != null
                && SantaPreferences.getCurrentTime() >= mNextStreamEntry.timestamp) {
            announceNewCard(addStreamEntry(mNextStreamEntry));
            mNextStreamEntry = mStream.getNext();
        }
        mSantaCamTimeout.check();
    }

    private void addPastStream() {
        // add all visited destinations from the cursors current position to the map
        StreamEntry next = mStream.getCurrent();
        while (next != null && next.timestamp < SantaPreferences.getCurrentTime()) {
            addStreamEntry(mStream.getCurrent());
            next = mStream.getNext();
        }
        mNextStreamEntry = next;
    }

    private TrackerCard addStreamEntry(StreamEntry entry) {
        SantaLog.d(TAG, "Add Stream entry: " + entry.timestamp);
        return mAdapter.addStreamEntry(entry);
    }

    private void announceNewCard(TrackerCard card) {
        if (mAccessibilityManager == null) {
            return;
        }
        String text = null;

        if (card instanceof TrackerCard.FactoidCard) {
            text = getString(R.string.new_trivia_from_santa);
        } else if (card instanceof TrackerCard.MovieCard) {
            text = getString(R.string.new_video_from_santa);
        } else if (card instanceof TrackerCard.PhotoCard) {
            text = getString(R.string.new_photo_from_santa);
        } else if (card instanceof TrackerCard.StatusCard) {
            text = getString(R.string.new_update_from_santa);
        }

        if (text != null) {
            // Announce the new card
            AccessibilityUtil.announceText(text, mRecyclerView, mAccessibilityManager);
        }
    }

    /**
     * Called when the state of santa cam mode changes (It is enabled or disabled).
     */
    public void onSantacamStateChange(boolean santacamEnabled) {

        // Hide/show the SantaCam ActionBar item if it has been initialised
        // (Otherwise the visibility is set when it is initialised.)
        if (mSantaCamButton != null && !isFinishing()) {
            if (santacamEnabled) {
                mSantaCamButton.hide();
            } else {
                mSantaCamButton.show();
            }
        }

        if (santacamEnabled) {
            mSantaCamTimeout.cancel();
        }
    }

    @Override
    public void onShowDestination(Destination destination) {
        // TODO: Jump tot the destination
        mAdapter.addDestination(true, destination, mSupportStreetView);
        mAdapter.notifyDataSetChanged();
        mRecyclerView.smoothScrollToPosition(0);
    }

    @Override
    public void onClearDestination() {
        mRecyclerView.smoothScrollToPosition(0);
    }

    /**
     * Called when the map is clicked
     */
    @Override
    public void mapClickAction() {
        // Nothing to do
    }

    @Override
    public Destination getDestination(int id) {
        return null;
    }

    public void notifyCamInteraction() {
        if (mSantaCamTimeout != null) {
            mSantaCamTimeout.reset();
        }
    }

    private CardAdapter.CardAdapterListener mCardAdapterListener = new CardAdapter.CardAdapterListener() {
        @Override
        public void onOpenStreetView(Destination.StreetView streetView) {
            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_tracker),
                    getString(R.string.analytics_tracker_action_streetview), streetView.id);

            // [ANALYTICS EVENT]: StreetView
            AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                    R.string.analytics_tracker_action_streetview, streetView.id);
            Intent intent = Intents.getStreetViewIntent(getString(R.string.streetview_uri), streetView);
            startActivity(intent);
        }

        @Override
        public void onPlayVideo(String youtubeId) {
            // App Measurement
            MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_tracker),
                    getString(R.string.analytics_tracker_action_video), youtubeId);

            // [ANALYTICS EVENT]: Video
            AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                    R.string.analytics_tracker_action_video, youtubeId);
            Intent intent = Intents.getYoutubeIntent(mRecyclerView.getContext(), youtubeId);
            startActivity(intent);
        }
    };

    private static class IncomingHandler extends Handler {

        private final WeakReference<SantaMapActivity> mActivityRef;

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

        @Override
        public void handleMessage(Message msg) {
            SantaLog.d(TAG, "message=" + msg.what);
            SantaMapActivity activity = mActivityRef.get();
            if (activity == null) {
                return;
            }
            if (!activity.mIgnoreNextUpdate || msg.what == SantaServiceMessages.MSG_SERVICE_STATUS) {
                // ignore all updates while flag is toggled until status update is received
                switch (msg.what) {
                case SantaServiceMessages.MSG_SERVICE_STATE_BEGIN:
                    // beginning full state update, ignore if already live
                    if (activity.mIsLive) {
                        activity.mIgnoreNextUpdate = true;
                    }
                    break;
                case SantaServiceMessages.MSG_SERVICE_STATUS:
                    // Current state of service, received once when connecting, reset ignore
                    activity.mIgnoreNextUpdate = false;

                    switch (msg.arg1) {
                    case SantaServiceMessages.STATUS_IDLE:
                        activity.mHaveApiError = false;
                        if (!activity.mHasDataLoaded) {
                            activity.mHasDataLoaded = true;
                            activity.getSupportLoaderManager().restartLoader(LOADER_DESTINATIONS, null,
                                    activity.mLoaderCallbacks);
                        }
                        break;
                    case SantaServiceMessages.STATUS_ERROR_NODATA:
                    case SantaServiceMessages.STATUS_ERROR:
                        Log.d(TAG, "Santa tracking error 3, continue for now");
                        activity.mHaveApiError = true;
                        break;
                    case SantaServiceMessages.STATUS_PROCESSING:
                        // wait for success, but tell user we are waiting
                        Toast.makeText(activity, R.string.contacting_santa, Toast.LENGTH_LONG).show();
                        activity.mHaveApiError = false;
                        break;
                    }
                    break;
                case SantaServiceMessages.MSG_INPROGRESS_UPDATE_ROUTE:
                    Log.d(TAG, "Santa tracking update 0 - returning.");
                    // route is about to be updated, return to StartupActivity
                    activity.handleErrorFinish();
                    break;
                case SantaServiceMessages.MSG_UPDATED_STREAM:
                    // stream data has been updated - requery data
                    if (activity.mHasDataLoaded && activity.mStream != null) {
                        Log.d(TAG, "Santa stream update received.");
                        activity.getSupportLoaderManager().restartLoader(LOADER_STREAM, null,
                                activity.mLoaderCallbacks);
                    }
                    break;
                case SantaServiceMessages.MSG_UPDATED_ROUTE:
                    // route data has been updated - requery data
                    if (activity.mHasDataLoaded && activity.mDestinations != null) {
                        Log.d(TAG, "Santa tracking update 1 received.");
                        activity.getSupportLoaderManager().restartLoader(LOADER_DESTINATIONS, null,
                                activity.mLoaderCallbacks);
                    }
                    break;
                case SantaServiceMessages.MSG_UPDATED_ONOFF:
                    // exit if flag has been set
                    activity.mSwitchOff = (msg.arg1 == SantaServiceMessages.SWITCH_OFF);
                    if (activity.mSwitchOff) {
                        Log.d(TAG, "Lost Santa.");

                        if (mActivityRef.get() != null) {
                            // App Measurement
                            Context context = mActivityRef.get();
                            FirebaseAnalytics measurement = FirebaseAnalytics.getInstance(context);
                            MeasurementManager.recordCustomEvent(measurement,
                                    context.getString(R.string.analytics_event_category_tracker),
                                    context.getString(R.string.analytics_tracker_action_error),
                                    context.getString(R.string.analytics_tracker_error_switchoff));
                        }

                        // [ANALYTICS EVENT]: Error SwitchOff
                        AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                                R.string.analytics_tracker_action_error,
                                R.string.analytics_tracker_error_switchoff);
                        activity.handleErrorFinish();
                    }
                    break;
                case SantaServiceMessages.MSG_UPDATED_TIMES:
                    onMessageUpdatedTimes(activity, msg);
                    break;
                case SantaServiceMessages.MSG_UPDATED_DESTINATIONPHOTO:
                    final boolean disablePhoto = msg.arg1 == SantaServiceMessages.DISABLED;
                    activity.setDestinationPhotoDisabled(disablePhoto);
                    break;
                case SantaServiceMessages.MSG_UPDATED_CASTDISABLED:
                    activity.mFlagDisableCast = (msg.arg1 == SantaServiceMessages.DISABLED);
                    activity.onCastFlagUpdate();
                    break;
                case SantaServiceMessages.MSG_ERROR_NODATA:
                    //for no data: wait to run out of locations, proceed with normal error handling
                case SantaServiceMessages.MSG_ERROR:
                    // Error accessing the API - ignore and run until out of locations
                    Log.d(TAG, "Couldn't track Santa, continue for now.");
                    activity.mHaveApiError = true;
                    break;
                case SantaServiceMessages.MSG_SUCCESS:
                    activity.mHaveApiError = false;
                    // If data has been received for first time, start tracking
                    // Otherwise ignore all other updates
                    if (!activity.mHasDataLoaded) {
                        activity.mHasDataLoaded = true;
                        activity.getSupportLoaderManager().restartLoader(LOADER_DESTINATIONS, null,
                                activity.mLoaderCallbacks);
                    }
                    break;
                default:
                    super.handleMessage(msg);
                    break;
                }

            }
        }

        private static boolean hasSignificantChange(long newOffset, SantaMapActivity activity) {
            return newOffset > activity.mOffset + SantaPreferences.OFFSET_ACCEPTABLE_RANGE_DIFFERENCE
                    || newOffset < activity.mOffset - SantaPreferences.OFFSET_ACCEPTABLE_RANGE_DIFFERENCE;
        }

        private void onMessageUpdatedTimes(SantaMapActivity activity, Message msg) {
            Bundle b = (Bundle) msg.obj;
            long newOffset = b.getLong(SantaServiceMessages.BUNDLE_OFFSET);
            // If offset has changed significantly, return to village
            if (activity.mHasDataLoaded && hasSignificantChange(newOffset, activity)) {
                Log.d(TAG, "Santa tracking update 2 - returning.");

                if (mActivityRef.get() != null) {
                    // App Measurement
                    Context context = mActivityRef.get();
                    FirebaseAnalytics measurement = FirebaseAnalytics.getInstance(context);
                    MeasurementManager.recordCustomEvent(measurement,
                            context.getString(R.string.analytics_event_category_tracker),
                            context.getString(R.string.analytics_tracker_action_error),
                            context.getString(R.string.analytics_tracker_error_timeupdate));
                }

                // [ANALYTICS EVENT]: Error TimeUpdate
                AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                        R.string.analytics_tracker_action_error, R.string.analytics_tracker_error_timeupdate);

                activity.handleErrorFinish();

            } else if (!activity.mHasDataLoaded && newOffset != activity.mOffset) {
                // New offset but data has not been loaded yet, cache new offset
                activity.mOffset = newOffset;
                SantaPreferences.cacheOffset(activity.mOffset);
            }

            activity.mFinalArrival = b.getLong(SantaServiceMessages.BUNDLE_FINAL_ARRIVAL);
            activity.mFinalDeparture = b.getLong(SantaServiceMessages.BUNDLE_FINAL_DEPARTURE);
            activity.mFirstDeparture = b.getLong(SantaServiceMessages.BUNDLE_FIRST_DEPARTURE);
        }
    }

    private void onCastFlagUpdate() {
        SantaApplication.toogleCast(this, mFlagDisableCast);
    }

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

            //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 View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.santacam:
                mMapFragment.enableSantaCam(true);

                // App Measurement
                MeasurementManager.recordCustomEvent(mMeasurement,
                        getString(R.string.analytics_event_category_tracker),
                        getString(R.string.analytics_tracker_action_cam),
                        getString(R.string.analytics_tracker_cam_fab));

                // [ANALYTICS EVENT]: SantaCamEnabled ActionBar
                AnalyticsManager.sendEvent(R.string.analytics_event_category_tracker,
                        R.string.analytics_tracker_action_cam, R.string.analytics_tracker_cam_fab);
                break;
            case R.id.top:
                mRecyclerView.smoothScrollToPosition(0);
                break;
            }
        }
    };

    private BottomSheetBehavior.BottomSheetListener mBottomSheetListener = new BottomSheetBehavior.BottomSheetListener() {

        private static final float FAB_THRESHOLD = 0.8f;

        @Override
        public void onStateChanged(@BottomSheetBehavior.State int newState) {
            adjustMapPaddings(newState);
        }

        @Override
        public void onSlide(float slideOffset) {
            // Hide/show the FAB
            if (mSantaCamButton != null) {
                if (mSantaCamButton.getVisibility() == View.VISIBLE) {
                    if (slideOffset > FAB_THRESHOLD) {
                        mSantaCamButton.hide();
                    }
                } else if (!mMapFragment.isInSantaCam()) {
                    if (slideOffset <= FAB_THRESHOLD) {
                        mSantaCamButton.show();
                    }
                }
            }
        }
    };

    private void adjustMapPaddings(@BottomSheetBehavior.State int newState) {
        if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
            mMapFragment.setCamPadding(0, 0, 0,
                    mBottomSheetBehavior.getPeekHeight() - mBottomSheetBehavior.getHiddenPeekHeight());
        } else if (newState == BottomSheetBehavior.STATE_HIDDEN) {
            mMapFragment.setCamPadding(0, 0, 0, 0);
        }
    }

    private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
        boolean mShowTopButton = false;

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            boolean showButton = false;
            if (mLayoutManager.findFirstVisibleItemPosition() > CardAdapter.DASHBOARD_POSITION) {
                showButton = true;
            }

            // Only animate if the button state changes
            Animation ani = null;
            if (showButton && !mShowTopButton) {
                ani = new AlphaAnimation(0, 1);
                mButtonTop.setVisibility(View.VISIBLE);
            } else if (!showButton && mShowTopButton) {
                ani = new AlphaAnimation(1, 0);
                ani.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        mButtonTop.setVisibility(View.INVISIBLE);
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
            }

            if (ani != null) {
                ani.setDuration(300);
                ani.setInterpolator(new AccelerateInterpolator());
                mButtonTop.startAnimation(ani);
            }

            mShowTopButton = showButton;
        }
    };

}