/* * 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.presentquest; import android.Manifest; import android.content.DialogInterface; import android.content.Intent; import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.util.Pair; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.apps.santatracker.presentquest.model.Messages; import com.google.android.apps.santatracker.presentquest.model.Place; import com.google.android.apps.santatracker.presentquest.model.Present; import com.google.android.apps.santatracker.presentquest.model.User; import com.google.android.apps.santatracker.presentquest.model.Workshop; import com.google.android.apps.santatracker.presentquest.ui.CirclePulseAnimator; import com.google.android.apps.santatracker.presentquest.ui.ClickMeAnimator; import com.google.android.apps.santatracker.presentquest.ui.OnboardingView; import com.google.android.apps.santatracker.presentquest.ui.PlayGameDialog; import com.google.android.apps.santatracker.presentquest.ui.PlayJetpackDialog; import com.google.android.apps.santatracker.presentquest.ui.PlayWorkshopDialog; import com.google.android.apps.santatracker.presentquest.ui.ScoreTextAnimator; import com.google.android.apps.santatracker.presentquest.ui.SlideAnimator; import com.google.android.apps.santatracker.presentquest.util.Config; import com.google.android.apps.santatracker.presentquest.util.FuzzyLocationUtil; import com.google.android.apps.santatracker.presentquest.util.MarkerCache; import com.google.android.apps.santatracker.presentquest.util.PreferencesUtil; import com.google.android.apps.santatracker.presentquest.util.VibrationUtil; import com.google.android.apps.santatracker.util.MapHelper; import com.google.android.apps.santatracker.util.MeasurementManager; import com.google.android.apps.santatracker.util.NetworkHelper; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.firebase.analytics.FirebaseAnalytics; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import pub.devrel.easypermissions.AfterPermissionGranted; import pub.devrel.easypermissions.AppSettingsDialog; import pub.devrel.easypermissions.EasyPermissions; public class MapsActivity extends AppCompatActivity implements GoogleMap.OnCameraMoveListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnMapLongClickListener, GoogleMap.OnMyLocationButtonClickListener, LocationListener, OnMapReadyCallback, View.OnClickListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, PlayGameDialog.GameDialogListener, EasyPermissions.PermissionCallbacks { private static final String TAG = "PQ(MapsActivity)"; // RequestCode for launching the Workshop game public static final int RC_WORKSHOP_GAME = 9004; // RequestCode for launching the Jetpack game public static final int RC_JETPACK_GAME = 9005; // Intent action for moving workshop mode public static final String ACTION_MOVE_WORKSHOP = "ACTION_MOVE_WORKSHOP"; // Extras for moving workshop public static final String EXTRA_MOVE_WORKSHOP_ID = "move_workshop_id"; // Zoom when we're automatically moving the map as location changes. private static final int FOLLOWING_ZOOM = 16; // Location permissions private static final int RC_PERMISSIONS = 101; private static final int RC_SETTINGS = 102; private static final String[] PERMISSIONS_REQUIRED = new String[]{ Manifest.permission.ACCESS_FINE_LOCATION}; // Map and location private GoogleApiClient mGoogleApiClient; private SupportMapFragment mSupportMapFragment; private GoogleMap mMap; // User's current location. private LatLng mCurrentLatLng; // How far has the user walked since we last recorded it? private int mMetersWalked; // Are we moving the map as the user moves. private boolean mFollowing = true; // View elements for workshop moving flow private Marker mMovingWorkshopMarker; private View mWorkshopView; // Current game user private User mUser; // DialogFragment that can launch the Workshop game private PlayWorkshopDialog mWorkshopDialog = new PlayWorkshopDialog(); // DialogFragment that can launch the Jetpack game private PlayJetpackDialog mJetpackDialog = new PlayJetpackDialog(); // Marker showing current location private Marker mLocationMarker; // Circle that pulses around current location private CirclePulseAnimator mActionHorizonAnimator; private Circle mActionHorizon; // Text and animator for showing game scores private TextView mScoreTextView; private ScoreTextAnimator mScoreTextAnimator; // Views showing current level/progress. private ImageView mAvatarView; private ClickMeAnimator mAvatarAnimator; // Views showing bag fullness private ImageView mBagView; private ClickMeAnimator mBagAnimator; // Offline scrim private ViewGroup mMapScrim; // Snackbar private ViewGroup mSnackbar; private TextView mSnackbarText; private ImageView mSnackbarButton; // True when this activity has been launched for the purposes of moving the workshop private boolean mIsInWorkshopMoveMode = false; // Cache for Marker resources private MarkerCache mMarkerCache; // Map of workshop ID --> Marker private Map<Long, Marker> mWorkshopMarkers = new HashMap<>(); // Map of present ID --> Marker private Map<Long, Marker> mPresentMarkers = new HashMap<>(); // List of known presents private List<Present> mPresents; private List<Present> mReachablePresents = new ArrayList<>(); // List of known workshops private List<Workshop> mWorkshops; private List<Workshop> mReachableWorkshops = new ArrayList<>(); // Shared Prefs private PreferencesUtil mPreferences; // Firebase Analytics private FirebaseAnalytics mAnalytics; // Firebase Analytics private Config mConfig; // General-purpose handler private Handler mHandler = new Handler(); // Receiver for results from PlacesIntentService private PlacesIntentService.NearbyResultReceiver mNearbyReceiver = new PlacesIntentService.NearbyResultReceiver() { @Override public void onResult(LatLng result) { // Add present result Log.d(TAG, "nearbyRecever:onResult"); addPresent(result, false); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_maps); // [ANALYTICS] mAnalytics = FirebaseAnalytics.getInstance(this); MeasurementManager.recordScreenView(mAnalytics, getString(R.string.analytics_screen_pq_map)); // Init marker cache mMarkerCache = new MarkerCache(this); // Init prefs mPreferences = new PreferencesUtil(this); // Init views mAvatarView = (ImageView) findViewById(R.id.map_user_image); mBagView = (ImageView) findViewById(R.id.image_bag); mWorkshopView = findViewById(R.id.workshop); mScoreTextView = (TextView) findViewById(R.id.map_text_big_score); mMapScrim = (ViewGroup) findViewById(R.id.offline_map_scrim); mSnackbar = (ViewGroup) findViewById(R.id.snackbar_container); mSnackbarText = (TextView) findViewById(R.id.snackbar_text); mSnackbarButton = (ImageView) findViewById(R.id.close_snackbar); // Onboarding OnboardingView onboardingView = (OnboardingView) findViewById(R.id.container_onboarding); onboardingView.setOnFinishListener(new OnboardingView.OnFinishListener() { @Override public void onFinish() { mPreferences.setHasOnboarded(true); } }); if (mPreferences.getHasOnboarded()) { onboardingView.setVisibility(View.GONE); } else { onboardingView.setVisibility(View.VISIBLE); } // Animator(s) mScoreTextAnimator = new ScoreTextAnimator(mScoreTextView); mAvatarAnimator = new ClickMeAnimator(mAvatarView); mBagAnimator = new ClickMeAnimator(mBagView); // Dialog listeners mJetpackDialog.setListener(this); mWorkshopDialog.setListener(this); // Debug UI initializeDebugUI(); // Set current level mUser = User.get(); setUserProgress(); // Firebase config mConfig = new Config(); // Click listeners mWorkshopView.setOnClickListener(this); mSnackbarButton.setOnClickListener(this); mMapScrim.setOnClickListener(this); findViewById(R.id.blue_bar).setOnClickListener(this); findViewById(R.id.map_user_image).setOnClickListener(this); findViewById(R.id.fab_location).setOnClickListener(this); findViewById(R.id.fab_accept_workshop_move).setOnClickListener(this); findViewById(R.id.fab_cancel_workshop_move).setOnClickListener(this); if (getIntent() != null) { mIsInWorkshopMoveMode = ACTION_MOVE_WORKSHOP.equals(getIntent().getAction()); } initializeMap(); } @Override protected void onStart() { super.onStart(); // Update workshops and presents mWorkshops = Workshop.listAll(Workshop.class); mPresents = Present.listAll(Present.class); // Register result receiver for nearby places LocalBroadcastManager.getInstance(this).registerReceiver(mNearbyReceiver, PlacesIntentService.getNearbySearchIntentFilter()); // Start animations if (mActionHorizonAnimator != null && mActionHorizon != null) { mActionHorizonAnimator.start(); } // Nudge the user to go to the profile if (mPreferences.getHasCollectedPresent() && !mPreferences.getHasVisitedProfile()) { mAvatarAnimator.start(); } else { mAvatarAnimator.stop(); } initializeUI(); } @Override protected void onResume() { super.onResume(); if (mMap != null && mCurrentLatLng != null) { mMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { @Override public void onMapLoaded() { // Update workshops and draw markers mWorkshops = Workshop.listAll(Workshop.class); drawMarkers(); } }); } } @Override protected void onStop() { super.onStop(); // Unregister result receiver for nearby places LocalBroadcastManager.getInstance(this).unregisterReceiver(mNearbyReceiver); // Stop animations if (mActionHorizonAnimator != null) { mActionHorizonAnimator.stop(); } if (mAvatarAnimator != null) { mAvatarAnimator.stop(); } if (mBagAnimator != null) { mBagAnimator.stop(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RC_JETPACK_GAME) { // Get score of the game int score = data != null ? data.getIntExtra("jetpack_score", 0) : 0; onPresentsCollected(score); } if (requestCode == RC_WORKSHOP_GAME) { // Get score of the game int stars = data != null ? data.getIntExtra("presentDropStars", 1) : 1; onPresentsReturned(stars); } // User has returned from being sent to the Settings screen to enable permissions if (requestCode == RC_SETTINGS) { if (EasyPermissions.hasPermissions(this, PERMISSIONS_REQUIRED)) { // We have the permissions we need, start location tracking startLocationTracking(); } else { // User did not complete the task, quit the game onCompletePermissionDenial(); } } } @Override public void onBackPressed() { if (mIsInWorkshopMoveMode) { onCancelWorkshopMove(); return; } super.onBackPressed(); } private void initializeMap() { // Check for network and begin if (NetworkHelper.hasNetwork(this)) { // Kick off the map load, which kicks off the location update cycle which // is where all of the action happens mSupportMapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mSupportMapFragment.getMapAsync(this); // Hide the offline overlay mMapScrim.setVisibility(View.GONE); } else { // Show "offline" overlay mMapScrim.setVisibility(View.VISIBLE); } } private void onPresentsCollected(int score) { if (score > 0) { // Show the game score after a 500ms delay showScoreText(score, 500); // Mark that the user has done a successful present collection mPreferences.setHasCollectedPresent(true); // [ANALYTICS] MeasurementManager.recordPresentsCollected(mAnalytics, score); } // Update number of presents collected mUser.collectPresents(score); setUserProgress(); // If the user's bag is full (or almost full) show a message if (mUser.getBagFillPercentage() >= 100) { showSnackbar(Messages.UNLOAD_BAG); } else if (mUser.getBagFillPercentage() >= 75) { showSnackbar(Messages.BAG_ALMOST_FULL); } } private void onPresentsReturned(int stars) { // Returning present works like this: // 0 stars - return 50% of bag // 1 stars - return 70% of bag // 2 stars - return 90% of bag // 3 stars - return 100% of bag float factor; int presentsReturned; switch (stars) { case 3: factor = 1.0f; break; case 2: factor = 0.90f; break; case 1: factor = 0.70f; break; default: factor = 0.50f; break; } // Multiply number of presents collected by return factor presentsReturned = (int) (mUser.presentsCollected * factor); if (presentsReturned > 0) { // Show the game score after a 500ms delay showScoreText(presentsReturned, 500); // Mark that the user has done a successful present return mPreferences.setHasReturnedPresent(true); // [ANALYTICS] MeasurementManager.recordPresentsReturned(mAnalytics, presentsReturned); } // Update number of presents returned int previousLevel = mUser.getLevel(); mUser.returnPresentsAndEmpty(presentsReturned); setUserProgress(); int currentLevel = mUser.getLevel(); if (currentLevel != previousLevel) { // [ANALYTICS] MeasurementManager.recordPresentQuestLevel(mAnalytics, currentLevel); // Start level up animation onLevelUp(); } } private void setUserProgress() { // Set level text ((TextView) findViewById(R.id.text_current_level)).setText(String.valueOf(mUser.getLevel())); // Set level progress ((ProgressBar) findViewById(R.id.progress_level)).setProgress(mUser.getLevelProgress()); // Set avatar image mAvatarView.setImageDrawable(ContextCompat.getDrawable(this, mUser.getAvatar())); // Set bag fullness in text and image int percentFull = mUser.getBagFillPercentage(); int bagImageId; if (percentFull >= 100) { bagImageId = R.drawable.bag_6; } else if (percentFull >= 75) { bagImageId = R.drawable.bag_5; } else if (percentFull >= 50) { bagImageId = R.drawable.bag_4; } else if (percentFull >= 25) { bagImageId = R.drawable.bag_3; } else if (percentFull > 0) { bagImageId = R.drawable.bag_2; } else { bagImageId = R.drawable.bag_1; } // Show fullness as percentage (0 to 100%) String percentString = String.valueOf(percentFull) + "%"; ((TextView) findViewById(R.id.text_bag_level)).setText(percentString); // Show fullness image mBagView.setImageResource(bagImageId); // Pulse the bag if it's 100% full if (percentFull >= 100) { mBagAnimator.start(); } else { mBagAnimator.stop(); } } private void launchProfileActivity() { Intent intent = ProfileActivity.getIntent(MapsActivity.this, false, mCurrentLatLng); Pair<View, String> imagePair = new Pair<>(findViewById(R.id.map_user_image), "user_image"); Pair<View, String> levelTextPair = new Pair<>(findViewById(R.id.layout_level_text), "level_text"); ActivityOptionsCompat options = ActivityOptionsCompat .makeSceneTransitionAnimation(MapsActivity.this, imagePair, levelTextPair); startActivity(intent, options.toBundle()); } private void onLevelUp() { mHandler.postDelayed(new Runnable() { @Override public void run() { Intent intent = ProfileActivity.getIntent(MapsActivity.this, true, mCurrentLatLng); startActivity(intent); overridePendingTransition(R.anim.fade_in, R.anim.fade_out); } }, 500); } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; mMap.setIndoorEnabled(false); mMap.setOnCameraMoveListener(this); mMap.setOnMarkerClickListener(this); mMap.setOnMapLongClickListener(this); mMap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style)); // Min and max zoom mMap.setMaxZoomPreference(FOLLOWING_ZOOM + 1.0f); mMap.setMinZoomPreference(FOLLOWING_ZOOM - 3.0f); // Only enable showing user location if in workshop edit mode //noinspection MissingPermission mMap.setMyLocationEnabled(mIsInWorkshopMoveMode); mMap.getUiSettings().setMapToolbarEnabled(false); mMap.getUiSettings().setIndoorLevelPickerEnabled(false); if (mIsInWorkshopMoveMode) { drawMarkerForWorkshop(getMovingWorkshopId()); startWorkshopMove(getMovingWorkshopId()); } else { startLocationTracking(); } } @AfterPermissionGranted(RC_PERMISSIONS) private void startLocationTracking() { // Check for location permissions if (!EasyPermissions.hasPermissions(this, PERMISSIONS_REQUIRED)) { // Request permissions EasyPermissions.requestPermissions(this, getString(R.string.perm_location_rationale), RC_PERMISSIONS, PERMISSIONS_REQUIRED); } if (mGoogleApiClient == null) { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .enableAutoManage(this, this) .addApi(LocationServices.API) .build(); } else if (mGoogleApiClient.isConnected()) { requestLocationUpdates(); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { // Forward results to EasyPermissions EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } @SuppressWarnings("MissingPermission") private void requestLocationUpdates() { if (!EasyPermissions.hasPermissions(this, PERMISSIONS_REQUIRED)) { return; } Location location = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); if (location != null) { onLocationChanged(location); } LocationRequest request = new LocationRequest(); request.setInterval(mConfig.LOCATION_REQUEST_INTERVAL_MS); request.setFastestInterval(mConfig.LOCATION_REQUEST_INTERVAL_FASTEST_MS); request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, request, this); } @Override public void onLocationChanged(Location location) { if (location == null) { Log.w(TAG, "onLocationChanged: null location"); return; } // Check if no workshop exists, create one if the user has ever collected a present if (mWorkshops.isEmpty() && mUser.getPresentsCollectedAllTime() > 0) { Workshop workshop = new Workshop(); // Put the workshop very close to current location but slightly offset LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); workshop.setLatLng(FuzzyLocationUtil.fuzz(latLng)); workshop.save(); // Reload workshops mWorkshops = Workshop.listAll(Workshop.class); } // If this is the first run, we'll draw markers. boolean firstRun = mCurrentLatLng == null; // Update our current location only if we've moved at least a metre, to avoid // jitter due to lack of accuracy in FusedLocationApi. LatLng newLatLng = new LatLng(location.getLatitude(), location.getLongitude()); if (firstRun || Distance.between(mCurrentLatLng, newLatLng) > 1) { updateMetersWalked(mCurrentLatLng, newLatLng); mCurrentLatLng = newLatLng; if (mFollowing) { if (firstRun) { safeMoveCamera(mMap, CameraUpdateFactory.newLatLngZoom(mCurrentLatLng, FOLLOWING_ZOOM)); } else { safeAnimateCamera(mMap, CameraUpdateFactory.newLatLngZoom(mCurrentLatLng, FOLLOWING_ZOOM)); } } } if (firstRun && !mPresents.isEmpty()) { animateToNearbyPresents(); } // Draw all markers on the map drawMarkers(); drawMarkerLabels(); // Update reachable presents and workshops, nudge the user to click on things when appropriate int oldPresentsReachable = mReachablePresents.size(); mReachablePresents = getPresentsReachable(); if ((mReachablePresents.size() > oldPresentsReachable) && mUser.getBagFillPercentage() < 100) { // New present reachable and bag is not full, show prompt showSnackbar(Messages.CLICK_PRESENT); } int oldWorkshopsReachable = mReachableWorkshops.size(); mReachableWorkshops = getWorkshopsReachable(); if ((mReachableWorkshops.size() > oldWorkshopsReachable) && mUser.getBagFillPercentage() > 0) { // New workshop reachable and bag is not empty, show prompt showSnackbar(Messages.CLICK_WORKSHOP); } // Ask the places service for a new place if too few nearby. int near = getPresentsNearby().size(); if (near < mConfig.MIN_NEARBY_PRESENTS) { // [DEBUG ONLY] log the user's position if (isDebug()) { Log.d(TAG, "Searching for places near: " + mCurrentLatLng); } PlacesIntentService.startNearbySearch(this, mCurrentLatLng, mConfig.NEARBY_RADIUS_METERS); } // Action horizon initCurrentLocationMarkers(); // Update locations mLocationMarker.setPosition(mCurrentLatLng); mActionHorizon.setCenter(mCurrentLatLng); } private void initCurrentLocationMarkers() { // Show current location MarkerOptions options = mMarkerCache.getElfMarker().position(mCurrentLatLng); if (mLocationMarker == null) { mLocationMarker = mMap.addMarker(options); } else { MarkerCache.updateMarker(mLocationMarker, options); } // Create circle around current location if (mActionHorizon == null) { mActionHorizon = mMap.addCircle(new CircleOptions() .center(mCurrentLatLng) .radius(10.0f) .strokeColor(ContextCompat.getColor(this, R.color.action_horizon_stroke)) .fillColor(ContextCompat.getColor(this, R.color.action_horizon_fill))); } // Create animations for circle around current location if (mActionHorizonAnimator == null) { mActionHorizonAnimator = new CirclePulseAnimator(this, mActionHorizon, mConfig.REACHABLE_RADIUS_METERS); mActionHorizonAnimator.start(); } } private void addPresent(LatLng latLng, boolean force) { if (!isNearLatLng(latLng) || force) { // Don't add if too close. float chanceIsLarge = mUser.getLargePresentChance(); float randomChance = new Random().nextFloat(); boolean isLarge = (randomChance <= chanceIsLarge); Present newPresent = new Present(latLng, isLarge); newPresent.save(); // Show a message about the new present showSnackbar(Messages.NEW_PRESENT); // [ANALYTICS] MeasurementManager.recordPresentDropped(mAnalytics, newPresent.isLarge); // Reload the presents mPresents = getPresentsSorted(); // If adding the present exceeds MAX_PRESENTS, delete the farthest. if (mPresents.size() > mConfig.MAX_PRESENTS) { Present removePresent = mPresents.get(mPresents.size() - 1); deletePresent(removePresent); } animateToNearbyPresents(); drawMarkers(); drawMarkerLabels(); } } private void animateToNearbyPresents() { // Animate to bounds showing presents and current location. LatLngBounds.Builder bounds = LatLngBounds.builder().include(mCurrentLatLng); List<Present> nearbyPresents = getPresentsNearby(); if (nearbyPresents.size() == 0) { // If no nearby presents, add last one nearbyPresents.add(Present.last(Present.class)); } else { for (Present present : nearbyPresents) { bounds.include(present.getLatLng()); } } safeAnimateCamera(mMap, CameraUpdateFactory.newLatLngBounds(bounds.build(), MapHelper.getMapPadding(mSupportMapFragment))); } private boolean isNearLatLng(LatLng latLng) { if (latLng == null) { return false; } return Distance.between(mCurrentLatLng, latLng) <= mConfig.REACHABLE_RADIUS_METERS; } private void initializeUI() { if (mIsInWorkshopMoveMode) { findViewById(R.id.fab_location).setVisibility(View.GONE); findViewById(R.id.fab_accept_workshop_move).setVisibility(View.VISIBLE); findViewById(R.id.fab_cancel_workshop_move).setVisibility(View.VISIBLE); } else { findViewById(R.id.fab_location).setVisibility(View.VISIBLE); findViewById(R.id.fab_accept_workshop_move).setVisibility(View.GONE); findViewById(R.id.fab_cancel_workshop_move).setVisibility(View.GONE); } } private void initializeDebugUI() { if (!isDebug()) { return; } FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_debug); fab.setVisibility(View.VISIBLE); final PopupMenu popup = new PopupMenu(this, fab); MenuInflater inflater = popup.getMenuInflater(); inflater.inflate(R.menu.menu_debug_popup, popup.getMenu()); // Show the menu when clicked fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popup.show(); } }); // Handle debug menu clicks popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if (id == R.id.item_drop_present) { // This should be about 20m away, so within the action radius LatLng fuzzyCurrent = FuzzyLocationUtil.fuzz(mCurrentLatLng); addPresent(fuzzyCurrent, true); } if (id == R.id.item_delete_present) { if (!mPresents.isEmpty()) { deletePresent(mPresents.get(0)); drawMarkers(); drawMarkerLabels(); } } if (id == R.id.item_play_jetpack) { showPlayJetpackDialog(null); } if (id == R.id.item_play_present_toss) { showPlayWorkshopDialog(); } if (id == R.id.item_collect_20) { onPresentsCollected(20); } if (id == R.id.item_return) { onPresentsReturned(3); } if (id == R.id.item_downlevel) { mUser.downlevel(); setUserProgress(); } if (id == R.id.reset_prefs) { // Reset prefs mPreferences.resetAll(); // Reset user mUser.presentsCollected = 0; mUser.presentsReturned = 0; mUser.save(); // Delete all workshops Workshop.deleteAll(Workshop.class); // Delete all places and presents Present.deleteAll(Present.class); Place.deleteAll(Place.class); // Restart the activity recreate(); } return true; } }); } private void drawMarkers() { // Workshop markers for (Workshop workshop : mWorkshops) { boolean isNear = isNearLatLng(workshop.getLatLng()); Marker workshopMarker = mWorkshopMarkers.get(workshop.getId()); // Get workshop marker options MarkerOptions options = mMarkerCache.getWorkshopMarker(isNear) .position(workshop.getLatLng()) .visible(false); // Add or update marker if (workshopMarker == null) { workshopMarker = mMap.addMarker(options); } else { MarkerCache.updateMarker(workshopMarker, options); } // Tag marker with workshop workshopMarker.setTag(workshop); // Hide workshop markers for moving workshop workshopMarker.setVisible(getMovingWorkshopId() != workshop.getId()); // Cache mWorkshopMarkers.put(workshop.getId(), workshopMarker); } // Present markers, only drawn when not in workshop moving mode if (!mIsInWorkshopMoveMode) { for (Present present : mPresents) { boolean isNear = isNearLatLng(present.getLatLng()); Marker presentMarker = mPresentMarkers.get(present.getId()); // Get present marker options MarkerOptions options = mMarkerCache.getPresentMarker(present, isNear) .position(present.getLatLng()); // Add or update marker if (presentMarker == null) { presentMarker = mMap.addMarker(options); } else { MarkerCache.updateMarker(presentMarker, options); } // Tag marker with present presentMarker.setTag(present); // Cache mPresentMarkers.put(present.getId(), presentMarker); } } } /** * Draws a single Workshop marker, always in non-pin mode. * @param workshopId the ID of the workshop to draw. */ private void drawMarkerForWorkshop(long workshopId) { Workshop workshop = Workshop.findById(Workshop.class, workshopId); Marker workshopMarker = mMap.addMarker(mMarkerCache.getWorkshopMarker(true) .position(workshop.getLatLng()) .visible(false)); workshopMarker.setTag(workshop); workshopMarker.setVisible(getMovingWorkshopId() != workshop.getId()); // Cache mWorkshopMarkers.put(workshopId, workshopMarker); } /** * Draw helpful labels on top of map markers to nudge the player in the right direction. */ private void drawMarkerLabels() { // Show marker label if the user has never collected a present if (!mPreferences.getHasCollectedPresent()) { // Get first present marker long presentId = mPresents.isEmpty() ? -1 : mPresents.get(0).getId(); Marker presentMarker = mPresentMarkers.get(presentId); // Show marker label if (presentMarker != null) { presentMarker.setTitle(getString(R.string.go_here)); presentMarker.showInfoWindow(); } } else { // If the user has collected a present, hide all marker labels on presents for (Marker presentMarker : mPresentMarkers.values()) { presentMarker.setTitle(null); presentMarker.hideInfoWindow(); } } // Show marker label if the user has collected presents but never returned if (mUser.getBagFillPercentage() > 0 && !mPreferences.getHasReturnedPresent()) { // Get workshop marker long workshopId = mWorkshops.isEmpty() ? -1 : mWorkshops.get(0).getId(); Marker workshopMarker = mWorkshopMarkers.get(workshopId); // Show marker label if (workshopMarker != null) { workshopMarker.setTitle(getString(R.string.go_here)); workshopMarker.showInfoWindow(); } } else { // Hide the workshop marker labels for (Marker workshopMarker : mWorkshopMarkers.values()) { workshopMarker.setTitle(null); workshopMarker.hideInfoWindow(); } } } private long getMovingWorkshopId() { if (!mIsInWorkshopMoveMode) { return 0; } else { return getIntent().getLongExtra(EXTRA_MOVE_WORKSHOP_ID, 1L); } } private void toast(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } /** * Move the camera. If the map is not yet loaded, catch the exception and try the move again * after it loads. This helps to avoid a rare race condition on very low-spec devices. */ private void safeMoveCamera(final GoogleMap map, final CameraUpdate cameraUpdate) { try { map.moveCamera(cameraUpdate); } catch (IllegalStateException e) { map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { @Override public void onMapLoaded() { map.moveCamera(cameraUpdate); } }); } } /** * Animate the camera. If the map is not yet loaded, catch the exception and try the animation * again after it loads. This helps to avoid a rare race condition on very low-spec devices. */ private void safeAnimateCamera(final GoogleMap map, final CameraUpdate cameraUpdate) { try { map.animateCamera(cameraUpdate); } catch (IllegalStateException e) { map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { @Override public void onMapLoaded() { map.animateCamera(cameraUpdate); } }); } } @Override public boolean onMarkerClick(Marker marker) { Object object = marker.getTag(); if (object instanceof Workshop) { return onWorkshopMarkerClick((Workshop) object, marker); } else if (object instanceof Present) { return onPresentMarkerClick((Present) object, marker); } else { return false; } } private boolean onWorkshopMarkerClick(Workshop workshop, Marker marker) { if (isDebug() && !workshop.isMovable()) { workshop.updated = 0; workshop.save(); toast("DEBUG: Workshop now movable"); } if (!isNearLatLng(marker.getPosition())) { showSnackbar(Messages.WORKSHOP_TOO_FAR); VibrationUtil.vibratePattern(this, VibrationUtil.PATTERN_BAD); } else if (mUser.presentsCollected == 0) { showSnackbar(Messages.NO_PRESENTS_COLLECTED); VibrationUtil.vibratePattern(this, VibrationUtil.PATTERN_BAD); } else { showPlayWorkshopDialog(); } return true; } private void startWorkshopMove(long workshopId) { if (mMovingWorkshopMarker != null) { return; } Workshop workshop = Workshop.findById(Workshop.class, workshopId); Marker workshopMarker = mWorkshopMarkers.get(workshopId); // Go to workshop location, or near the last workshops if this is a new workshop LatLng target = workshop.getLatLng(); if (Workshop.NULL_LATLNG.equals(target)) { Workshop firstWorkshop = Workshop.first(Workshop.class); LatLng firstLatLng = firstWorkshop.getLatLng(); target = FuzzyLocationUtil.fuzz(firstLatLng); } safeAnimateCamera(mMap, CameraUpdateFactory.newLatLngZoom(target, FOLLOWING_ZOOM)); mWorkshopView.setVisibility(View.VISIBLE); mMovingWorkshopMarker = workshopMarker; mMovingWorkshopMarker.setVisible(false); } private boolean onPresentMarkerClick(Present present, Marker marker) { if (isNearLatLng(present.getLatLng())) { if (mUser.getBagFillPercentage() >= 100) { // Bag is full, warn the user but allow them to get more presents showSnackbar(Messages.BAG_IS_FULL); VibrationUtil.vibratePattern(this, VibrationUtil.PATTERN_BAD); } // Near the present, play the jetpack game showPlayJetpackDialog(present); // TODO: Only delete if they play the game deletePresent(present); } else { // Too far from the presents showSnackbar(Messages.PRESENT_TOO_FAR); VibrationUtil.vibratePattern(this, VibrationUtil.PATTERN_BAD); } return true; } private void showPlayWorkshopDialog() { mWorkshopDialog.show(getSupportFragmentManager(), PlayWorkshopDialog.TAG); } private void showPlayJetpackDialog(Present present) { mJetpackDialog.show(getSupportFragmentManager(), PlayJetpackDialog.TAG); if (present != null) { mJetpackDialog.setPresent(present); } } private void showScoreText(final int score, long delayMs) { mScoreTextView.postDelayed(new Runnable() { @Override public void run() { mScoreTextAnimator.start("+" + String.valueOf(score)); } }, delayMs); } private void showSnackbar(Messages.Message message) { // Don't show messages if already showing the snackbar if (mSnackbar.getVisibility() == View.VISIBLE) { return; } // Record how many times a message is displayed, and don't display it too many times. int numTimesDisplayed = mPreferences.getMessageTimesDisplayed(message); if (numTimesDisplayed >= message.timesToShow) { Log.d(TAG, "showSnackbar: not showing " + message.key); return; } else { mPreferences.incrementMessageTimesDisplayed(message); } // Hide location button, it can get in the way findViewById(R.id.fab_location).setVisibility(View.GONE); // Set text and slide up mSnackbarText.setText(getString(message.stringId)); SlideAnimator.slideUp(mSnackbar); // Slide back down after some delay mSnackbar.postDelayed(new Runnable() { @Override public void run() { hideSnackbar(); } }, 3000); } private void hideSnackbar() { // Don't play the hide animation if the snackbar is already invisible if (mSnackbar.getVisibility() != View.VISIBLE) { return; } // Show location button findViewById(R.id.fab_location).setVisibility(View.VISIBLE); // Hide snackbar SlideAnimator.slideDown(mSnackbar); } private void onCancelWorkshopMove() { Workshop workshop = (Workshop) mMovingWorkshopMarker.getTag(); if (Workshop.NULL_LATLNG.equals(workshop.getLatLng())) { workshop.delete(); } setResult(RESULT_CANCELED); finish(); } private void endWorkshopMove() { // Workshop view was clicked - workshop placement mode exited, so save the new // workshop location, hide the workshop view, and redraw markers (to reflect the // new workshop location). LatLng latLng = mMap.getCameraPosition().target; Workshop workshop = (Workshop) mMovingWorkshopMarker.getTag(); workshop.setLatLng(latLng); workshop.saveWithTimestamp(); mWorkshopView.setVisibility(View.INVISIBLE); mMovingWorkshopMarker = null; } @Override public boolean onMyLocationButtonClick() { if (!mFollowing && mCurrentLatLng != null) { safeAnimateCamera(mMap, CameraUpdateFactory.newLatLngZoom(mCurrentLatLng, FOLLOWING_ZOOM)); mFollowing = true; } return true; } @Override public void onCameraMove() { mFollowing = false; } private void updateMetersWalked(LatLng oldLocation, LatLng newLocation) { if (oldLocation == null || newLocation == null) { return; } int distance = Distance.between(oldLocation, newLocation); mMetersWalked += distance; // Record distances in increments of 100 meters walked while (mMetersWalked >= 100) { MeasurementManager.recordHundredMetersWalked(mAnalytics); mMetersWalked -= 100; } } private List<Present> getPresentsSorted() { List<Present> presents = Present.listAll(Present.class); Collections.sort(presents, new PresentComparator()); return presents; } private List<Present> getPresentsNearby() { return getPresentsInRadius(mConfig.NEARBY_RADIUS_METERS); } private List<Present> getPresentsReachable() { return getPresentsInRadius(mConfig.REACHABLE_RADIUS_METERS); } private List<Present> getPresentsInRadius(int radius) { if (mCurrentLatLng == null) { return new ArrayList<>(); } List<Present> presents = new ArrayList<>(); for (Present present : mPresents) { if (Distance.between(mCurrentLatLng, present.getLatLng()) <= radius) { presents.add(present); } } return presents; } private List<Workshop> getWorkshopsReachable() { if (mCurrentLatLng == null) { return new ArrayList<>(); } List<Workshop> workshops = new ArrayList<>(); for (Workshop workshop : mWorkshops) { if (Distance.between(mCurrentLatLng, workshop.getLatLng()) <= mConfig.REACHABLE_RADIUS_METERS) { workshops.add(workshop); } } return workshops; } @Override public void onClick(View v) { int id = v.getId(); if (id == R.id.map_user_image || id == R.id.blue_bar) { mAvatarAnimator.stop(); mPreferences.setHasVisitedProfile(true); launchProfileActivity(); } else if (id == R.id.workshop) { // No-op } else if (id == R.id.fab_location){ onMyLocationButtonClick(); } else if (id == R.id.fab_accept_workshop_move) { endWorkshopMove(); setResult(RESULT_OK); finish(); } else if (id == R.id.fab_cancel_workshop_move) { onCancelWorkshopMove(); } else if (id == R.id.close_snackbar) { hideSnackbar(); } else if (id == R.id.offline_map_scrim) { initializeMap(); } } @Override public void onConnected(@Nullable Bundle bundle) { requestLocationUpdates(); } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { Log.w(TAG, "onConnectionFailed:" + connectionResult); } @Override public void onDialogShow() { if (mActionHorizonAnimator != null) { mActionHorizonAnimator.stop(); } } @Override public void onDialogDismiss() { if (mActionHorizonAnimator != null) { mActionHorizonAnimator.start(); } } @Override public void onPermissionsGranted(int requestCode, List<String> perms) { Log.d(TAG, "onPermissionsGranted:" + perms); startLocationTracking(); } @Override public void onPermissionsDenied(int requestCode, List<String> perms) { Log.d(TAG, "onPermissionsDenied:" + perms); if (!EasyPermissions.somePermissionPermanentlyDenied(this, perms)) { // If they deny it but non-permanently, we have to ask again startLocationTracking(); } else { // This game will not work without location permissions, so we need to detect // if permissions are permanently denied and send the user to the settings screen new AppSettingsDialog.Builder(this, getString(R.string.perm_go_to_settings)) .setTitle(getString(R.string.perm_required)) .setPositiveButton(getString(android.R.string.ok)) .setNegativeButton(getString(android.R.string.cancel), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { onCompletePermissionDenial(); } }) .setRequestCode(RC_SETTINGS) .build() .show(); } } private void onCompletePermissionDenial() { // Show Toast message Toast.makeText(this, getString(R.string.required_permissions_missing), Toast.LENGTH_SHORT).show(); // Finish the Activity finish(); } private class PresentComparator implements Comparator<Present> { @Override public int compare(Present a, Present b) { int distA = Distance.between(mCurrentLatLng, a.getLatLng()); int distB = Distance.between(mCurrentLatLng, b.getLatLng()); return distA - distB; } } private void deletePresent(Present present) { Marker marker = mPresentMarkers.remove(present.getId()); if (marker != null) { marker.remove(); } present.delete(); mPresents = getPresentsSorted(); // Reload. } @Override public void onMapLongClick(LatLng location) { if (isDebug()) { // Add a present where the click was addPresent(location, true); } } private boolean isDebug() { return getPackageName().contains("debug"); } }