Create Location-Aware Ads in Android With the HERE Geofencing API
Inspired by fast food company dev teams, we take a look at how to add geofencing to a mobile application using Java and the HERE API.
Join the DZone community and get the full member experience.
Join For FreeA few months ago you probably heard the story about Burger King giving out coupons for $0.01 burgers every time someone entered a McDonald's with the Burger King mobile application installed. Whether or not you like either fast food chain, this was an ingenious marketing strategy and use of location in a mobile application.
So how does something like this work from a technical point of view?
Since I don't work for Burger King, I can't give you the definitive story behind their technical adventures, but I can make some assumptions. Long story short, the fast food chain created a virtual geofence around each of their competitor's stores. Using positioning within their mobile application, they likely determined whether or not that device entered a geofence and, if it did, sent them a notification with a coupon.
You might remember the tutorial "Geofencing Regions with JavaScript and HERE" and while it focused on geofencing, it wasn't for a mobile audience. This time around we're going to see an Android example in the spirit of what Burger King was doing in their mobile app.
Take a look at the following image of what we aim to accomplish:
Yes, the example does not look polished, but that would be the designers' job anyway. However, the functionality is there. The goal is to create two geofences. Where I live there is a Domino's Pizza directly next to a Pizza Hut. Why two pizza restaurants are directly next to each other is beyond me, but it leaves an opportunity for this example.
If we enter the geofence for Domino's, we'll get a message to go eat at Pizza Hut and if we enter the geofence for Pizza Hut we'll get a message to go eat at Domino's. It's simple, but can be expanded to be more powerful.
To save the client from having to do a lot of number crunching, geofence position detection is handled server side from the HERE servers. Essentially, we upload fence data and then we send our position every time we want to check the proximity to a fence. Remember, we could have potentially thousands of fences, so doing this client side would not be a good idea.
HERE expects the geofence data to be in well-known text (WKT) format, so take the following example:
POLYGON ((-121.4367556774805 37.73954861759737,-121.43642310582709 37.73954227917106,-121.43643380257618 37.73925593087524,-121.43674763281797 37.73924735532325,-121.4367556774805 37.73954861759737))
The above POLYGON
has numerous points that define it. It is, for example, a Pizza Hut restaurant in Tracy, CA. Another fence might look like the following:
POLYGON ((-121.43717674042182 37.739442604238896,-121.43687095484256 37.739442604238896,-121.43688173999453 37.73916470683655,-121.43720352649598 37.739149792814075,-121.43717674042182 37.739442604238896))
The above POLYGON
data represents our Domino's restaurant. While the data is correctly formatted, it isn't quite ready to be sent to the HERE servers. Instead, it has to be added to a tab-delimited file like the following:
NAME WKT
PIZZA_HUT POLYGON ((-121.4367556774805 37.73954861759737,-121.43642310582709 37.73954227917106,-121.43643380257618 37.73925593087524,-121.43674763281797 37.73924735532325,-121.4367556774805 37.73954861759737))
DOMINOS POLYGON ((-121.43717674042182 37.739442604238896,-121.43687095484256 37.739442604238896,-121.43688173999453 37.73916470683655,-121.43720352649598 37.739149792814075,-121.43717674042182 37.739442604238896))
It is very important that the file is tab-delimited and those are actual tabs, not spaces representing tabs. The NAME
column represents the name of the particular fence. For example, it could be a store id value or whatever else you can think up. The WKT
column represents the actual geofence.
Give the file a name with a .wkt extension and add it to a ZIP archive. The file must exist at the root of the ZIP archive.
Per the HERE Geofencing API documentation, the fence data can be uploaded with the following cURL command:
curl
--request
-i
-X POST
-H "Content-Type: multipart/form-data"
-F "zipfile=@/path/to/zip/file.zip"
"https://gfe.api.here.com/2/layers/upload.json?layer_id=4711&app_id={YOUR_APP_ID}&app_code={YOUR_APP_CODE}"
Take note of the layer_id
and make sure you provide your own app id and app code values, found in the HERE Developer Portal. You can have numerous layers, representing different groups of geofencing data, so it is important to reference the correct layer_id
in the Android project.
Displaying a HERE Map in Android With Polygon Objects Drawn
While neither a map or visual polygon objects are truly necessary for this example, it gives us something a little more interesting to talk about. Remember, positioning and geofencing don't require a visual component at all as most of our logic is run through HTTP requests.
If you've never used a HERE map before in your Android project, I recommend you take a look at a tutorial I wrote titled, Getting Started with HERE Maps in an Android Application. We won't be going through the configuration steps for this particular tutorial.
Instead, add the following to your MainActivity.java file:
package com.example.raboy;
import android.graphics.Color;
import android.graphics.PointF;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import com.here.android.mpa.common.GeoCoordinate;
import com.here.android.mpa.common.GeoPolygon;
import com.here.android.mpa.common.GeoPosition;
import com.here.android.mpa.common.OnEngineInitListener;
import com.here.android.mpa.mapping.Map;
import com.here.android.mpa.mapping.MapFragment;
import com.here.android.mpa.mapping.MapPolygon;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private Map map = null;
private MapFragment mapFragment = null;
private GeoCoordinate currentPosition = new GeoCoordinate(37.7397, -121.4252);
private GeoCoordinate oldPosition = new GeoCoordinate(37.7397, -121.4252);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mapFragment = (MapFragment) getFragmentManager().findFragmentById(R.id.mapfragment);
mapFragment.init(new OnEngineInitListener() {
@Override
public void onEngineInitializationCompleted(OnEngineInitListener.Error error) {
if (error == OnEngineInitListener.Error.NONE) {
map = mapFragment.getMap();
map.setCenter(new GeoCoordinate(37.7394, -121.4366, 0.0), Map.Animation.NONE);
map.setZoomLevel(18);
List<GeoCoordinate> pizzahutShape = new ArrayList<GeoCoordinate>();
pizzahutShape.add(new GeoCoordinate(37.73954861759737, -121.4367556774805, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73954227917106, -121.43642310582709, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73925593087524, -121.43643380257618, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73924735532325, -121.43674763281797, 0.0));
List<GeoCoordinate> dominosShape = new ArrayList<GeoCoordinate>();
dominosShape.add(new GeoCoordinate(37.739442604238896, -121.43717674042182, 0.0));
dominosShape.add(new GeoCoordinate(37.739442604238896, -121.43687095484256, 0.0));
dominosShape.add(new GeoCoordinate(37.73916470683655, -121.43688173999453, 0.0));
dominosShape.add(new GeoCoordinate(37.739149792814075, -121.43720352649598, 0.0));
GeoPolygon polygon = new GeoPolygon(pizzahutShape);
MapPolygon mapPolygon = new MapPolygon(polygon);
mapPolygon.setFillColor(Color.argb(70, 0, 255, 0));
GeoPolygon geoDominosPolygon = new GeoPolygon(dominosShape);
MapPolygon mapDominosPolygon = new MapPolygon(geoDominosPolygon);
mapDominosPolygon.setFillColor(Color.argb(70, 0, 0, 255));
map.addMapObject(mapPolygon);
map.addMapObject(mapDominosPolygon);
}
}
});
}
}
In the above code, we are initializing our map component, centering it on some coordinates, and drawing two polygons on the map. If you look at the coordinates used in each of the List<GeoCoordinate>
variables, they should look very familiar to what we had put in our WKT file before uploading it to the HERE servers. Essentially, we're just drawing each of the two geofences to the map. Remember, to use a geofence, it is not a requirement to draw one, it only helps us out for visual reasons.
Detecting Changes in Device Location With the HERE Positioning API
You might remember that I wrote a previous tutorial titled, Gathering the Android Device Position with the HERE Positioning API, where we listened for changes in the position. We're going to carry those concepts into this tutorial so we can check for how close our changed location is to a geofence.
Take a look at the following modifications:
public class MainActivity extends AppCompatActivity {
private Map map = null;
private MapFragment mapFragment = null;
private PositioningManager positioningManager = null;
private PositioningManager.OnPositionChangedListener positionListener;
private GeoCoordinate currentPosition = new GeoCoordinate(37.7397, -121.4252);
private GeoCoordinate oldPosition = new GeoCoordinate(37.7397, -121.4252);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mapFragment = (MapFragment) getFragmentManager().findFragmentById(R.id.mapfragment);
mapFragment.init(new OnEngineInitListener() {
@Override
public void onEngineInitializationCompleted(OnEngineInitListener.Error error) {
if (error == OnEngineInitListener.Error.NONE) {
map = mapFragment.getMap();
map.setCenter(new GeoCoordinate(37.7394, -121.4366, 0.0), Map.Animation.NONE);
map.setZoomLevel(18);
positioningManager = PositioningManager.getInstance();
positionListener = new PositioningManager.OnPositionChangedListener() {
@Override
public void onPositionUpdated(PositioningManager.LocationMethod method, GeoPosition position, boolean isMapMatched) {
currentPosition = position.getCoordinate();
if(!currentPosition.equals(oldPosition)) {
map.setCenter(position.getCoordinate(), Map.Animation.NONE);
oldPosition = currentPosition;
}
}
@Override
public void onPositionFixChanged(PositioningManager.LocationMethod method, PositioningManager.LocationStatus status) { }
};
try {
positioningManager.addListener(new WeakReference<>(positionListener));
if(!positioningManager.start(PositioningManager.LocationMethod.GPS_NETWORK)) {
Log.e("HERE", "PositioningManager.start: Failed to start...");
}
} catch (Exception e) {
Log.e("HERE", "Caught: " + e.getMessage());
}
map.getPositionIndicator().setVisible(true);
List<GeoCoordinate> pizzahutShape = new ArrayList<GeoCoordinate>();
pizzahutShape.add(new GeoCoordinate(37.73954861759737, -121.4367556774805, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73954227917106, -121.43642310582709, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73925593087524, -121.43643380257618, 0.0));
pizzahutShape.add(new GeoCoordinate(37.73924735532325, -121.43674763281797, 0.0));
List<GeoCoordinate> dominosShape = new ArrayList<GeoCoordinate>();
dominosShape.add(new GeoCoordinate(37.739442604238896, -121.43717674042182, 0.0));
dominosShape.add(new GeoCoordinate(37.739442604238896, -121.43687095484256, 0.0));
dominosShape.add(new GeoCoordinate(37.73916470683655, -121.43688173999453, 0.0));
dominosShape.add(new GeoCoordinate(37.739149792814075, -121.43720352649598, 0.0));
GeoPolygon polygon = new GeoPolygon(pizzahutShape);
MapPolygon mapPolygon = new MapPolygon(polygon);
mapPolygon.setFillColor(Color.argb(70, 0, 255, 0));
GeoPolygon geoDominosPolygon = new GeoPolygon(dominosShape);
MapPolygon mapDominosPolygon = new MapPolygon(geoDominosPolygon);
mapDominosPolygon.setFillColor(Color.argb(70, 0, 0, 255));
map.addMapObject(mapPolygon);
map.addMapObject(mapDominosPolygon);
}
}
});
}
}
The changes to the above code will configure a listener so we know when the location changes. If you're curious to know what exactly changed, take a look at the following chunk of code:
positioningManager = PositioningManager.getInstance();
positionListener = new PositioningManager.OnPositionChangedListener() {
@Override
public void onPositionUpdated(PositioningManager.LocationMethod method, GeoPosition position, boolean isMapMatched) {
currentPosition = position.getCoordinate();
if(!currentPosition.equals(oldPosition)) {
map.setCenter(position.getCoordinate(), Map.Animation.NONE);
oldPosition = currentPosition;
}
}
@Override
public void onPositionFixChanged(PositioningManager.LocationMethod method, PositioningManager.LocationStatus status) { }
};
try {
positioningManager.addListener(new WeakReference<>(positionListener));
if(!positioningManager.start(PositioningManager.LocationMethod.GPS_NETWORK)) {
Log.e("HERE", "PositioningManager.start: Failed to start...");
}
} catch (Exception e) {
Log.e("HERE", "Caught: " + e.getMessage());
}
map.getPositionIndicator().setVisible(true);
The most meaningful part of the code, for us, resides in the onPositionUpdated
method. This is where we'll take the updated position and send it to the HERE server to see our proximity to a geofence.
A few things to note, though:
- The user will have to accept GPS permissions at runtime for the application, otherwise, the location can not be determined.
- The AndroidManifest.xml file needs to have updated permissions for location.
You can learn about both of these in my previous tutorial on positioning with Android.
Checking the Proximity of a Geofence With HTTP in Android
As of now, we've created and uploaded our geofence and configured our Android application to listen for changes in position. The real magic is checking to see if our position is within a geofence that we've uploaded.
To do this, we'll need to make use of HTTP requests within Android. When the position changes, we'll need to send that position to the HERE server along with the layer_id
for our possible geofences. If there is a match, we'll see as much in the results and be able to do a push notification or similar to the user.
There are numerous ways to do HTTP requests in Android, but the easiest is with Volley. You will need to install Volley with Gradle before you can use it, but if you need help, you can learn more about it in a previous tutorial I wrote titled, Using Volley to Make Android HTTP Requests.
Within the project, add the following method to the MainActivity
class:
public void makeRequest(GeoCoordinate coords) {
RequestQueue queue = Volley.newRequestQueue(this);
JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET,"https://gfe.api.here.com/2/search/proximity.json?layer_ids=4711&app_id=APP_ID_HERE&app_code=APP_CODE_HERE&proximity=" + coords.getLatitude() + "," + coords.getLongitude() + "&key_attribute=NAME", null, new com.android.volley.Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
JSONArray geometries = response.getJSONArray("geometries");
if(geometries.length() > 0) {
if(geometries.getJSONObject(0).getJSONObject("attributes").getString("NAME").equals("PIZZA_HUT")) {
Log.d("HERE", "Don't eat at Pizza Hut, eat at Dominos.");
} else {
Log.d("HERE", "Don't eat at Dominos, eat at Pizza Hut.");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, new com.android.volley.Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d("HERE", error.getMessage());
}
});
queue.add(request);
}
Alright, so what is happening in this makeRequest
method?
We're going to be making a GET request and expect a response in JSON format. The HTTP request is defined in the HERE Geofencing API documentation, but we're taking our layer_id
and current coordinates to check our proximity. We're also asking for a NAME
attribute because we uploaded name information in our WKT file.
If we get a successful response, we'll parse the data and see if any geometries were returned. If a geometry was returned it means we are likely within a geofence. If we're in a geofence we should respond as such. In this example, we are only printing to the logs, but a push notification or alert dialog could work too.
With the method in place, we can update our onPositionUpdated
function to look like the following:
@Override
public void onPositionUpdated(PositioningManager.LocationMethod method, GeoPosition position, boolean isMapMatched) {
currentPosition = position.getCoordinate();
if(!currentPosition.equals(oldPosition)) {
makeRequest(new GeoCoordinate(position.getCoordinate().getLatitude(), position.getCoordinate().getLongitude()));
map.setCenter(position.getCoordinate(), Map.Animation.NONE);
oldPosition = currentPosition;
}
}
Notice that we are calling the makeRequest
method every time the position changes. In a production scenario, you may want to make sure the position changes based on a particular threshold so you're not making non-stop requests and draining the battery.
Conclusion
You just saw how to do geofencing with HERE and Android. This example was modeled after the McDonalds and Burger King shenanigans that happened a few months ago, but you can easily take this much further. For example, you could expand until this demo and have the location and geofence checking happen in the background.
Location intelligence is becoming more of a thing with brands trying to leverage everything in terms of location data to drive sales. For example, check out McDonalds latest acquisition of Dynamic Yield.
Published at DZone with permission of Nic Raboy, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments