/* * 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.launch; import android.Manifest; 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.net.Uri; 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.annotation.VisibleForTesting; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; 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.R; import com.google.android.apps.santatracker.SantaNotificationBuilder; 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.common.NotificationConstants; import com.google.android.apps.santatracker.data.GameDisabledState; import com.google.android.apps.santatracker.data.SantaPreferences; import com.google.android.apps.santatracker.games.PlayGamesFragment; import com.google.android.apps.santatracker.games.SignInListener; 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.PlayServicesUtil; import com.google.android.apps.santatracker.util.SantaLog; import com.google.android.apps.santatracker.village.SnowFlakeView; import com.google.android.apps.santatracker.village.Village; import com.google.android.apps.santatracker.village.VillageView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastStateListener; import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.games.Games; import com.google.firebase.analytics.FirebaseAnalytics; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * 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 StartupActivity extends AppCompatActivity implements View.OnClickListener, Village.VillageListener, VoiceAction.VoiceActionHandler, SignInListener, SantaContext, LaunchCountdown.LaunchCountdownContext { protected static final String TAG = "SantaStart"; public static final String EXTRA_DISABLE_ANIMATIONS = "disable_animations"; private static final String VILLAGE_TAG = "VillageFragment"; private static final String INTENT_HANDLED = "intent_handled"; private static final String KEY_RECYCLER_POSITION = "key_recycler_position"; private static final String KEY_CATEGORY_SELECTED = "key_category_selected"; public static final int NUM_CATEGORIES = 3; public static final int SANTA_CATEGORY = 0; public static final int GAMES_CATEGORY = 1; public static final int MOVIES_CATEGORY = 2; private PlayGamesFragment mGamesFragment; private AppInvitesFragment mInvitesFragment; private AudioPlayer mAudioPlayer; private boolean mResumed = false; private boolean mSignedIn = false; private Village mVillage; private VillageView mVillageView; private ImageView mVillageBackdrop; private View mLaunchButton; private SantaCollapsingToolbarLayout mSantaCollapsing; private View mCountdownView; private View mWavingSanta; private ImageView mWavingSantaArm; private Animation mWavingAnim; private View mOrnament; private ImageView mSantaIcon; private ImageView mGamesIcon; private ImageView mMoviesIcon; // RecyclerView and associated bits private SantaCardAdapter mSantaCardAdapter; private GamesCardAdapter mGamesCardAdapter; private MoviesCardAdapter mMoviesCardAdapter; private CardAdapter[] mCardAdapters; private CardLayoutManager[] mCardLayoutManagers; private int[] mScrollPositions; private StickyScrollListener[] mScrollListeners; private int mCategory; private TextView mStatusText; private LaunchCountdown mCountdown; private ViewPager mCardsViewPager; private CardListPagerAdapter mCardListPagerAdapter; // 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_GUMBALL; private static long UNLOCK_JETPACK; private static long UNLOCK_MEMORY; private static long UNLOCK_ROCKET; private static long UNLOCK_DANCER; private static long UNLOCK_SNOWDOWN; private static long UNLOCK_CITY_QUIZ; 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 mFlagDisableCast = false; // Game flags private GameDisabledState mGameDisabledState = new GameDisabledState(); private String[] mVideoList = new String[]{null, null, null}; // Check if we have Google Play Services 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; // Launching a child activity (another launch request should be blocked) private boolean mLaunchingChild = false; // Service integration private Messenger mService = null; private boolean mIsBound = false; private Messenger mMessenger; // request code for games Activities private final int RC_STARTUP = 1111; private final int RC_GAMES = 9999; // Permission request codes private final int RC_DEBUG_PERMS = 1; private FirebaseAnalytics mMeasurement; // Cast private MenuItem mMediaRouteMenuItem; private SessionManagerListener mCastListener; private CastStateListener mCastStateListener; // Flag used to disable animations to make testing more reliable // Never to be used outside of testing private boolean mAnimationDisabled = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestPermissionsIfDebugModeEnabled(); // Record Firebase properties MeasurementManager.recordDeviceProperties(this); // 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)); } mMessenger = new Messenger(new IncomingHandler(this)); setContentView(R.layout.layout_startup); loadResourceFields(getResources()); mCountdown = new LaunchCountdown(this); mCountdownView = findViewById(R.id.countdown_container); mAudioPlayer = new AudioPlayer(getApplicationContext()); Toolbar toolBar = (Toolbar) findViewById(R.id.toolbar); if (toolBar != null) { setSupportActionBar(toolBar); } // Set up collapsing mSantaCollapsing = (SantaCollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); mSantaCollapsing.setSnowFlakeView((SnowFlakeView)findViewById(R.id.snowFlakeView)); mSantaCollapsing.setToolbarContentView(findViewById(R.id.toolbar_content)); mSantaCollapsing.setOverlayView(findViewById(R.id.view_color_overlay)); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowTitleEnabled(false); actionBar.setHomeButtonEnabled(false); actionBar.setDisplayHomeAsUpEnabled(false); } mStatusText = (TextView) findViewById(R.id.statusText); 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); mOrnament = findViewById(R.id.countdown_ornament); mSantaIcon = (ImageView) findViewById(R.id.santa_icon); mGamesIcon = (ImageView) findViewById(R.id.arcade_icon); mMoviesIcon = (ImageView) findViewById(R.id.theatre_icon); mWavingSanta = findViewById(R.id.santa_waving); mWavingSantaArm = (ImageView) findViewById(R.id.santa_arm); mWavingSanta.setOnClickListener(this); mCardLayoutManagers = new CardLayoutManager[NUM_CATEGORIES]; mScrollListeners = new StickyScrollListener[NUM_CATEGORIES]; mScrollPositions = new int[NUM_CATEGORIES]; for(int i = 0; i < mScrollPositions.length; i++) { mScrollPositions[i] = RecyclerView.NO_POSITION; } mCardsViewPager = (ViewPager)findViewById(R.id.cards_view_pager); mCardListPagerAdapter = new CardListPagerAdapter(this); mCardsViewPager.setAdapter(mCardListPagerAdapter); mCardsViewPager.addOnPageChangeListener(mCardListPagerAdapter); initialiseViews(); // Restore saved instance state if (savedInstanceState != null) { onRestoreInstanceState(savedInstanceState); } // Check if we have Google Play Services mHaveGooglePlayServices = PlayServicesUtil.hasPlayServices(getApplicationContext()); findViewById(R.id.games_button).setOnClickListener(this); findViewById(R.id.movies_button).setOnClickListener(this); findViewById(R.id.santa_button).setOnClickListener(this); // initialize our connection to Google Play Games mGamesFragment = PlayGamesFragment.getInstance(this, this); ViewCompat.setElevation(findViewById(R.id.category_picker_bar), 4); // App invites mInvitesFragment = AppInvitesFragment.getInstance(this); // set up click listeners for our buttons findViewById(R.id.fab_achievement).setOnClickListener(this); findViewById(R.id.fab_leaderboard).setOnClickListener(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); // See if it was a voice action which triggered this activity and handle it onNewIntent(getIntent()); mCastListener = new LoggingCastSessionListener(this, R.string.analytics_cast_session_launch); mCastStateListener = new LoggingCastStateListener(this, R.string.analytics_cast_statechange_launch); // FOR TESTING -- Check if we should disable animations if (getIntent() != null && getIntent().getBooleanExtra(EXTRA_DISABLE_ANIMATIONS, false)) { disableAnimations(); } // set the initial states resetLauncherStates(); } @Override protected void onDestroy() { mMessenger = null; super.onDestroy(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Save RecyclerView scroll position if (mCardLayoutManagers[mCategory] != null) { mScrollPositions[mCategory] = mCardLayoutManagers[mCategory].findFirstCompletelyVisibleItemPosition(); } outState.putInt(KEY_CATEGORY_SELECTED, mCategory); outState.putInt(KEY_RECYCLER_POSITION, mScrollPositions[mCategory]); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mCategory = savedInstanceState.getInt(KEY_CATEGORY_SELECTED); // Restore RecyclerView scroll position mScrollPositions[mCategory] = savedInstanceState.getInt(KEY_RECYCLER_POSITION, RecyclerView.NO_POSITION); } @VisibleForTesting protected void disableAnimations() { // Disable animations in general mAnimationDisabled = true; // Disable snow ((SnowFlakeView) findViewById(R.id.snowFlakeView)).disableAnimation(); // Disable village view mVillageView.disableAnimation(); } private void updateCategory() { switch(mCategory) { case GAMES_CATEGORY: setGamesCategory(); break; case SANTA_CATEGORY: setSantaCategory(); break; case MOVIES_CATEGORY: setMoviesCategory(); break; } } private void setGamesCategory() { mCategory = GAMES_CATEGORY; mCardAdapters[GAMES_CATEGORY].refreshData(); mGamesIcon.setImageResource(R.drawable.filter_games); mSantaIcon.setImageResource(R.drawable.filter_compass_inactive); mMoviesIcon.setImageResource(R.drawable.filter_videos_inactive); } private void setMoviesCategory() { mCategory = MOVIES_CATEGORY; mCardAdapters[MOVIES_CATEGORY].refreshData(); mGamesIcon.setImageResource(R.drawable.filter_games_inactive); mSantaIcon.setImageResource(R.drawable.filter_compass_inactive); mMoviesIcon.setImageResource(R.drawable.filter_videos); } private void setSantaCategory() { mCategory = SANTA_CATEGORY; mCardAdapters[SANTA_CATEGORY].refreshData(); mGamesIcon.setImageResource(R.drawable.filter_games_inactive); mSantaIcon.setImageResource(R.drawable.filter_compass); mMoviesIcon.setImageResource(R.drawable.filter_videos_inactive);; } 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_GUMBALL = res.getInteger(R.integer.unlock_gumball) * ms; UNLOCK_JETPACK = res.getInteger(R.integer.unlock_jetpack) * ms; UNLOCK_MEMORY = res.getInteger(R.integer.unlock_memory) * ms; UNLOCK_ROCKET = res.getInteger(R.integer.unlock_rocket) * ms; UNLOCK_DANCER = res.getInteger(R.integer.unlock_dancer) * ms; UNLOCK_SNOWDOWN = res.getInteger(R.integer.unlock_snowdown) * ms; UNLOCK_CITY_QUIZ = res.getInteger(R.integer.unlock_city_quiz) * 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); mCardAdapters = new CardAdapter[NUM_CATEGORIES]; // Initialize the RecyclerView int numColumns = getResources().getInteger(R.integer.village_columns); mCardLayoutManagers[SANTA_CATEGORY] = new CardLayoutManager(this, numColumns); mSantaCardAdapter = new SantaCardAdapter(this); mScrollListeners[SANTA_CATEGORY] = new StickyScrollListener(mCardLayoutManagers[SANTA_CATEGORY], numColumns); mCardLayoutManagers[GAMES_CATEGORY] = new CardLayoutManager(this, numColumns); mGamesCardAdapter = new GamesCardAdapter(this); mScrollListeners[GAMES_CATEGORY] = new StickyScrollListener(mCardLayoutManagers[GAMES_CATEGORY], numColumns); mCardLayoutManagers[MOVIES_CATEGORY] = new CardLayoutManager(this, numColumns); mMoviesCardAdapter = new MoviesCardAdapter(this); mScrollListeners[MOVIES_CATEGORY] = new StickyScrollListener(mCardLayoutManagers[MOVIES_CATEGORY], numColumns); mCardAdapters[SANTA_CATEGORY] = mSantaCardAdapter; mCardAdapters[GAMES_CATEGORY] = mGamesCardAdapter; mCardAdapters[MOVIES_CATEGORY] = mMoviesCardAdapter; } 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); handleVoiceActions(); Bundle extras = intent.getExtras(); if (extras != null && extras.getString(NotificationConstants.KEY_NOTIFICATION_TYPE) != null) { // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_notification), extras.getString(NotificationConstants.KEY_NOTIFICATION_TYPE)); // [ANALYTICS EVENT]: Launch Notification AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_notification, extras.getString(NotificationConstants.KEY_NOTIFICATION_TYPE)); SantaLog.d(TAG, "launched from notification"); } } @Override public boolean handleVoiceAction(Intent intent) { if (VoiceAction.ACTION_PLAY_RANDOM_GAME.equals(intent.getAction())) { Log.d(TAG, String.format("Voice command: [%s]", VoiceAction.ACTION_PLAY_RANDOM_GAME)); return startRandomGame(); } else { return false; } } /** * Pick a game at random from the available games in STATE_READY state * @return true if a game was launched */ private boolean startRandomGame() { // find out all the games that are ready to play AbstractLaunch[] pins = mCardAdapters[GAMES_CATEGORY].getLaunchers(); List<AbstractLaunch> games = new ArrayList<>(pins.length); for (AbstractLaunch pin : pins) { if (pin.isGame()) { if (pin.mState == AbstractLaunch.STATE_READY) { games.add(pin); } } } // now pick one of the games from games and launch it if (games.size() > 0) { Random r = new Random(); int index = r.nextInt(games.size()); AbstractLaunch game = games.get(index); Log.d(TAG, String.format("Picked a game at random [%s]", game.mContentDescription)); // launch the game by simulating a click game.onClick(game.getClickTarget()); // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_voice), game.mContentDescription); // [ANALYTICS EVENT]: Launch Voice AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_voice, game.mContentDescription); return true; } else { return false; } } private void handleVoiceActions() { Intent intent = getIntent(); if (VoiceAction.isVoiceAction(intent)) { if (isAlreadyHandled(intent)) { Log.d(TAG, String.format("Ignoring an already handled intent [%s]", intent.getAction())); return; // already processed } boolean handled; // first check if *this* activity can handle the voice action handled = handleVoiceAction(intent); // next check all the pins if (!handled) { AbstractLaunch[] pins = mCardAdapters[SANTA_CATEGORY].getLaunchers(); // try sending the voice command to all launchers, the first one that handles it wins for (AbstractLaunch launch : pins) { if (handled = launch.handleVoiceAction(intent)) { // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_voice), launch.mContentDescription); // [ANALYTICS EVENT]: Launch Voice AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_voice, launch.mContentDescription); break; } } } if (!handled) { Toast.makeText(this, getResources().getText(R.string.voice_command_unhandled), Toast.LENGTH_SHORT) .show(); // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_voice), getString(R.string.analytics_launch_voice_unhandled)); // [ANALYTICS EVENT]: Launch Voice Unhandled AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_voice, R.string.analytics_launch_voice_unhandled); } else { setAlreadyHandled(intent); } } } /** * This method is responsible for handling a corner case. * Upon orientation change, the Activity is re-created (onCreate is called) * and the same intent is (re)delivered to the app. * Fortunately the Intent is Parcelable so we can mark it and check for this condition. * Without this, if the phone is in portrait mode, and the user issues voice command to * start a game (or other forcing orientation change), the following happens: * * 1. com.google.android.apps.santatracker.PLAY_GAME is delivered to the app. * 2. Game is started and phone switches to landscape. * 3. User ends the game, rotates the phone back to portrait. * 4. onCreate is called again since StartupActivity is re-created. * 5. The voice action is re-executed * (since getIntent returns com.google.android.apps.santatracker.PLAY_GAME). * * We don't want #5 to take place. * * @param intent current intent */ private void setAlreadyHandled(Intent intent) { intent.putExtra(INTENT_HANDLED, true); } /** * Checks to see if the intent (voice command) has already been processed * * @param intent current intent (voice command) * @return true if the intent (voice command) has already been processed */ private boolean isAlreadyHandled(Intent intent) { return intent.getBooleanExtra(INTENT_HANDLED, false); } @Override protected void onResume() { super.onResume(); if (mCardLayoutManagers[mCategory] != null) { mCardLayoutManagers[mCategory].scrollToPosition(mScrollPositions[mCategory]); } registerCastListeners(); updateCategory(); mResumed = true; } @Override protected void onPause() { super.onPause(); mResumed = false; mAudioPlayer.stopAll(); CastUtil.removeCastListener(this, mCastListener); CastUtil.removeCastStateListener(this, mCastStateListener); 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(); } else { hideStatus(); } } @Override protected void onStop() { super.onStop(); unregisterFromService(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); mGamesFragment.onActivityResult(requestCode, resultCode, data); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (mResumed && hasFocus && !AccessibilityUtil.isTouchAccessiblityEnabled(this)) { mAudioPlayer.playTrackExclusive(R.raw.village_music, true); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu getMenuInflater().inflate(R.menu.menu_startup, menu); MenuItem menuItemLegal = menu.findItem(R.id.legal); menuItemLegal.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { final Resources resources = getResources(); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(StartupActivity.this); dialogBuilder.setItems(resources.getStringArray(R.array.legal_privacy), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String url; switch (which) { case 1: // Privacy url = resources.getString(R.string.url_privacy); break; case 2: // Terms of Service url = resources.getString(R.string.url_tos); break; case 3: // Open source licenses LicenseDialogFragment.newInstance() .show(getSupportFragmentManager(), "dialog"); return; case 4: // Open source licenses for Google Play Services GmsLicenseDialogFragment.newInstance() .show(getSupportFragmentManager(), "dialog"); return; case 0: default: url = resources.getString(R.string.url_legal); break; } startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } }); dialogBuilder.create().show(); return true; } }); menu.findItem(R.id.menu_app_invite) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { mInvitesFragment.sendGenericInvite(); return true; } }); menu.findItem(R.id.open_help) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_help)))); return true; } }); menu.findItem(R.id.github_santa) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_github_santa)))); return true; } }); menu.findItem(R.id.github_pienoon) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_github_pienoon)))); return true; } }); menu.findItem(R.id.sign_out) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { Games.signOut(mGamesFragment.getGamesApiClient()); updateSignInState(false); return true; } }); menu.findItem(R.id.sync_config) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { if (mService != null) { Message msg = Message.obtain(null, SantaServiceMessages.MSG_SERVICE_FORCE_SYNC); try { mService.send(msg); } catch (RemoteException e) { Log.e(TAG, "sendMessage:FORCE_SYNC", e); } } return true; } }); menu.findItem(R.id.notification_takeoff) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { SantaNotificationBuilder .createSantaNotification(StartupActivity.this, R.string.notification_takeoff); return true; } }); menu.findItem(R.id.launch_mode) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { enableTrackerMode(true); return true; } }); menu.findItem(R.id.countdown_mode) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { startCountdown(OFFLINE_SANTA_DEPARTURE); return true; } }); 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); } return super.onPrepareOptionsMenu(menu); } @Override protected boolean onPrepareOptionsPanel(View view, Menu menu) { menu.findItem(R.id.sign_out).setVisible(mSignedIn); return super.onPrepareOptionsPanel(view, menu); } 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 options menu supportInvalidateOptionsMenu(); } /** * 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(); // Disable cast only if not already casting if (!CastUtil.isCasting(this)) { setCastDisabled(true); } AbstractLaunch launchSanta = mSantaCardAdapter.getLauncher(SantaCardAdapter.KEY_SANTA_CARD); int prevSantaState = launchSanta.getState(); // 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(); boolean takenOff = (time >= OFFLINE_SANTA_DEPARTURE); boolean isFlying = takenOff && (time < OFFLINE_SANTA_FINALARRIVAL); if (!takenOff) { // Santa hasn't departed yet, show countdown launchSanta.setState(AbstractLaunch.STATE_LOCKED); startCountdown(OFFLINE_SANTA_DEPARTURE); final long notificationTime = SantaPreferences .getAdjustedTime(OFFLINE_SANTA_DEPARTURE); SantaNotificationBuilder .scheduleSantaNotification(getApplicationContext(), notificationTime, NotificationConstants.NOTIFICATION_TAKEOFF); } else if (isFlying) { // Santa should have already left, but no data yet, hide countdown and show message stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_DISABLED); showStatus(R.string.contacting_santa); } else { // Post Christmas stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_FINISHED); } // Santa's state changed, reload the Santa card adapter if (prevSantaState != launchSanta.getState()) { Log.d(TAG, "data:santaLaunchState: " + prevSantaState + " --> " + launchSanta.getState()); mSantaCardAdapter.changeState(this, isFlying); } } /** * 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(); // hide status hideStatus(); // Set cast state setCastDisabled(mFlagDisableCast); AbstractLaunch launchSanta = mSantaCardAdapter.getLauncher(SantaCardAdapter.KEY_SANTA_CARD); int prevSantaState = launchSanta.getState(); final long time = SantaPreferences.getCurrentTime(); boolean takenOff = (time >= OFFLINE_SANTA_DEPARTURE); boolean isFlying = takenOff && (time < OFFLINE_SANTA_FINALARRIVAL); if (isFlying) { // 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); showStatus(R.string.still_trying_to_reach_santa); } else { launchSanta.setState(AbstractLaunch.STATE_READY); } } else if (!takenOff) { // Santa hasn't taken off yet, start count-down and schedule // notification to first departure, hide buttons final long notificationTime = SantaPreferences .getAdjustedTime(mFirstDeparture); SantaNotificationBuilder.scheduleSantaNotification(getApplicationContext(), notificationTime, NotificationConstants.NOTIFICATION_TAKEOFF); startCountdown(mFirstDeparture); launchSanta.setState(AbstractLaunch.STATE_LOCKED); } else { // Post Christmas, hide countdown and buttons launchSanta.setState(AbstractLaunch.STATE_FINISHED); stopCountdown(); enableTrackerMode(false); } // Santa's state changed, reload the Santa card adapter if (prevSantaState != launchSanta.getState()) { Log.d(TAG, "nodata:santaLaunchState: " + prevSantaState + " --> " + launchSanta.getState()); mSantaCardAdapter.changeState(this, isFlying); } } public void enableTrackerMode(boolean showLaunchButton) { mCountdown.cancel(); mVillageBackdrop.setImageResource(R.drawable.village_bg_launch); mVillage.setPlaneEnabled(false); mLaunchButton.setVisibility(showLaunchButton ? View.VISIBLE : View.GONE); mSantaCollapsing.setOverlayColor(R.color.villageToolbarDark); mCountdownView.setVisibility(View.GONE); mWavingSanta.setVisibility(View.GONE); mOrnament.setVisibility(View.GONE); // Change the color of the status bar on SDK 21+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor( ContextCompat.getColor(this, R.color.villageStatusBarDark)); } } public void startCountdown(long time) { if (mAnimationDisabled) { return; } mCountdown.startTimer(time - SantaPreferences.getCurrentTime()); mVillageBackdrop.setImageResource(R.drawable.village_bg_countdown); mVillage.setPlaneEnabled(true); mLaunchButton.setVisibility(View.GONE); mSantaCollapsing.setOverlayColor(R.color.villageToolbarLight); mCountdownView.setVisibility(View.VISIBLE); mWavingSanta.setVisibility(View.VISIBLE); mOrnament.setVisibility(View.VISIBLE); // Change the color of the status bar on SDK 21+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor( ContextCompat.getColor(this, R.color.villageStatusBarLight)); } } public void stopCountdown() { mCountdown.cancel(); mCountdownView.setVisibility(View.GONE); } /* * Village Markers */ private void updateNavigation() { // Games mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.GUMBALL_CARD).setState( getGamePinState(mGameDisabledState.disableGumballGame, UNLOCK_GUMBALL)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.MEMORY_CARD).setState( getGamePinState(mGameDisabledState.disableMemoryGame, UNLOCK_MEMORY)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.ROCKET_CARD).setState( getGamePinState(mGameDisabledState.disableRocketGame, UNLOCK_ROCKET)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.DANCER_CARD).setState( getGamePinState(mGameDisabledState.disableDancerGame, UNLOCK_DANCER)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.SNOWDOWN_CARD).setState( getGamePinState(mGameDisabledState.disableSnowdownGame, UNLOCK_SNOWDOWN)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.CITY_QUIZ_CARD).setState( getGamePinState(mGameDisabledState.disableCityQuizGame, UNLOCK_CITY_QUIZ)); // Minigames // TODO: Decide if we want time-based unlocks for doodles games mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.MINIGAME_SWIMMING_CARD).setState( getGamePinState(mGameDisabledState.disableSwimmingGame, 0L)); mCardAdapters[GAMES_CATEGORY].getLauncher(GamesCardAdapter.MINIGAME_RUNNING_CARD).setState( getGamePinState(mGameDisabledState.disableRunningGame, 0L)); // Movies ((LaunchVideo) mCardAdapters[MOVIES_CATEGORY].getLauncher(MoviesCardAdapter.VIDEO01_CARD)).setVideo( mVideoList[0], UNLOCK_VIDEO_1); ((LaunchVideo) mCardAdapters[MOVIES_CATEGORY].getLauncher(MoviesCardAdapter.VIDEO15_CARD)).setVideo( mVideoList[1], UNLOCK_VIDEO_15); ((LaunchVideo) mCardAdapters[MOVIES_CATEGORY].getLauncher(MoviesCardAdapter.VIDEO23_CARD)).setVideo( mVideoList[2], UNLOCK_VIDEO_23); // Present Quest mSantaCardAdapter.getLauncher(SantaCardAdapter.KEY_PRESENT_QUEST_CARD) .setState(getGamePinState(mGameDisabledState.disablePresentQuest, 0L)); // reinitialise action bar supportInvalidateOptionsMenu(); } private int getGamePinState(boolean disabledFlag, long unlockTime) { if (disabledFlag) { return AbstractLaunch.STATE_HIDDEN; } else if (SantaPreferences.getCurrentTime() < unlockTime) { return AbstractLaunch.STATE_LOCKED; } else { return AbstractLaunch.STATE_READY; } } /* * Status Message */ private void showStatus(int i) { int state = mSantaCardAdapter.getLauncher(SantaCardAdapter.KEY_SANTA_CARD).getState(); if (state == AbstractLaunch.STATE_DISABLED || state == AbstractLaunch.STATE_LOCKED) { mStatusText.setVisibility(View.VISIBLE); mStatusText.setText(i); mStatusText.setContentDescription(mStatusText.getText()); } } private void hideStatus() { mStatusText.setVisibility(View.GONE); } /* * 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 // TODO: If doodle games get unlock times, include them here final long[] delays = new long[]{ mGameDisabledState.disableGumballGame ? Long.MAX_VALUE : UNLOCK_GUMBALL - time, mGameDisabledState.disableJetpackGame ? Long.MAX_VALUE : UNLOCK_JETPACK - time, mGameDisabledState.disableMemoryGame ? Long.MAX_VALUE : UNLOCK_MEMORY - time, mGameDisabledState.disableRocketGame ? Long.MAX_VALUE : UNLOCK_ROCKET - time, mGameDisabledState.disableDancerGame ? Long.MAX_VALUE : UNLOCK_DANCER - time, mGameDisabledState.disableSnowdownGame ? Long.MAX_VALUE : UNLOCK_SNOWDOWN - time, mGameDisabledState.disableCityQuizGame ? Long.MAX_VALUE : UNLOCK_CITY_QUIZ - 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(); } } }; private void updateSignInState(boolean signedIn) { mSignedIn = signedIn; setFabVisibility((FloatingActionButton) findViewById(R.id.fab_leaderboard), signedIn); setFabVisibility((FloatingActionButton) findViewById(R.id.fab_achievement), signedIn); supportInvalidateOptionsMenu(); } @Override public void onSignInFailed() { updateSignInState(false); } @Override public void onSignInSucceeded() { updateSignInState(true); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.launch_button: launchTracker(); break; case R.id.santa_waving: MeasurementManager.recordVillageSantaClick(mMeasurement); animateSanta(); break; case R.id.fab_achievement: showAchievements(); break; case R.id.fab_leaderboard: showLeaderboards(); break; case R.id.games_button: MeasurementManager.recordVillageTabClick(mMeasurement, "games"); mCardsViewPager.setCurrentItem(GAMES_CATEGORY); break; case R.id.santa_button: MeasurementManager.recordVillageTabClick(mMeasurement, "santa"); mCardsViewPager.setCurrentItem(SANTA_CATEGORY); break; case R.id.movies_button: MeasurementManager.recordVillageTabClick(mMeasurement, "movies"); mCardsViewPager.setCurrentItem(MOVIES_CATEGORY); break; } } private void animateSanta() { boolean play = false; if (mWavingAnim == null) { mWavingAnim = AnimationUtils.loadAnimation(this, R.anim.santa_wave); play = true; } else if (mWavingAnim.hasEnded()) { play = true; } if (play) { playSoundOnce(R.raw.ho_ho_ho); mWavingSantaArm.startAnimation(mWavingAnim); } } private void showAchievements() { GoogleApiClient apiClient = mGamesFragment.getGamesApiClient(); if (apiClient != null && apiClient.isConnected()) { startActivityForResult( Games.Achievements.getAchievementsIntent(apiClient), RC_GAMES); } } private void showLeaderboards() { GoogleApiClient apiClient = mGamesFragment.getGamesApiClient(); if (apiClient != null && apiClient.isConnected()) { startActivityForResult( Games.Leaderboards.getAllLeaderboardsIntent(apiClient), RC_GAMES); } } /** * Shows/hides one of the FloatingActionButtons. This sets both the visibility and the * appropriate anchor, which is required to keep the FAB hidden. */ private void setFabVisibility(FloatingActionButton fab, boolean show) { if (show) { fab.setVisibility(View.VISIBLE); } else { fab.setVisibility(View.GONE); } } @Override public void playSoundOnce(int resSoundId) { mAudioPlayer.playTrack(resSoundId, false); } @Override public Context getActivityContext() { return this; } @Override public void launchActivity(Intent intent) { launchActivityInternal(intent, 0); } @Override public void launchActivityDelayed(final Intent intent, View v) { launchActivityInternal(intent, 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 = mSantaCardAdapter.getLauncher(SantaCardAdapter.KEY_SANTA_CARD); 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(tracker.getClickTarget()); } } /* * Service communication */ static class IncomingHandler extends Handler { private WeakReference<StartupActivity> mActivityRef; IncomingHandler(StartupActivity activity) { mActivityRef = new WeakReference<>(activity); } /* Order in which messages are received: Data updates, State of Service [Idle, Error] */ @Override public void handleMessage(Message msg) { StartupActivity activity = mActivityRef.get(); if (activity == null) { return; } SantaLog.d(TAG, "message=" + msg.what); 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_CASTDISABLED: activity.mFlagDisableCast = (msg.arg1 == SantaServiceMessages.DISABLED); activity.onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_GAMES: final int arg = msg.arg1; activity.mGameDisabledState = new GameDisabledState(arg); 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, long delayMs) { if (!mLaunchingChild) { mLaunchingChild = true; // stop timer if (mCountdown != null) { mCountdown.cancel(); } SantaNotificationBuilder.dismissNotifications(getApplicationContext()); mHandler.postDelayed(new Runnable() { @Override public void run() { startActivityForResult(intent, RC_STARTUP); mLaunchingChild = false; } }, delayMs); } } private class CardListPagerAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener { private Context mContext; private RecyclerView[] recyclerViews; CardListPagerAdapter(Context ctx) { mContext = ctx; recyclerViews = new RecyclerView[NUM_CATEGORIES]; } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { if(recyclerViews[position] != null) { mCategory = position; updateCategory(); } } @Override public void onPageScrollStateChanged(int state) { } @Override public int getCount() { return NUM_CATEGORIES; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { LayoutInflater inflater = LayoutInflater.from(mContext); if (recyclerViews[position] == null) { recyclerViews[position] = (RecyclerView) inflater.inflate( R.layout.card_list, container, false); recyclerViews[position].setAdapter(mCardAdapters[position]); recyclerViews[position].setLayoutManager(mCardLayoutManagers[position]); recyclerViews[position].addOnScrollListener(mScrollListeners[position]); // Set tag key for testing recyclerViews[position].setTag(position); } container.removeView(recyclerViews[position]); container.addView(recyclerViews[position]); return recyclerViews[position]; } @Override public CharSequence getPageTitle(int position) { return super.getPageTitle(position); } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } } }