/* * 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.animation.Animator; import android.annotation.TargetApi; import android.app.Dialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.FocusHighlight; import android.support.v17.leanback.widget.ItemBridgeAdapter; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.ObjectAdapter; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentActivity; import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.DecodeFormat; import com.google.android.apps.santatracker.AudioPlayer; import com.google.android.apps.santatracker.BuildConfig; import com.google.android.apps.santatracker.R; import com.google.android.apps.santatracker.data.SantaPreferences; import com.google.android.apps.santatracker.service.SantaService; import com.google.android.apps.santatracker.service.SantaServiceMessages; import com.google.android.apps.santatracker.util.AccessibilityUtil; import com.google.android.apps.santatracker.util.AnalyticsManager; import com.google.android.apps.santatracker.util.MeasurementManager; import com.google.android.apps.santatracker.util.SantaLog; import com.google.android.apps.santatracker.village.Village; import com.google.android.apps.santatracker.village.VillageView; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.firebase.analytics.FirebaseAnalytics; import java.lang.ref.WeakReference; /** * Launch activity for the app. Handles loading of the village, the state of the markers (based on * the date/time) and incoming voice intents. */ public class TvStartupActivity extends FragmentActivity implements View.OnClickListener, Village.VillageListener, SantaContext, LaunchCountdown.LaunchCountdownContext { protected static final String TAG = "SantaStart"; private static final String VILLAGE_TAG = "VillageFragment"; private AudioPlayer mAudioPlayer; private boolean mResumed = false; private boolean mIsDebug = false; private Village mVillage; private VillageView mVillageView; private ImageView mVillageBackdrop; private View mLaunchButton; private View mCountdownView; private VerticalGridView mMarkers; private TvCardAdapter mCardAdapter; private LaunchCountdown mCountdown; // Load these values from resources when an instance of this activity is initialised. private static long OFFLINE_SANTA_DEPARTURE; private static long OFFLINE_SANTA_FINALARRIVAL; private static long UNLOCK_JETPACK; private static long UNLOCK_ROCKET; private static long UNLOCK_SNOWDOWN; private static long UNLOCK_VIDEO_1; private static long UNLOCK_VIDEO_15; private static long UNLOCK_VIDEO_23; // Server controlled flags private long mOffset = 0; private boolean mFlagSwitchOff = false; private boolean mFlagDisableJetpack = false; private boolean mFlagDisableRocket = false; private boolean mFlagDisableSnowdown = false; private String[] mVideoList = new String[]{null, null, null}; private boolean mHaveGooglePlayServices = false; private long mFirstDeparture; private long mFinalArrival; // Handler for scheduled UI updates private Handler mHandler = new Handler(); // Waiting for data from the API (no data or data is outdated) private boolean mWaitingForApi = true; // Service integration private Messenger mService = null; private boolean mIsBound = false; private Messenger mMessenger; // request code for games Activities private static final int RC_STARTUP = 1111; // Permission request codes private static final int RC_DEBUG_PERMS = 1; private FirebaseAnalytics mMeasurement; private boolean mLaunchingChild = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestPermissionsIfDebugModeEnabled(); // Glide's pretty aggressive at caching images, so get the 8888 preference in early. if (!Glide.isSetup()) { Glide.setup(new GlideBuilder(getActivityContext()) .setDecodeFormat(DecodeFormat.PREFER_ARGB_8888)); } setContentView(R.layout.layout_startup_tv); loadResourceFields(getResources()); mIsDebug = BuildConfig.DEBUG; mMessenger= new Messenger(new IncomingHandler(this)); mCountdown = new LaunchCountdown(this); mCountdownView = findViewById(R.id.countdown_container); mAudioPlayer = new AudioPlayer(getApplicationContext()); mVillageView = (VillageView) findViewById(R.id.villageView); mVillage = (Village) getSupportFragmentManager().findFragmentByTag(VILLAGE_TAG); if (mVillage == null) { mVillage = new Village(); getSupportFragmentManager().beginTransaction().add(mVillage, VILLAGE_TAG).commit(); } mVillageBackdrop = (ImageView) findViewById(R.id.villageBackground); mLaunchButton = findViewById(R.id.launch_button); mLaunchButton.setOnClickListener(this); mMarkers = (VerticalGridView) findViewById(R.id.santa_markers); initialiseViews(); mHaveGooglePlayServices = checkGooglePlayServicesAvailable(); // Initialize measurement mMeasurement = FirebaseAnalytics.getInstance(this); MeasurementManager.recordScreenView(mMeasurement, getString(R.string.analytics_screen_village)); // [ANALYTICS SCREEN]: Village AnalyticsManager.sendScreenView(R.string.analytics_screen_village); // set the initial states resetLauncherStates(); // See if it was a voice action which triggered this activity and handle it onNewIntent(getIntent()); } private void loadResourceFields(Resources res) { final long ms = 1000L; OFFLINE_SANTA_DEPARTURE = res.getInteger(R.integer.santa_takeoff) * ms; OFFLINE_SANTA_FINALARRIVAL = res.getInteger(R.integer.santa_arrival) * ms; mFinalArrival = OFFLINE_SANTA_FINALARRIVAL; mFirstDeparture = OFFLINE_SANTA_DEPARTURE; // Game unlock UNLOCK_JETPACK = res.getInteger(R.integer.unlock_jetpack) * ms; UNLOCK_ROCKET = res.getInteger(R.integer.unlock_rocket) * ms; UNLOCK_SNOWDOWN = res.getInteger(R.integer.unlock_snowdown) * ms; // Video unlock UNLOCK_VIDEO_1 = res.getInteger(R.integer.unlock_video1) * ms; UNLOCK_VIDEO_15 = res.getInteger(R.integer.unlock_video15) * ms; UNLOCK_VIDEO_23 = res.getInteger(R.integer.unlock_video23) * ms; } void initialiseViews() { mVillageView.setVillage(mVillage); // Initialize ListRowPresenter ListRowPresenter listRowPresenter = new ListRowPresenter(FocusHighlight.ZOOM_FACTOR_SMALL); listRowPresenter.setShadowEnabled(true); final int rowHeight = getResources().getDimensionPixelOffset(R.dimen.tv_marker_height_and_shadow); listRowPresenter.setRowHeight(rowHeight); // Initialize ListRow TvCardPresenter presenter = new TvCardPresenter(this); mCardAdapter = new TvCardAdapter(this, presenter); ListRow listRow = new ListRow(mCardAdapter); // Initialize ObjectAdapter for ListRow ArrayObjectAdapter arrayObjectAdapter = new ArrayObjectAdapter(listRowPresenter); arrayObjectAdapter.add(listRow); // Initialized Debug menus only for debug build. if (mIsDebug) { addDebugMenuListRaw(arrayObjectAdapter); } // set ItemBridgeAdapter to RecyclerView ItemBridgeAdapter adapter = new ItemBridgeAdapter(arrayObjectAdapter); mMarkers.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); mMarkers.setAdapter(adapter); } private void addDebugMenuListRaw(ArrayObjectAdapter objectAdapter) { mMarkers.setPadding( mMarkers.getPaddingLeft(), mMarkers.getPaddingTop() + 150, mMarkers.getPaddingRight(), mMarkers.getPaddingBottom()); Presenter debugMenuPresenter = new Presenter() { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { TextView tv = new TextView(parent.getContext()); ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(200, 150); tv.setLayoutParams(params); tv.setGravity(Gravity.CENTER); tv.setBackgroundColor(getResources().getColor(R.color.SantaBlueDark)); tv.setFocusableInTouchMode(false); tv.setFocusable(true); tv.setClickable(true); return new ViewHolder(tv); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView)viewHolder.view).setText((String)item); viewHolder.view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String text = ((TextView)v).getText().toString(); if (text.contains("Enable Tracker")) { enableTrackerMode(true); } else if (text.contains("Enable CountDown")){ startCountdown(SantaPreferences.getCurrentTime()); } else { mIsDebug = false; initialiseViews(); resetLauncherStates(); } } }); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { } }; ObjectAdapter debugMenuAdapter = new ObjectAdapter(debugMenuPresenter) { private final String[] mMenuString = {"Enable Tracker", "Enable CountDown", "Hide DebugMenu"}; @Override public int size() { return mMenuString.length; } @Override public Object get(int position) { return mMenuString[position]; } }; ListRow debugMenuListRow = new ListRow(debugMenuAdapter); objectAdapter.add(debugMenuListRow); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void requestPermissionsIfDebugModeEnabled() { // If debug mode is enabled in debug_settings.xml, and we don't yet have storage perms, ask. if (getResources().getBoolean(R.bool.prompt_for_sdcard_perms) && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, RC_DEBUG_PERMS); } } // see http://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances/ @Override protected void onNewIntent(Intent intent) { setIntent(intent); } @Override protected void onResume() { super.onResume(); showColorMask(false); mResumed = true; } @Override protected void onPause() { super.onPause(); mResumed = false; mAudioPlayer.stopAll(); cancelUIUpdate(); } @Override protected void onStart() { super.onStart(); registerWithService(); initialiseViews(); resetLauncherStates(); } private void resetLauncherStates() { // Start only if play services are available if (mHaveGooglePlayServices) { stateNoData(); } } @Override protected void onStop() { super.onStop(); unregisterFromService(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (mResumed && hasFocus && !AccessibilityUtil.isTouchAccessiblityEnabled(this)) { mAudioPlayer.playTrackExclusive(R.raw.village_music, true); } } /** * Move to 'no valid data' state ("offline"). No further locations, rely on local offline data * only. */ private void stateNoData() { Log.d(TAG, "Santa is offline."); // Enable/disable pins and nav drawer updateNavigation(); // Schedule UI Updates scheduleUIUpdate(); // Note that in the "no data" state, this may or may not include the TIME_OFFSET, depending // on whether we've had a successful API call and still have the data. We can't use // System.currentTimeMillis() as it *will* ignore TIME_OFFSET. final long time = SantaPreferences.getCurrentTime(); AbstractLaunch launchSanta = mCardAdapter.getLauncher(TvCardAdapter.SANTA); if (time < OFFLINE_SANTA_DEPARTURE) { // Santa hasn't departed yet, show countdown launchSanta.setState(AbstractLaunch.STATE_LOCKED); startCountdown(OFFLINE_SANTA_DEPARTURE); } else if (time >= OFFLINE_SANTA_DEPARTURE && time < OFFLINE_SANTA_FINALARRIVAL) { // Santa should have already left, but no data yet, hide countdown and show message stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_DISABLED); } else { // Post Christmas stopCountdown(); enableTrackerMode(false); launchSanta.setState(AbstractLaunch.STATE_FINISHED); } } /** * Move to 'data' (online) state. */ private void stateData() { Log.d(TAG, "Santa is online."); // Enable/disable pins and nav drawer updateNavigation(); // Schedule next UI update scheduleUIUpdate(); long time = SantaPreferences.getCurrentTime(); AbstractLaunch launchSanta = mCardAdapter.getLauncher(TvCardAdapter.SANTA); // Is Santa finished? if (time > mFirstDeparture && time < OFFLINE_SANTA_FINALARRIVAL) { // Santa should be travelling, enable map and hide countdown enableTrackerMode(true); if (mFlagSwitchOff) { // Kill-switch triggered, disable button launchSanta.setState(AbstractLaunch.STATE_DISABLED); } else if (time > mFinalArrival) { // No data launchSanta.setState(AbstractLaunch.STATE_DISABLED); } else { launchSanta.setState(AbstractLaunch.STATE_READY); } } else if (time < mFirstDeparture) { // Santa hasn't taken off yet, start count-down and schedule // notification to first departure, hide buttons startCountdown(mFirstDeparture); launchSanta.setState(AbstractLaunch.STATE_LOCKED); } else { // Post Christmas, hide countdown and buttons launchSanta.setState(AbstractLaunch.STATE_FINISHED); stopCountdown(); enableTrackerMode(false); } } public void enableTrackerMode(boolean showLaunchButton) { mCountdown.cancel(); mVillageBackdrop.setImageResource(R.drawable.village_bg_launch); mVillage.setPlaneEnabled(false); mLaunchButton.setVisibility(showLaunchButton ? View.VISIBLE : View.GONE); mCountdownView.setVisibility(View.GONE); } public void startCountdown(long time) { mCountdown.startTimer(time - SantaPreferences.getCurrentTime()); mVillageBackdrop.setImageResource(R.drawable.village_bg_countdown); mVillage.setPlaneEnabled(true); mLaunchButton.setVisibility(View.GONE); mCountdownView.setVisibility(View.VISIBLE); } public void stopCountdown() { mCountdown.cancel(); mCountdownView.setVisibility(View.GONE); } /* * Village Markers */ private void updateNavigation() { mCardAdapter.getLauncher(TvCardAdapter.ROCKET).setState( getGamePinState(mFlagDisableRocket, UNLOCK_ROCKET)); mCardAdapter.getLauncher(TvCardAdapter.SNOWDOWN).setState( getGamePinState(mFlagDisableSnowdown, UNLOCK_SNOWDOWN)); ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO01)).setVideo( mVideoList[0], UNLOCK_VIDEO_1); ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO15)).setVideo( mVideoList[1], UNLOCK_VIDEO_15); ((LaunchVideo) mCardAdapter.getLauncher(TvCardAdapter.VIDEO23)).setVideo( mVideoList[2], UNLOCK_VIDEO_23); } private int getGamePinState(boolean disabledFlag, long unlockTime) { if (disabledFlag) { return AbstractLaunch.STATE_HIDDEN; } else if (!disabledFlag && SantaPreferences.getCurrentTime() < unlockTime) { return AbstractLaunch.STATE_LOCKED; } else { return AbstractLaunch.STATE_READY; } } /* * Scheduled UI update */ /** * Schedule a call to {@link #stateData()} or {@link #stateNoData()} at the next time at which * the UI should be updated (games become available, Santa takes off, Santa is finished). */ private void scheduleUIUpdate() { // cancel scheduled update cancelUIUpdate(); final long delay = calculateNextUiUpdateDelay(); if (delay > 0 && delay < Long.MAX_VALUE) { // schedule if delay is in the future mHandler.postDelayed(mUpdateUiRunnable, delay); } } private long calculateNextUiUpdateDelay() { final long time = SantaPreferences.getCurrentTime(); final long departureDelay = mFirstDeparture - time; final long arrivalDelay = mFinalArrival - time; // if disable flag is toggled, exclude from calculation final long[] delays = new long[]{ mFlagDisableJetpack ? Long.MAX_VALUE : UNLOCK_JETPACK - time, mFlagDisableRocket ? Long.MAX_VALUE : UNLOCK_ROCKET - time, mFlagDisableSnowdown ? Long.MAX_VALUE : UNLOCK_SNOWDOWN - time, departureDelay, arrivalDelay}; // find lowest delay, but only count positive values or zero (ie. that are in the future) long delay = Long.MAX_VALUE; for (final long x : delays) { if (x >= 0) { delay = Math.min(delay, x); } } return delay; } private void cancelUIUpdate() { mHandler.removeCallbacksAndMessages(null); } private Runnable mUpdateUiRunnable = new Runnable() { @Override public void run() { if (!mWaitingForApi) { stateData(); } else { stateNoData(); } } }; /* * Google Play Services - from * http://code.google.com/p/google-api-java-client/source/browse/tasks-android-sample/src/main/ * java/com/google/api/services/samples/tasks/android/TasksSample.java?repo=samples */ /** * Check that Google Play services APK is installed and up to date. */ private boolean checkGooglePlayServicesAvailable() { if (getPackageName().contains("debug")) { return true; } GoogleApiAvailability availability = GoogleApiAvailability.getInstance(); final int connectionStatusCode = availability.isGooglePlayServicesAvailable(this); if (availability.isUserResolvableError(connectionStatusCode)) { Dialog dialog = availability.getErrorDialog(this, connectionStatusCode, 123); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { finish(); } }); dialog.show(); return false; } return (connectionStatusCode == ConnectionResult.SUCCESS); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.launch_button: launchTracker(); break; } } @Override public void playSoundOnce(int resSoundId) { mAudioPlayer.playTrack(resSoundId, false); } @Override public Context getActivityContext() { return this; } @Override public void launchActivity(Intent intent) { launchActivityInternal(intent, null, 0); } @Override public void launchActivityDelayed(final Intent intent, final View v) { launchActivityInternal(intent, v, 200); } @Override public View getCountdownView() { return findViewById(R.id.countdown_container); } @Override public void onCountdownFinished() { if (!mWaitingForApi) { stateData(); } else { stateNoData(); } } /** Attempt to launch the tracker, if available. */ public void launchTracker() { AbstractLaunch launch = mCardAdapter.getLauncher(TvCardAdapter.SANTA); if (launch instanceof LaunchSanta) { LaunchSanta tracker = (LaunchSanta) launch; AnalyticsManager.sendEvent(R.string.analytics_event_category_launch, R.string.analytics_launch_action_village); // App Measurement MeasurementManager.recordCustomEvent(mMeasurement, getString(R.string.analytics_event_category_launch), getString(R.string.analytics_launch_action_village)); tracker.onClick(mLaunchButton); } } /* * Service communication */ private static class IncomingHandler extends Handler { private final WeakReference<TvStartupActivity> mActivityRef; public IncomingHandler(TvStartupActivity activity) { mActivityRef = new WeakReference<>(activity); } /* Order in which messages are received: Data updates, State of Service [Idle, Error] */ @Override public void handleMessage(Message msg) { SantaLog.d(TAG, "message=" + msg.what); final TvStartupActivity activity = mActivityRef.get(); if (activity == null) { return; } switch (msg.what) { case SantaServiceMessages.MSG_SERVICE_STATUS: // Current state of service, received once when connecting activity.onSantaServiceStateUpdate(msg.arg1); break; case SantaServiceMessages.MSG_INPROGRESS_UPDATE_ROUTE: // route is about to be updated activity.onRouteUpdateStart(); break; case SantaServiceMessages.MSG_UPDATED_ROUTE: // route data has been updated activity.onRouteDataUpdateFinished(); break; case SantaServiceMessages.MSG_UPDATED_ONOFF: activity.mFlagSwitchOff = (msg.arg1 == SantaServiceMessages.SWITCH_OFF); activity.onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_TIMES: Bundle b = (Bundle) msg.obj; activity.mOffset = b.getLong(SantaServiceMessages.BUNDLE_OFFSET); SantaPreferences.cacheOffset(activity.mOffset); activity.mFinalArrival = b.getLong(SantaServiceMessages.BUNDLE_FINAL_ARRIVAL); activity.mFirstDeparture = b.getLong(SantaServiceMessages.BUNDLE_FIRST_DEPARTURE); activity.onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_GAMES: final int arg = msg.arg1; activity.mFlagDisableJetpack = (arg & SantaServiceMessages.MSG_FLAG_GAME_JETPACK) == SantaServiceMessages.MSG_FLAG_GAME_JETPACK; activity.mFlagDisableRocket = (arg & SantaServiceMessages.MSG_FLAG_GAME_ROCKET) == SantaServiceMessages.MSG_FLAG_GAME_ROCKET; activity.mFlagDisableSnowdown = (arg & SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN) == SantaServiceMessages.MSG_FLAG_GAME_SNOWDOWN; activity.onDataUpdate(); break; case SantaServiceMessages.MSG_UPDATED_VIDEOS: Bundle data = msg.getData(); activity.mVideoList = data.getStringArray(SantaServiceMessages.BUNDLE_VIDEOS); activity.onDataUpdate(); break; case SantaServiceMessages.MSG_ERROR: // Error accessing the API, ignore because there is data. activity.onApiSuccess(); break; case SantaServiceMessages.MSG_ERROR_NODATA: activity.stateNoData(); break; case SantaServiceMessages.MSG_SUCCESS: activity.onApiSuccess(); break; default: super.handleMessage(msg); break; } } } /** * Handle the state of the SantaService when first connecting to it. */ private void onSantaServiceStateUpdate(int state) { switch (state) { case SantaServiceMessages.STATUS_IDLE: // Service is idle, data should be uptodate mWaitingForApi = false; stateData(); break; case SantaServiceMessages.STATUS_IDLE_NODATA: mWaitingForApi = true; stateNoData(); break; case SantaServiceMessages.STATUS_ERROR_NODATA: // Service is in error state and there is no valid data mWaitingForApi = true; stateNoData(); case SantaServiceMessages.STATUS_ERROR: // Service is in error state and waiting for another attempt to access API mWaitingForApi = true; stateNoData(); case SantaServiceMessages.STATUS_PROCESSING: // Service is busy processing an update, wait for success and ignore this state mWaitingForApi = true; break; } } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = new Messenger(service); //reply with local Messenger to establish bi-directional communication Message msg = Message.obtain(null, SantaServiceMessages.MSG_SERVICE_REGISTER_CLIENT); msg.replyTo = mMessenger; try { mService.send(msg); } catch (RemoteException e) { // Could not connect to Service, connection will be terminated soon. } } @Override public void onServiceDisconnected(ComponentName name) { mService = null; mIsBound = false; } }; private void onApiSuccess() { if (mWaitingForApi) { mWaitingForApi = false; stateData(); } } private void onRouteDataUpdateFinished() { // switch to 'online' mode, data has been loaded if (!mWaitingForApi) { stateData(); } } private void onRouteUpdateStart() { // temporarily switch back to offline mode until route update has finished if (!mWaitingForApi) { stateNoData(); } } private void onDataUpdate() { if (!mWaitingForApi) { stateData(); } } private void registerWithService() { bindService(new Intent(this, SantaService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; } private void unregisterFromService() { if (mIsBound) { if (mService != null) { Message msg = Message .obtain(null, SantaServiceMessages.MSG_SERVICE_UNREGISTER_CLIENT); msg.replyTo = mMessenger; try { mService.send(msg); } catch (RemoteException e) { // ignore if service is not available } } unbindService(mConnection); mIsBound = false; } } private synchronized void launchActivityInternal( final Intent intent, View srcView, long delayMs) { if (!mLaunchingChild) { mLaunchingChild = true; // stop timer if (mCountdown != null) { mCountdown.cancel(); } if (srcView != null) { playCircularRevealTransition(srcView); } mHandler.postDelayed(new Runnable() { @Override public void run() { startActivityForResult(intent, RC_STARTUP); mLaunchingChild = false; } }, delayMs); } } final Rect mSrcRect = new Rect(); @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void playCircularRevealTransition(View srcView) { showColorMask(true); View mask = findViewById(R.id.content_mask); srcView.getGlobalVisibleRect(mSrcRect); Animator anim = ViewAnimationUtils.createCircularReveal(mask, mSrcRect.centerX(), mSrcRect.centerY(), 0.f, mask.getWidth()); anim.start(); } private void showColorMask(boolean show) { int visibility = show ? View.VISIBLE: View.INVISIBLE; (findViewById(R.id.content_mask)).setVisibility(visibility); } }