/* * Copyright (C) 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.santatracker.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.annotation.StringRes; 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.MenuItem; 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.cast.CastUtil; import com.google.android.apps.santatracker.cast.LoggingCastSessionListener; import com.google.android.apps.santatracker.cast.LoggingCastStateListener; 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.PlayServicesUtil; import com.google.android.apps.santatracker.util.SantaLog; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastState; import com.google.android.gms.cast.framework.IntroductoryOverlay; import com.google.android.gms.cast.framework.SessionManagerListener; 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_UPDATE_INTERVAL = 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 FirebaseAnalytics mMeasurement; private LinearLayoutManager mLayoutManager; private ImageButton mButtonTop; // Cast private boolean mHaveGooglePlayServices; private MenuItem mMediaRouteMenuItem; private SessionManagerListener mCastListener; private OverlayCastStateListener mCastStateListener; private boolean mCastOverlayShown = false; @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); // Check for play services mHaveGooglePlayServices = PlayServicesUtil.hasPlayServices(this); // Cast mCastListener = new LoggingCastSessionListener(this, R.string.analytics_cast_session_launch); mCastStateListener = new OverlayCastStateListener(this, R.string.analytics_cast_statechange_map); 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); // 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.from(bottomSheet); mBottomSheetBehavior.setBottomSheetListener(mBottomSheetListener); } findViewById(R.id.main_touchinterceptor).setOnTouchListener(mInterceptorListener); } @Override protected void onDestroy() { if (mAdapter != null) { mAdapter.release(); mAdapter = null; } super.onDestroy(); } 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 (mBottomSheetBehavior != null) { adjustMapPaddings(mBottomSheetBehavior.getState()); } registerCastListeners(); } @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(); CastUtil.removeCastListener(this, mCastListener); CastUtil.removeCastStateListener(this, mCastStateListener); // stop santa cam onSantacamStateChange(false); // 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); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { boolean castDisabled = getCastDisabled(); // Add cast button if (!castDisabled && mMediaRouteMenuItem == null) { mMediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton( getApplicationContext(), menu, R.id.media_route_menu_item); } // Toggle cast visibility if (mMediaRouteMenuItem != null) { mMediaRouteMenuItem.setVisible(!castDisabled); // Display the cast overlay if the item exists. // The overlay is only shown if the item is visible. showCastOverlay(); } return super.onPrepareOptionsMenu(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_UPDATE_INTERVAL) { @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_UPDATE_INTERVAL) { @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.setVisibility(View.INVISIBLE); Log.d(TAG, "hide"); } else { mSantaCamButton.show(); Log.d(TAG, "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 } 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; 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.setCastDisabled(activity.mFlagDisableCast); 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 registerCastListeners() { CastUtil.registerCastListener(this, mCastListener); CastUtil.registerCastStateListener(this, mCastStateListener); } private boolean getCastDisabled() { // Cast should be disabled if we don't have the proper version of Google Play Services // (to avoid a crash) or if we choose to disable it from the server. return (!mHaveGooglePlayServices || mFlagDisableCast); } private void setCastDisabled(boolean disableCast) { if (!mHaveGooglePlayServices) { return; } if (disableCast) { // If cast was previously enabled and we are disabling it, try to stop casting CastUtil.stopCasting(this); } else { // If cast was disabled and is becoming enabled, register listeners registerCastListeners(); } // Update state mFlagDisableCast = disableCast; // Update menu supportInvalidateOptionsMenu(); } private void showCastOverlay() { // Only show the cast overlay if the the cast menu item is visible. if (!mCastOverlayShown && mMediaRouteMenuItem != null && mMediaRouteMenuItem.isVisible()) { new Handler().post(new Runnable() { @Override public void run() { Log.d(TAG, "Displaying introductory overlay."); IntroductoryOverlay overlay = new IntroductoryOverlay.Builder(SantaMapActivity.this, mMediaRouteMenuItem) .setTitleText(R.string.cast_overlay_text) .setSingleTime() .setOnOverlayDismissedListener( mCastOverlayDismissedListener) .build(); overlay.show(); mCastOverlayShown = true; } }); } } /** * Logs an event when the Cast IntroductoryOverlay is dismissed. */ IntroductoryOverlay.OnOverlayDismissedListener mCastOverlayDismissedListener = new IntroductoryOverlay.OnOverlayDismissedListener() { @Override public void onOverlayDismissed() { // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_cast), getString(R.string.analytics_cast_overlayshown)); } }; 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; } }; class OverlayCastStateListener extends LoggingCastStateListener { public OverlayCastStateListener(Context context, @StringRes int category) { super(context, category); } @Override public void onCastStateChanged(int newState) { super.onCastStateChanged(newState); if (newState != CastState.NO_DEVICES_AVAILABLE) { showCastOverlay(); } } } }