package com.androidexperiments.landmarker;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.location.Location;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Toast;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
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.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResult;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.creativelabs.androidexperiments.typecompass.R;
import com.androidexperiments.landmarker.data.NearbyPlace;
import com.androidexperiments.landmarker.sensors.HeadTracker;
import com.androidexperiments.landmarker.util.HeadTransform;
import com.androidexperiments.landmarker.widget.DirectionalTextViewContainer;
import com.androidexperiments.landmarker.widget.IntroView;
import com.androidexperiments.landmarker.widget.SwingPhoneView;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
import de.greenrobot.event.EventBus;
import se.walkercrou.places.GooglePlaces;
import se.walkercrou.places.Place;
public class MainActivity extends BaseActivity implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener
{
private static final String TAG = MainActivity.class.getSimpleName();
//go to https://code.google.com/apis/console to register an app and get a key!
private static final String PLACES_API_KEY = Secrets.PLACES_API_KEY;
private static final String STATE_RESOLVING_ERROR = "resolving_error";
private static final double MAX_RADIUS = 1000;
private static final int REQUEST_CHECK_SETTINGS = 100;
/**
* attempts at finding a location with decent accuracy
*/
private static final int MAX_UPDATE_TRIES = 5;
/**
* if a location is older than an hour, try and get a new one
*/
private static final int MIN_AGE_IN_HOURS = 1;
private GoogleApiClient mGoogleApiClient;
private boolean mResolvingError = false;
private Location mLastLocation;
private GooglePlaces mPlacesApi;
@InjectView(R.id.intro_view) IntroView mIntroView;
@InjectView(R.id.swing_phone_view) SwingPhoneView mSwingPhoneView;
@InjectView(R.id.directional_text_view_container) DirectionalTextViewContainer mDirectionalTextViewContainer;
@InjectView(R.id.maps_button_view_container) View mMapsButtonViewContainer;
private NearbyPlace mCurrentPlace;
private boolean mIsFirstRun = true;
private boolean mIsConnectedToGApi = false;
private boolean mIsReadyToCheckLastLocation = false;
private LocationRequest mLocationReq;
private HeadTracker mHeadTracker;
private HeadTransform mHeadTransform;
private Handler mTrackingHandler = new Handler();
private boolean mIsTracking = false;
private float[] mEulerAngles = new float[3];
private boolean mHasPlaces = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mResolvingError = savedInstanceState != null && savedInstanceState.getBoolean(STATE_RESOLVING_ERROR, false);
mHasPlaces = false;
initViews();
initSensors();
buildGoogleApiClient();
buildPlacesApi();
}
private void initViews() {
ButterKnife.inject(this);
mSwingPhoneView.setVisibility(View.GONE);
mDirectionalTextViewContainer.setVisibility(View.GONE);
}
private void initSensors()
{
mHeadTracker = HeadTracker.createFromContext(this);
mHeadTransform = new HeadTransform();
}
protected synchronized void buildGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
private void buildPlacesApi() {
mPlacesApi = new GooglePlaces(PLACES_API_KEY);
}
@Override
protected void onStart() {
super.onStart();
if (!mResolvingError && !mGoogleApiClient.isConnected()) { // more about this later
Log.d(TAG, "onStart() && Api.connect()");
mGoogleApiClient.connect();
}
}
@Override
protected void onResume() {
super.onResume();
//events
EventBus.getDefault().register(this);
//sensors
mHeadTracker.startTracking();
//drawing
mDirectionalTextViewContainer.startDrawing();
//animateIn
if(mIsFirstRun)
{
animateTitleIn();
mIsFirstRun = false;
return;
}
//resuming from pause/maps
if(mHasPlaces)
startTracking();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_RESOLVING_ERROR, mResolvingError);
}
@Override
protected void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
mIsTracking = false;
mHeadTracker.stopTracking();
mDirectionalTextViewContainer.stopDrawing();
}
@Override
protected void onStop() {
super.onStop();
mGoogleApiClient.disconnect();
}
//butterknife
@OnClick(R.id.maps_button_view)
public void onMapsButtonClick()
{
if(mCurrentPlace == null)
{
Log.w(TAG, "No currentPlace available - must be empty. Ignore click.");
return;
}
try {
Intent intent = new Intent(
Intent.ACTION_VIEW,
Uri.parse("geo:0,0?q=" + URLEncoder.encode(mCurrentPlace.getName(), "UTF-8"))
);
//cheating!
intent.setPackage("com.google.android.apps.maps");
startActivity(intent);
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@OnClick(R.id.maps_button_close)
public void onMapsViewCloseClicked()
{
hideMapsButtonView();
}
@OnClick(R.id.maps_button_view_container)
public void onContainerClick() {
//do nothing - just need registered for onClick so it doesnt get passed through
}
//overrides
@Override
public void onBackPressed()
{
if(mMapsButtonViewContainer.getVisibility() == View.VISIBLE)
hideMapsButtonView();
else
super.onBackPressed();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_CHECK_SETTINGS)
{
if(resultCode == RESULT_OK) {
//settings have been enabled, continue forward!
setLocationListener();
}
else {
//we need location enabled for this app to work, so exit if we can't
Toast.makeText(
this,
"Location Services need to be enabled for app to function. Please enable and try again.",
Toast.LENGTH_LONG)
.show();
this.finish();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
//event bus
/**
* handle when a place is clicked
* @param event custom EventBus event
*/
public void onEvent(DirectionalTextViewContainer.OnPlaceClickedEvent event)
{
if(event.place == null) {
Log.w(TAG, "ignoring because no place is currently available.");
return;
}
mCurrentPlace = event.place;
showMapsButtonView();
}
//private api
private void animateTitleIn()
{
final Runnable completeRunner = new Runnable() {
@Override
public void run() {
if(mIsConnectedToGApi)
checkLastLocation();
else
mIsReadyToCheckLastLocation = true;
}
};
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mIntroView.animateIn(completeRunner);
}
});
}
}, 500);
}
private void showMapsButtonView()
{
mMapsButtonViewContainer.setVisibility(View.VISIBLE);
Animation anim = new AlphaAnimation(0.f, 1.f);
anim.setDuration(300);
mMapsButtonViewContainer.startAnimation(anim);
}
private void hideMapsButtonView()
{
mMapsButtonViewContainer.setVisibility(View.GONE);
Animation anim = new AlphaAnimation(1.f, 0.f);
anim.setDuration(300);
mMapsButtonViewContainer.startAnimation(anim);
}
/**
* method for refreshing content from Places API.
* will check location if its latest and do as needed
*/
private void checkLastLocation()
{
mLocationReq = new LocationRequest();
mLocationReq.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
mLocationReq.setInterval(1000);
mLocationReq.setFastestInterval(5000);
mLocationReq.setNumUpdates(MAX_UPDATE_TRIES);
mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
if(mLastLocation == null) {
checkSettings();
return;
}
//it exists, see how old it is
int hours = getLocationAgeHours(mLastLocation);
Log.d(TAG, mLastLocation + "\nHours since update: " + hours);
if(hours > MIN_AGE_IN_HOURS) { // || seconds > 15 ) //for testing
setLocationListener();
return;
}
//location is fine, update places
getNewPlaces();
}
private int getLocationAgeHours(Location loc)
{
long duration = (SystemClock.elapsedRealtimeNanos() - loc.getElapsedRealtimeNanos()) / 1000000L;
int seconds = (int) Math.floor(duration / 1000);
// Log.d(TAG, "getLocationAge() elapsed: " + (SystemClock.elapsedRealtimeNanos() / 1000000L) + " location: " + (loc.getElapsedRealtimeNanos() / 1000000L) + " seconds: " + seconds);
return (int) Math.floor(seconds / 60 / 60);
}
private void checkSettings()
{
//get settings request for our location request
LocationSettingsRequest req = new LocationSettingsRequest.Builder()
.addLocationRequest(mLocationReq)
.build();
PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, req);
result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
@Override
public void onResult(LocationSettingsResult result) {
final Status status = result.getStatus();
switch (status.getStatusCode()) {
case LocationSettingsStatusCodes.SUCCESS:
setLocationListener();
break;
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
// Location settings are not satisfied. But could be fixed by showing the user a dialog.
try {
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
status.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, e.getLocalizedMessage());
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
// Location settings are not satisfied. However, we have no way to fix the
// settings so we won't animateIn the dialog.
break;
}
}
});
}
private void setLocationListener()
{
Log.d(TAG, "setLocationListener() " + mLocationReq);
PendingResult<Status> result = LocationServices.FusedLocationApi.requestLocationUpdates(
mGoogleApiClient,
mLocationReq,
new LocationListener() {
int numTries = 0;
@Override
public void onLocationChanged(Location location)
{
numTries++;
Log.d(TAG, "onLocationChanged() attempt: " + numTries + " :: " + location);
if(getLocationAgeHours(location) <= MIN_AGE_IN_HOURS || numTries == MAX_UPDATE_TRIES) {
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
mLastLocation = location;
getNewPlaces();
}
}
}
);
result.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
Log.d(TAG, "setLocationListener() result status: " + status);
}
});
}
private void getNewPlaces()
{
//update introview
runOnUiThread(new Runnable() {
@Override
public void run() {
mIntroView.setIsFindingPlaces();
}
});
//find some places!
new AsyncTask<Void, Void, List<Place>>()
{
@Override
protected List<Place> doInBackground(Void... params)
{
List<Place> places = null;
try {
places = mPlacesApi.getNearbyPlaces(mLastLocation.getLatitude(), mLastLocation.getLongitude(), MAX_RADIUS, 60);
}
catch(Exception e) {
//if getNearbyPlaces fails, return null and directional will do what it needs to
Log.e(TAG, e.getLocalizedMessage());
e.printStackTrace();
}
return places;
}
@Override
protected void onPostExecute(List<Place> places)
{
if(places == null)
{
Toast.makeText(
MainActivity.this,
"There are no places near you - Please try again later.",
Toast.LENGTH_LONG
).show();
goBackToSplash();
return;
}
mHasPlaces = true;
startTracking();
mDirectionalTextViewContainer.updatePlaces(places, mLastLocation);
showSwingPhoneView();
}
}.execute();
}
private void showSwingPhoneView() {
mIntroView.animateOut();
//animate in triggers its own animate out once completed, the next method
mSwingPhoneView.animateIn();
}
public void onEvent(SwingPhoneView.OnAnimateOutCompleteEvent event) {
mDirectionalTextViewContainer.animateIn();
}
private void goBackToSplash() {
Intent intent = new Intent(this, SplashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); //unneccessary with finish()?
startActivity(intent);
this.finish();
}
private void startTracking()
{
mIsTracking = true;
mTrackingHandler.post(new Runnable() {
@Override
public void run() {
if(!mIsTracking) return;
mHeadTracker.getLastHeadView(mHeadTransform.getHeadView(), 0);
mHeadTransform.getEulerAngles(mEulerAngles, 0);
runOnUiThread(updateDirectionalTextView);
mTrackingHandler.postDelayed(this, 100);
}
});
}
private Runnable updateDirectionalTextView = new Runnable() {
@Override
public void run() {
mDirectionalTextViewContainer.updateView(Math.toDegrees(mEulerAngles[1]));
}
};
//google api stuffs
@Override
public void onConnected(Bundle bundle)
{
Log.d(TAG, "onConnected() " + (bundle != null ? bundle.toString() : "null"));
mIsConnectedToGApi = true;
if(mIsReadyToCheckLastLocation) {
checkLastLocation();
mIsReadyToCheckLastLocation = false;
}
}
@Override
public void onConnectionSuspended(int i)
{
Log.d(TAG, "onConnectionSuspended() " + i);
mIsConnectedToGApi = false;
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "onConnectionFailed() " + connectionResult);
GooglePlayServicesUtil.getErrorDialog(connectionResult.getErrorCode(), this, 0, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
Log.d(TAG, "onCancelDialog()");
}
}).show();
}
}