From 0f89563c7c186ac295da59ad82755455ad8e0a93 Mon Sep 17 00:00:00 2001 From: ynnadkrap Date: Wed, 1 Jul 2015 21:57:30 -0400 Subject: [PATCH 01/13] Create geofences for nearby hospitals --- app/app.iml | 50 +++++++- app/build.gradle | 4 + app/src/main/AndroidManifest.xml | 15 +++ .../java/org/healtheheartstudy/Constants.java | 10 ++ .../GeofenceIntentService.java | 74 +++++++++++ .../org/healtheheartstudy/HospitalHelper.java | 107 ++++++++++++++++ .../org/healtheheartstudy/MainActivity.java | 73 ++++++++--- .../java/org/healtheheartstudy/MainApp.java | 18 +++ .../org/healtheheartstudy/MainService.java | 97 ++++++++++++++ .../healtheheartstudy/PlaceSearchResult.java | 80 ++++++++++++ .../org/healtheheartstudy/client/Client.java | 49 ++++++++ .../client/GeofenceClient.java | 119 ++++++++++++++++++ .../client/LocationClient.java | 117 +++++++++++++++++ .../network/GsonRequest.java | 64 ++++++++++ .../network/RequestManager.java | 24 ++++ app/src/main/res/layout/activity_main.xml | 9 +- 16 files changed, 891 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/org/healtheheartstudy/Constants.java create mode 100644 app/src/main/java/org/healtheheartstudy/GeofenceIntentService.java create mode 100644 app/src/main/java/org/healtheheartstudy/HospitalHelper.java create mode 100644 app/src/main/java/org/healtheheartstudy/MainApp.java create mode 100644 app/src/main/java/org/healtheheartstudy/MainService.java create mode 100644 app/src/main/java/org/healtheheartstudy/PlaceSearchResult.java create mode 100644 app/src/main/java/org/healtheheartstudy/client/Client.java create mode 100644 app/src/main/java/org/healtheheartstudy/client/GeofenceClient.java create mode 100644 app/src/main/java/org/healtheheartstudy/client/LocationClient.java create mode 100644 app/src/main/java/org/healtheheartstudy/network/GsonRequest.java create mode 100644 app/src/main/java/org/healtheheartstudy/network/RequestManager.java diff --git a/app/app.iml b/app/app.iml index 2e68398..08e2231 100644 --- a/app/app.iml +++ b/app/app.iml @@ -70,7 +70,29 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -84,11 +106,37 @@ + - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7c5f0c1..2405cb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,4 +22,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.2.0' + compile 'com.google.android.gms:play-services:7.5.0' + compile 'com.mcxiaoke.volley:library:1.0.+' + compile 'com.google.code.gson:gson:2.2.+' + compile 'com.jakewharton.timber:timber:3.1.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f408466..b777259 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,12 @@ + + + + + + + + + diff --git a/app/src/main/java/org/healtheheartstudy/Constants.java b/app/src/main/java/org/healtheheartstudy/Constants.java new file mode 100644 index 0000000..0228cc3 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/Constants.java @@ -0,0 +1,10 @@ +package org.healtheheartstudy; + +/** + * Created by dannypark on 7/1/15. + */ +public class Constants { + + public static final String INTENT_HOSPITAL_NAME = "hospital_name"; + +} diff --git a/app/src/main/java/org/healtheheartstudy/GeofenceIntentService.java b/app/src/main/java/org/healtheheartstudy/GeofenceIntentService.java new file mode 100644 index 0000000..cf70587 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/GeofenceIntentService.java @@ -0,0 +1,74 @@ +package org.healtheheartstudy; + +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.text.TextUtils; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingEvent; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +/** + * GeofenceIntentService is invoked when the user enters a geofence. + */ +public class GeofenceIntentService extends IntentService { + + public GeofenceIntentService() { + super("GeofenceIntentService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + GeofencingEvent gEvent = GeofencingEvent.fromIntent(intent); + if (gEvent.hasError()) { + Timber.e("Geofence error code: " + gEvent.getErrorCode()); + } else if (gEvent.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_DWELL) { + List places = gEvent.getTriggeringGeofences(); + buildNotification(places); + } else { + Timber.e("Geofences were triggered with wrong transitions"); + } + } + + + private void buildNotification(List geofences) { + // Create the intent extra which contains the geofence(s) name(s) that were triggered + String intentExtra; + if (geofences.size() > 0) { + List geofenceNames = new ArrayList(); + for (Geofence g : geofences) { + geofenceNames.add(g.getRequestId()); + } + intentExtra = TextUtils.join(",", geofenceNames); + } else { + Geofence geofence = geofences.get(0); + intentExtra = geofence.getRequestId(); + } + + // Create intent to launch when notification is selected. If the activity is already + // running, bring it to the top and finish all other activities + Intent displayIntent = new Intent(this, MainActivity.class); + displayIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + displayIntent.putExtra(Constants.INTENT_HOSPITAL_NAME, intentExtra); + PendingIntent displayPendingIntent = PendingIntent.getActivity(this, 0, displayIntent, 0); + + NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_media_pause) + .setContentTitle("Health eHeart") + .setContentText("We noticed that you're near a hospital and we wanted to make sure you're OK!") + .setContentIntent(displayPendingIntent) + .setAutoCancel(true) + .setVibrate(new long[]{500, 500, 500, 500}); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(1, notifBuilder.build()); + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/HospitalHelper.java b/app/src/main/java/org/healtheheartstudy/HospitalHelper.java new file mode 100644 index 0000000..0792290 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/HospitalHelper.java @@ -0,0 +1,107 @@ +package org.healtheheartstudy; + +import android.location.Location; +import android.os.Handler; + +import com.android.volley.Response; +import com.android.volley.VolleyError; + +import org.healtheheartstudy.network.GsonRequest; +import org.healtheheartstudy.network.RequestManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * HospitalHelper retrieves all hospitals that are within a provided range of a provided location + * by using Google Places Search API (not a client API--need to use web API). + */ +public class HospitalHelper implements Response.Listener, Response.ErrorListener { + + private static final String API_KEY = "AIzaSyALHFB5BA3emjybU9ZrPUCLDEcCyC37vjk"; + private static final String API_ROOT = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=" + API_KEY; + + private List mHospitalsInArea; + private Listener listener; + + interface Listener { + void onHospitalsFound(List hospitals); + } + + /** + * Launches a request to retrieve hospitals that are within the provided radius + * away from the provided location. This is a public helper function. + * @param location The area to find hospitals in. + * @param radius The search radius in meters. + * @param listener The callback for when hospitals are found. + */ + public synchronized void findHospitalsInArea(Location location, int radius, Listener listener) { + this.mHospitalsInArea = new ArrayList<>(); + this.listener = listener; + findHospitalsInArea(location.getLatitude(), location.getLongitude(), radius, null); + } + + /** + * Makes a request to find all hospitals within the provided radius + * of location. + * @param lat + * @param lng + * @param radius + * @param nextPageToken @Nullable. If set, we need to retrieve the next page of results + */ + private void findHospitalsInArea(double lat, double lng, int radius, String nextPageToken) { + // Build URL + String requestURL = ""; + if (nextPageToken == null) { + requestURL = API_ROOT + "&location=" + lat + "," + lng + + "&radius=" + radius + + "&types=hospital"; + } else { + requestURL = API_ROOT + "&pagetoken=" + nextPageToken; + } + + // Build request + GsonRequest request = new GsonRequest<>( + requestURL, + PlaceSearchResult.class, + null, + this, + this + ); + + // Launch request + RequestManager.getRequestQueue().add(request); + } + + /** + * Handles success callback for retrieving a list of nearby hospitals. Will make another request + * if there are additional pages to retrieve. + * @param response + */ + @Override + public void onResponse(final PlaceSearchResult response) { + mHospitalsInArea.addAll(response.getPlaces()); + boolean moreResultsToFetch = !response.getNextPage().isEmpty(); + if (moreResultsToFetch) { + // Google docs state that the nextPage won't be accessible for 'a few moments', so + // take a 5 second nap and then request the next page + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + findHospitalsInArea(0, 0, 0, response.getNextPage()); + } + }, 5000); + } else { + listener.onHospitalsFound(mHospitalsInArea); + } + } + + /** + * Handles error callback for retrieving a list of nearby hospitals. + * @param error + */ + @Override + public void onErrorResponse(VolleyError error) { + // TODO: do something here + } +} diff --git a/app/src/main/java/org/healtheheartstudy/MainActivity.java b/app/src/main/java/org/healtheheartstudy/MainActivity.java index 96793de..e478770 100644 --- a/app/src/main/java/org/healtheheartstudy/MainActivity.java +++ b/app/src/main/java/org/healtheheartstudy/MainActivity.java @@ -1,38 +1,79 @@ package org.healtheheartstudy; +import android.content.DialogInterface; +import android.content.Intent; import android.support.v7.app.ActionBarActivity; import android.os.Bundle; +import android.support.v7.app.AlertDialog; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.widget.Button; public class MainActivity extends ActionBarActivity { + private Button mButton; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + + mButton = (Button) findViewById(R.id.main_button); + if (MainService.IS_ALIVE) mButton.setText("Turn tracking off"); + else mButton.setText("Turn tracking on"); + + String hospitalName = getIntent().getStringExtra(Constants.INTENT_HOSPITAL_NAME); + if (hospitalName != null) displaySurvey(hospitalName); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; + public void buttonClick(View v) { + if (MainService.IS_ALIVE) { + Intent service = new Intent(this, MainService.class); + stopService(service); + mButton.setText("Turn tracking on"); + } else { + Intent service = new Intent(this, MainService.class); + startService(service); + mButton.setText("Turn tracking off"); + } + } + + private void displaySurvey(final String hospitalName) { + AlertDialog builder = new AlertDialog.Builder(this) + .setTitle("Hospital Alert") + .setMessage("We noticed that you were near " + hospitalName + ". Are you visiting the hospital" + + "to treat a medical condition?") + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { +// Intent intent = new Intent(Constants.INTENT_USER_SURVEY); +// intent.putExtra(Constants.INTENT_VISITED_HOSPITAL, true); +// intent.putExtra(Constants.INTENT_HOSPITAL_NAME, hospitalName); +// LocalBroadcastManager.getInstance(MainActivity.this).sendBroadcast(intent); + dialog.dismiss(); + } + }) + .setNegativeButton("No", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setCancelable(false) + .create(); + builder.show(); } @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } + protected void onNewIntent(Intent intent) { + String hospitalName = getIntent().getStringExtra(Constants.INTENT_HOSPITAL_NAME); + if (hospitalName != null) displaySurvey(hospitalName); + } - return super.onOptionsItemSelected(item); + @Override + public void onDestroy() { + super.onDestroy(); } } diff --git a/app/src/main/java/org/healtheheartstudy/MainApp.java b/app/src/main/java/org/healtheheartstudy/MainApp.java new file mode 100644 index 0000000..2e2e4d9 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/MainApp.java @@ -0,0 +1,18 @@ +package org.healtheheartstudy; + +import android.app.Application; + +import org.healtheheartstudy.network.RequestManager; + +import timber.log.Timber; + +public class MainApp extends Application { + + @Override + public void onCreate() { + super.onCreate(); + RequestManager.init(this); + if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree()); + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/MainService.java b/app/src/main/java/org/healtheheartstudy/MainService.java new file mode 100644 index 0000000..1688dc9 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/MainService.java @@ -0,0 +1,97 @@ +package org.healtheheartstudy; + +import android.app.Service; +import android.content.Intent; +import android.location.Location; +import android.os.IBinder; + +import com.google.android.gms.location.LocationRequest; + +import org.healtheheartstudy.client.GeofenceClient; +import org.healtheheartstudy.client.LocationClient; + +import java.util.List; + +import timber.log.Timber; + +public class MainService extends Service implements LocationClient.Listener, HospitalHelper.Listener { + + public static boolean IS_ALIVE = false; + + private LocationClient mLocationClient; + private GeofenceClient mGeofenceClient; + private HospitalHelper mHospitalHelper; + + private boolean isGeofenceConnected; + + @Override + public void onCreate() { + super.onCreate(); + IS_ALIVE = true; + + // Create clients + mHospitalHelper = new HospitalHelper(); + mLocationClient = new LocationClient(this, this); + mGeofenceClient = new GeofenceClient(this, new GeofenceClient.Listener() { + @Override + public void onConnected() { + isGeofenceConnected = true; + } + }); + + // Connect clients + mLocationClient.connect(); + mGeofenceClient.connect(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() { + super.onDestroy(); + IS_ALIVE = false; + mLocationClient.disconnect(); + mGeofenceClient.disconnect(); + } + + @Override + public IBinder onBind(Intent intent) { + // TODO: Return the communication channel to the service. + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void onConnected() { + // Instead of getting lastKnownLocation, request a single location update for more precision + LocationRequest mLocationRequest = new LocationRequest(); + mLocationRequest.setInterval(1000 * 60 * 2); + mLocationRequest.setFastestInterval(30000); + mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + mLocationRequest.setNumUpdates(1); + mLocationClient.requestLocationUpdates(mLocationRequest); + } + + @Override + public void onLocationChanged(Location location) { + Timber.d("Found user's location: " + location.toString()); + mHospitalHelper.findHospitalsInArea(location, 10000, this); + mLocationClient.disconnect(); // No longer needed + } + + @Override + public void onHospitalsFound(List hospitals) { + Timber.d("********** Found hospitals **********"); + for (PlaceSearchResult.Place hospital : hospitals) { + Timber.d(hospital.name); + } + if (isGeofenceConnected) { + mGeofenceClient.createGeofences(hospitals); + } else { + // TODO: Do something here...wait for connection? (Should almost always be connected at this point) + } + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/PlaceSearchResult.java b/app/src/main/java/org/healtheheartstudy/PlaceSearchResult.java new file mode 100644 index 0000000..7c9698f --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/PlaceSearchResult.java @@ -0,0 +1,80 @@ +package org.healtheheartstudy; + +import java.util.ArrayList; +import java.util.List; + +/** + * PlaceSearchResult is a subset of Places from Google Places API. It is molded to our needs. + */ +public class PlaceSearchResult { + + private String status; + private List results; + private List html_attributions; + private String next_page_token; + + PlaceSearchResult() { + status = ""; + results = new ArrayList<>(); + html_attributions = new ArrayList<>(); + next_page_token = ""; + } + + public String getStatus() { + return status; + } + + public List getPlaces() { + return results; + } + + public String getNextPage() { + return next_page_token; + } + + public class Place { + + public Geometry geometry; + public String icon; + public String name; + public String place_id; + public String scope; + public List types; + public String vicinity; + + Place() { + geometry = new Geometry(); + icon = ""; + name = ""; + place_id = ""; + scope = ""; + types = new ArrayList<>(); + vicinity = ""; + } + + public android.location.Location getLocation() { + android.location.Location location = new android.location.Location(""); + location.setLatitude(geometry.location.lat); + location.setLongitude(geometry.location.lng); + return location; + } + + class Geometry { + Location location; + Geometry() { + location = new Location(); + } + } + + class Location { + double lat; + double lng; + Location() { + lat = 0.0; + lng = 0.0; + } + } + + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/client/Client.java b/app/src/main/java/org/healtheheartstudy/client/Client.java new file mode 100644 index 0000000..8d6ea2b --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/client/Client.java @@ -0,0 +1,49 @@ +package org.healtheheartstudy.client; + +import android.content.Context; +import android.os.Bundle; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +/** + * Client is a wrapper to easily use GoogleApiClient. + */ +public class Client implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + + private Context context; + private Api api; + protected GoogleApiClient mGoogleApiClient; + + protected synchronized void connect(Context context, Api api) { + this.context = context; + this.api = api; + if (mGoogleApiClient == null || !mGoogleApiClient.isConnected()) { + mGoogleApiClient = new GoogleApiClient.Builder(context) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .addApi(api) + .build(); + mGoogleApiClient.connect(); + } + } + + public void disconnect() { + mGoogleApiClient.disconnect(); + } + + @Override + public void onConnected(Bundle bundle) { + } + + @Override + public void onConnectionSuspended(int i) { + connect(context, api); + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + + } +} diff --git a/app/src/main/java/org/healtheheartstudy/client/GeofenceClient.java b/app/src/main/java/org/healtheheartstudy/client/GeofenceClient.java new file mode 100644 index 0000000..007258b --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/client/GeofenceClient.java @@ -0,0 +1,119 @@ +package org.healtheheartstudy.client; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; + +import org.healtheheartstudy.GeofenceIntentService; +import org.healtheheartstudy.PlaceSearchResult; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +/** + * GeofenceClient provides an easy-to-use interface for creating geofences. It is specifically + * designed to build geofences around 'places' from Google Places API. + */ +public class GeofenceClient extends Client implements ResultCallback { + + private static final int GEOFENCE_RADIUS_METERS = 100; + + private List mGeofences; + private PendingIntent mGeofencePendingIntent; + private Context context; + private List mPlaces; + private Listener mListener; + + public interface Listener { + void onConnected(); + } + + public GeofenceClient(Context context, Listener listener) { + this.context = context; + this.mListener = listener; + mGeofences = new ArrayList<>(); + } + + public void connect() { + connect(context, LocationServices.API); + } + + /** + * Handles the creation and setting of geofences. + * @param places + */ + public void createGeofences(List places) { + mPlaces = places; + populateGeofenceList(); + LocationServices.GeofencingApi.addGeofences( + mGoogleApiClient, + getGeofencingRequest(), + getGeofencePendingIntent() + ).setResultCallback(this); + } + + private GeofencingRequest getGeofencingRequest() { + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); + builder.addGeofences(mGeofences); + return builder.build(); + } + + private PendingIntent getGeofencePendingIntent() { + if (mGeofencePendingIntent != null) { + return mGeofencePendingIntent; + } + Intent intent = new Intent(context, GeofenceIntentService.class); + mGeofencePendingIntent = + PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return mGeofencePendingIntent; + } + + private void populateGeofenceList() { + for (PlaceSearchResult.Place place : mPlaces) { + Geofence gf = new Geofence.Builder() + .setRequestId(place.name) + .setCircularRegion( + place.getLocation().getLatitude(), + place.getLocation().getLongitude(), + GEOFENCE_RADIUS_METERS) + .setExpirationDuration(Geofence.NEVER_EXPIRE) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL) + .setLoiteringDelay(1000 * 60 * 5) + .build(); + mGeofences.add(gf); + } + } + + @Override + public void onConnected(Bundle bundle) { + mListener.onConnected(); + } + + @Override + public void disconnect() { + if (mGoogleApiClient.isConnected()) { + LocationServices.GeofencingApi.removeGeofences(mGoogleApiClient, getGeofencePendingIntent()); + super.disconnect(); + } + } + + @Override + public void onResult(Status status) { + if (status.isSuccess()) { + Timber.d("Geofences successfully setup"); + } else { + Timber.d("Geofences failed to setup"); + } + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/client/LocationClient.java b/app/src/main/java/org/healtheheartstudy/client/LocationClient.java new file mode 100644 index 0000000..5543407 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/client/LocationClient.java @@ -0,0 +1,117 @@ +package org.healtheheartstudy.client; + +import android.content.Context; +import android.location.Location; +import android.os.Bundle; + +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; + +import timber.log.Timber; + +/** + * LocationClient provides an easy-to-use interface for retrieving a user's location. + */ +public class LocationClient extends Client implements LocationListener { + + private static final int TWO_MINUTES = 1000 * 60 * 2; + + private Context context; + private Listener mListener; + private LocationRequest request; + private Location mCurrentLocation; + + public interface Listener { + void onConnected(); + void onLocationChanged(Location location); + } + + public LocationClient(Context context, Listener listener) { + this.context = context; + this.mListener = listener; + } + + public void connect() { + connect(context, LocationServices.API); + } + + public void requestLocationUpdates(LocationRequest request) { + this.request = request; + LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, + request, + this); + } + + public void stopLocationUpdates() { + LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); + } + + /** + * Determines if the new location is better than the previous location. This code is taken + * from Google docs. + * @param newLocation + * @return + */ + private boolean isBetterLocation(Location newLocation) { + if (mCurrentLocation == null) { + return true; + } + + long timeDelta = newLocation.getTime() - mCurrentLocation.getTime(); + boolean isMuchNewer = timeDelta > TWO_MINUTES; + boolean isMuchOlder = timeDelta < -TWO_MINUTES; + boolean isNewer = timeDelta > 0; + + if (isMuchNewer) { + return true; + } else if (isMuchOlder) { + return false; + } + + int accuracyDelta = (int) (newLocation.getAccuracy() - mCurrentLocation.getAccuracy()); + boolean isLessAccurate = accuracyDelta > 0; + boolean isMoreAccurate = accuracyDelta < 0; + boolean isMuchLessAccurate = accuracyDelta > 200; + + boolean isSameProvider; + if (newLocation.getProvider() == null) { + isSameProvider = mCurrentLocation.getProvider() == null; + } else { + isSameProvider = newLocation.getProvider().equals(mCurrentLocation.getProvider()); + } + + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else if (isNewer && !isMuchLessAccurate && isSameProvider) { + return true; + } + + return false; + } + + @Override + public void onLocationChanged(Location location) { + Timber.d("onLocationChanged()"); + if (isBetterLocation(location)) { + mCurrentLocation = location; + mListener.onLocationChanged(mCurrentLocation); + } + } + + @Override + public void onConnected(Bundle bundle) { + mListener.onConnected(); + } + + @Override + public void disconnect() { + if (mGoogleApiClient.isConnected()) { + LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); + super.disconnect(); + } + } + +} diff --git a/app/src/main/java/org/healtheheartstudy/network/GsonRequest.java b/app/src/main/java/org/healtheheartstudy/network/GsonRequest.java new file mode 100644 index 0000000..b5d244f --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/network/GsonRequest.java @@ -0,0 +1,64 @@ +package org.healtheheartstudy.network; + +import com.android.volley.AuthFailureError; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.toolbox.HttpHeaderParser; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import java.io.UnsupportedEncodingException; +import java.util.Map; + +/** + * GsonRequest is used in conjunction with Volley to transform JSON responses into objects. + */ +public class GsonRequest extends Request { + private final Gson gson = new Gson(); + private final Class clazz; + private final Map headers; + private final Response.Listener listener; + + /** + * Make a GET request and return a parsed object from JSON. + * + * @param url URL of the request to make + * @param clazz Relevant class object, for Gson's reflection + * @param headers Map of request headers + */ + public GsonRequest(String url, Class clazz, Map headers, + Response.Listener listener, Response.ErrorListener errorListener) { + super(Method.GET, url, errorListener); + this.clazz = clazz; + this.headers = headers; + this.listener = listener; + } + + @Override + public Map getHeaders() throws AuthFailureError { + return headers != null ? headers : super.getHeaders(); + } + + @Override + protected void deliverResponse(T response) { + listener.onResponse(response); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String json = new String( + response.data, + HttpHeaderParser.parseCharset(response.headers)); + return Response.success( + gson.fromJson(json, clazz), + HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JsonSyntaxException e) { + return Response.error(new ParseError(e)); + } + } +} diff --git a/app/src/main/java/org/healtheheartstudy/network/RequestManager.java b/app/src/main/java/org/healtheheartstudy/network/RequestManager.java new file mode 100644 index 0000000..78ebdb4 --- /dev/null +++ b/app/src/main/java/org/healtheheartstudy/network/RequestManager.java @@ -0,0 +1,24 @@ +package org.healtheheartstudy.network; + +import android.content.Context; + +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; + +/** + * RequestManager is a wrapper to simplify Volley's request queue. + */ +public class RequestManager { + + private static RequestQueue requestQueue; + + public static void init(Context context) { + requestQueue = Volley.newRequestQueue(context); + } + + public static RequestQueue getRequestQueue() { + if (requestQueue != null) return requestQueue; + else throw new IllegalStateException("Not initialized"); + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f7158b8..dcaaacc 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,12 @@ android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> - +