Java tutorial
/* * Copyright (C) 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.santatracker.launch; import android.Manifest; import android.app.AlertDialog; 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.Bundle; 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.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; 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.Menu; import android.view.MenuItem; import android.view.View; 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.SantaApplication; import com.google.android.apps.santatracker.SantaNotificationBuilder; import com.google.android.apps.santatracker.cast.NotificationDataCastManager; import com.google.android.apps.santatracker.common.NotificationConstants; 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.SantaLog; import com.google.android.apps.santatracker.village.Village; import com.google.android.apps.santatracker.village.VillageView; import com.google.firebase.analytics.FirebaseAnalytics; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.games.Games; 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"; private static final String VILLAGE_TAG = "VillageFragment"; private static final String INTENT_HANDLED = "intent_handled"; 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 MarkerManager mMarkerManager; private CardAdapter mCardAdapter; private CardLayoutManager mCardLayoutManager; private StickyScrollListener mScrollListener; private RecyclerView mMarkers; private TextView mStatusText; private LaunchCountdown mCountdown; // Load these values from resources when an instance of this activity is initialised. private static long OFFLINE_SANTA_DEPARTURE; private static long OFFLINE_SANTA_FINALARRIVAL; private static long UNLOCK_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_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; private boolean mFlagDisableGumball = false; private boolean mFlagDisableJetpack = false; private boolean mFlagDisableMemory = false; private boolean mFlagDisableRocket = false; private boolean mFlagDisableDancer = false; private boolean mFlagDisableSnowdown = false; private String[] mVideoList = new String[] { null, null, null }; private boolean mHaveGooglePlayServices = false; private long mFirstDeparture; private long mFinalArrival; private MenuItem mMenuItemLegal; // 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 final Messenger mMessenger = new Messenger(new IncomingHandler()); // 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; private NotificationDataCastManager mCastManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestPermissionsIfDebugModeEnabled(); // Glide's pretty aggressive at caching images, so get the 8888 preference in early. if (!Glide.isSetup()) { Glide.setup(new GlideBuilder(getActivityContext()).setDecodeFormat(DecodeFormat.PREFER_ARGB_8888)); } // TODO: rename temp 'layout_startup_2015' layout when its implementation is completed. setContentView(R.layout.layout_startup_2015); 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.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); mWavingSanta = findViewById(R.id.santa_waving); mWavingSantaArm = (ImageView) findViewById(R.id.santa_arm); mWavingSanta.setOnClickListener(this); mMarkers = (RecyclerView) findViewById(R.id.markers); initialiseViews(); mHaveGooglePlayServices = NotificationDataCastManager.checkGooglePlayServices(this); // initialize our connection to Google Play Games mGamesFragment = PlayGamesFragment.getInstance(this, this); // 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); // set the initial states resetLauncherStates(); // See if it was a voice action which triggered this activity and handle it onNewIntent(getIntent()); if (mHaveGooglePlayServices) { mCastManager = SantaApplication.getCastManager(this); } } 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; // Video unlock UNLOCK_VIDEO_1 = res.getInteger(R.integer.unlock_video1) * ms; UNLOCK_VIDEO_15 = res.getInteger(R.integer.unlock_video15) * ms; UNLOCK_VIDEO_23 = res.getInteger(R.integer.unlock_video23) * ms; } void initialiseViews() { mVillageView.setVillage(mVillage); // Initialize the RecyclerView int numColumns = getResources().getInteger(R.integer.village_columns); mCardLayoutManager = new CardLayoutManager(this, numColumns); mCardAdapter = new CardAdapter(this); mScrollListener = new StickyScrollListener(mCardLayoutManager, numColumns); mMarkers.setAdapter(mCardAdapter); mMarkers.setLayoutManager(mCardLayoutManager); mMarkers.addOnScrollListener(mScrollListener); } 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 = mCardAdapter.getLaunchers(); List<AbstractLaunch> games = new ArrayList<AbstractLaunch>(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 = mCardAdapter.getLaunchers(); // try sending the voice command to all launchers, the first one that handles it wins for (AbstractLaunch l : pins) { if (handled = l.handleVoiceAction(intent)) { // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_voice), l.mContentDescription); // [ANALYTICS EVENT]: Launch Voice AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_voice, l.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 (mHaveGooglePlayServices) { mCastManager = SantaApplication.getCastManager(this); mCastManager.incrementUiCounter(); } mResumed = true; } @Override protected void onPause() { super.onPause(); mResumed = false; mAudioPlayer.stopAll(); cancelUIUpdate(); if (mCastManager != null) { mCastManager.decrementUiCounter(); } } @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); // Add cast button if (mCastManager != null) { mCastManager.addMediaRouterButton(menu, R.id.media_route_menu_item); } mMenuItemLegal = menu.findItem(R.id.legal); mMenuItemLegal.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; AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(StartupActivity.this); switch (which) { case 1: // Privacy url = resources.getString(R.string.url_privacy); break; case 2: // TOS url = resources.getString(R.string.url_tos); break; case 3: // TOS url = resources.getString(R.string.url_seismic); break; case 4: // Show play services license text dialog.dismiss(); dialogBuilder .setMessage(GooglePlayServicesUtil .getOpenSourceSoftwareLicenseInfo(getApplicationContext())) .create().show(); dialogBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); 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; } }); // TODO Temp menu items for testing notifications menu.findItem(R.id.notification_nearby).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { SantaNotificationBuilder.CreateSantaNotification(StartupActivity.this, R.string.notification_nearby); /* Intent wearIntent = new Intent(StartupActivity.this, PhoneNotificationService.class); wearIntent.setAction(NotificationConstants.ACTION_SEND); wearIntent.putExtra(NotificationConstants.KEY_CONTENT, StartupActivity.this.getResources() .getString(R.string.notification_nearby)); StartupActivity.this.startService(wearIntent); */ 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); /* Intent wearIntent = new Intent(StartupActivity.this, PhoneNotificationService.class); wearIntent.setAction(NotificationConstants.ACTION_SEND); wearIntent.putExtra(NotificationConstants.KEY_CONTENT, StartupActivity.this.getResources() .getString(R.string.notification_takeoff)); StartupActivity.this.startService(wearIntent); */ return true; } }); menu.findItem(R.id.notification_location) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { /* SantaNotificationBuilder .CreateTriviaNotification(StartupActivity.this, "location", "photoUrl", "mapUrl", "fact"); */ SantaNotificationBuilder.CreateInfoNotification(StartupActivity.this, "Title", "text", "https://www.google.com/images/srpr/logo11w.png"); 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 protected boolean onPrepareOptionsPanel(View view, Menu menu) { menu.findItem(R.id.sign_out).setVisible(mSignedIn); return super.onPrepareOptionsPanel(view, menu); } private void setCastDisabled(boolean disableCast) { if (mCastManager == null || !mHaveGooglePlayServices) { return; } // Enable or disable casting SantaApplication.toogleCast(this, disableCast); } /** * 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 (mHaveGooglePlayServices && mCastManager != null && !mCastManager.isConnected()) { setCastDisabled(true); } // Note that in the "no data" state, this may or may not include the TIME_OFFSET, depending // on whether we've had a successful API call and still have the data. We can't use // System.currentTimeMillis() as it *will* ignore TIME_OFFSET. final long time = SantaPreferences.getCurrentTime(); AbstractLaunch launchSanta = mCardAdapter.getLauncher(CardAdapter.SANTA); if (time < OFFLINE_SANTA_DEPARTURE) { // Santa hasn't departed yet, show countdown launchSanta.setState(AbstractLaunch.STATE_LOCKED); startCountdown(OFFLINE_SANTA_DEPARTURE); final long notificationTime = SantaPreferences.getAdjustedTime(OFFLINE_SANTA_DEPARTURE); SantaNotificationBuilder.ScheduleSantaNotification(getApplicationContext(), notificationTime, NotificationConstants.NOTIFICATION_TAKEOFF); } else if (time >= OFFLINE_SANTA_DEPARTURE && time < OFFLINE_SANTA_FINALARRIVAL) { // Santa should have already left, but no data yet, hide countdown and show message stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_DISABLED); showStatus(R.string.contacting_santa); } else { // Post Christmas stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_FINISHED); } } /** * Move to 'data' (online) state. */ private void stateData() { Log.d(TAG, "Santa is online."); // Enable/disable pins and nav drawer updateNavigation(); // Schedule next UI update scheduleUIUpdate(); // hide status hideStatus(); // Set cast state setCastDisabled(mFlagDisableCast); long time = SantaPreferences.getCurrentTime(); AbstractLaunch launchSanta = mCardAdapter.getLauncher(CardAdapter.SANTA); // Is Santa finished? if (time > mFirstDeparture && time < OFFLINE_SANTA_FINALARRIVAL) { // Santa should be travelling, enable map and hide countdown enableTrackerMode(true); // Schedule stream notifications SantaNotificationBuilder.ScheduleNotificationNotification(this); 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 (time < mFirstDeparture) { // 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); // Schedule stream notifications SantaNotificationBuilder.ScheduleNotificationNotification(this); startCountdown(mFirstDeparture); launchSanta.setState(AbstractLaunch.STATE_LOCKED); } else { // Post Christmas, hide countdown and buttons launchSanta.setState(AbstractLaunch.STATE_FINISHED); stopCountdown(); enableTrackerMode(false); } } public void enableTrackerMode(boolean showLaunchButton) { mCountdown.cancel(); mVillageBackdrop.setImageResource(R.drawable.village_bg_launch); mVillage.setPlaneEnabled(false); mLaunchButton.setVisibility(showLaunchButton ? View.VISIBLE : View.GONE); mSantaCollapsing.setOverlayColor(R.color.villageToolbarDark); mCountdownView.setVisibility(View.GONE); mWavingSanta.setVisibility(View.GONE); mOrnament.setVisibility(View.GONE); } public void startCountdown(long time) { mCountdown.startTimer(time - SantaPreferences.getCurrentTime()); mVillageBackdrop.setImageResource(R.drawable.village_bg_countdown); mVillage.setPlaneEnabled(true); mLaunchButton.setVisibility(View.GONE); mSantaCollapsing.setOverlayColor(R.color.villageToolbarLight); mCountdownView.setVisibility(View.VISIBLE); mWavingSanta.setVisibility(View.VISIBLE); mOrnament.setVisibility(View.VISIBLE); } public void stopCountdown() { mCountdown.cancel(); mCountdownView.setVisibility(View.GONE); } /* * Village Markers */ private void updateNavigation() { // Games mCardAdapter.getLauncher(CardAdapter.GUMBALL) .setState(getGamePinState(mFlagDisableGumball, UNLOCK_GUMBALL)); mCardAdapter.getLauncher(CardAdapter.MEMORY).setState(getGamePinState(mFlagDisableMemory, UNLOCK_MEMORY)); mCardAdapter.getLauncher(CardAdapter.JETPACK) .setState(getGamePinState(mFlagDisableJetpack, UNLOCK_JETPACK)); mCardAdapter.getLauncher(CardAdapter.ROCKET).setState(getGamePinState(mFlagDisableRocket, UNLOCK_ROCKET)); mCardAdapter.getLauncher(CardAdapter.DANCER).setState(getGamePinState(mFlagDisableDancer, UNLOCK_DANCER)); mCardAdapter.getLauncher(CardAdapter.SNOWDOWN) .setState(getGamePinState(mFlagDisableSnowdown, UNLOCK_SNOWDOWN)); ((LaunchVideo) mCardAdapter.getLauncher(CardAdapter.VIDEO01)).setVideo(mVideoList[0], UNLOCK_VIDEO_1); ((LaunchVideo) mCardAdapter.getLauncher(CardAdapter.VIDEO15)).setVideo(mVideoList[1], UNLOCK_VIDEO_15); ((LaunchVideo) mCardAdapter.getLauncher(CardAdapter.VIDEO23)).setVideo(mVideoList[2], UNLOCK_VIDEO_23); // reinitialise action bar supportInvalidateOptionsMenu(); } private int getGamePinState(boolean disabledFlag, long unlockTime) { if (disabledFlag) { return AbstractLaunch.STATE_HIDDEN; } else if (!disabledFlag && SantaPreferences.getCurrentTime() < unlockTime) { return AbstractLaunch.STATE_LOCKED; } else { return AbstractLaunch.STATE_READY; } } /* * Status Message */ private void showStatus(int i) { int state = mCardAdapter.getLauncher(CardAdapter.SANTA).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 final long[] delays = new long[] { mFlagDisableGumball ? Long.MAX_VALUE : UNLOCK_GUMBALL - time, mFlagDisableJetpack ? Long.MAX_VALUE : UNLOCK_JETPACK - time, mFlagDisableMemory ? Long.MAX_VALUE : UNLOCK_MEMORY - time, mFlagDisableRocket ? Long.MAX_VALUE : UNLOCK_ROCKET - time, mFlagDisableDancer ? Long.MAX_VALUE : UNLOCK_DANCER - time, mFlagDisableSnowdown ? Long.MAX_VALUE : UNLOCK_SNOWDOWN - time, departureDelay, arrivalDelay }; // find lowest delay, but only count positive values or zero (ie. that are in the future) long delay = Long.MAX_VALUE; for (final long x : delays) { if (x >= 0) { delay = Math.min(delay, x); } } return delay; } private void cancelUIUpdate() { mHandler.removeCallbacksAndMessages(null); } private Runnable mUpdateUiRunnable = new Runnable() { @Override public void run() { if (!mWaitingForApi) { stateData(); } else { stateNoData(); } } }; 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: animateSanta(); break; case R.id.fab_achievement: showAchievements(); break; case R.id.fab_leaderboard: showLeaderboards(); 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) { CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); if (show) { p.setAnchorId(R.id.appbar); fab.setLayoutParams(p); fab.setVisibility(View.VISIBLE); } else { p.setAnchorId(View.NO_ID); fab.setLayoutParams(p); 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 = mCardAdapter.getLauncher(CardAdapter.SANTA); if (launch instanceof LaunchSanta) { LaunchSanta tracker = (LaunchSanta) launch; AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_village); // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_village)); tracker.onClick(tracker.getClickTarget()); } } /* * Service communication */ class IncomingHandler extends Handler { /* Order in which messages are received: Data updates, State of Service [Idle, Error] */ @Override public void handleMessage(Message msg) { SantaLog.d(TAG, "message=" + msg.what); switch (msg.what) { case SantaServiceMessages.MSG_SERVICE_STATUS: // Current state of service, received once when connecting onSantaServiceStateUpdate(msg.arg1); break; case SantaServiceMessages.MSG_INPROGRESS_UPDATE_ROUTE: // route is about to be updated onRouteUpdateStart(); break; case SantaServiceMessages.MSG_UPDATED_ROUTE: // route data has been updated onRouteDataUpdateFinished(); break; case SantaServiceMessages.MSG_UPDATED_ONOFF: mFlagSwitchOff = (msg.arg1 == SantaServiceMessages.SWITCH_OFF); onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_TIMES: Bundle b = (Bundle) msg.obj; mOffset = b.getLong(SantaServiceMessages.BUNDLE_OFFSET); SantaPreferences.cacheOffset(mOffset); mFinalArrival = b.getLong(SantaServiceMessages.BUNDLE_FINAL_ARRIVAL); mFirstDeparture = b.getLong(SantaServiceMessages.BUNDLE_FIRST_DEPARTURE); onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_CASTDISABLED: mFlagDisableCast = (msg.arg1 == SantaServiceMessages.DISABLED); onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_GAMES: final int arg = msg.arg1; mFlagDisableGumball = (arg & SantaServiceMessages.MSG_FLAG_GAME_GUMBALL) == SantaServiceMessages.MSG_FLAG_GAME_GUMBALL; mFlagDisableJetpack = (arg & SantaServiceMessages.MSG_FLAG_GAME_JETPACK) == SantaServiceMessages.MSG_FLAG_GAME_JETPACK; mFlagDisableMemory = (arg & SantaServiceMessages.MSG_FLAG_GAME_MEMORY) == SantaServiceMessages.MSG_FLAG_GAME_MEMORY; mFlagDisableRocket = (arg & SantaServiceMessages.MSG_FLAG_GAME_ROCKET) == SantaServiceMessages.MSG_FLAG_GAME_ROCKET; mFlagDisableDancer = (arg & SantaServiceMessages.MSG_FLAG_GAME_DANCER) == SantaServiceMessages.MSG_FLAG_GAME_DANCER; mFlagDisableSnowdown = (arg & SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN) == SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN; onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_VIDEOS: Bundle data = msg.getData(); mVideoList = data.getStringArray(SantaServiceMessages.BUNDLE_VIDEOS); onDataUpdate(); break; case SantaServiceMessages.MSG_ERROR: // Error accessing the API, ignore because there is data. onApiSuccess(); break; case SantaServiceMessages.MSG_ERROR_NODATA: stateNoData(); break; case SantaServiceMessages.MSG_SUCCESS: 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); } } }