Home

Mac OS X 10.10 Juniper VPN

I use a Juniper VPN in order to connect to a remote network. Since upgrading to OS X Yosemite, I found that I could no longer connect to the VPN. Normally, I would launch the application, Network Connect; enter my credentials, and bam! With the new version of OS X, Network Connect would hang when trying to establish a connection to the remote network after enter verifying my credentials. 

The issue lies within Yosemite's driver policies, where all drivers need to be signed now. In Mavericks (10.9) drivers installed to /Libraries/Extensions needed to be signed. If they are installed else where, such as /System/Libraries/Extensions or in the case of Juniper /usr/local/juniper/nc/8.0.7, they were fine. The only way that I was able to find is to turn off this feature, which isn't exactly the best option, but it's the only option until Juniper decides to sign their drivers.

Open up a terminal and run the following command:

sudo nvram boot-args="kext-devel-mode=1"

The command above sets the system into "development mode" where you can "test" the kernel extensions (kext). Again, this is not an ideal solution, but it works. This also works for other drivers as well, such as hard drive TRIM, sound cards, network cards, etc., any driver extensions that isn't currently being signed. To undo this type in the following command in a terminal:

sudo nvram -d boot-args

Android Geofences (Update)

Introduction

A few months ago, I had written about how to setup and monitor geofences. Since then, Google has made changes to their location APIs by removing some of the classes such as the LocationClient class. This change was announced in 2013, but Google's documentation had made no mention of the changes that were to come. At the time of writing, documentation and examples on how to setup and monitor geofences using the new method are scarce.

Requirements

Make sure that you update your SDK packages so that you have Google Play services revision 22. If you are using eclipse and already have Google Play services as  project, you may need to clean and rebuild or reimport the code. If you need help setting up Google Play services, see this.  

Application

The AndroidManifest.xml file doesn't require any changes from my previous entry.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paulusworld.geofence"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!--
            Requests address-level location access, which is usually
            necessary for Geofencing
    -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".GeofenceIntentService" />

        <!-- Required for Google Maps -->
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE" />
    </application>

</manifest>

The android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are necessary for downloading Google Map data. The android.permission.ACCESS_FINE_LOCATION and com.google.android.providers.gsf.permission.READ_GSERVICES are needed for use with geofences. The permission android.permission.WAKE_LOCK is used for notifications and is not required for geofencing.

We're specifying a service on line 38. By using a service, our app does not need to be running when we enter, exit, or dwell in a geofence.

I've defined some strings in the strings.xml that will be used in the example:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <string name="app_name">Geofence</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
 
    <string name="geofence_transition_notification_title">
        %1$s geofence(s) %2$s
    </string>
    <string name="geofence_transition_notification_text">
        Click notification to return to app
    </string>
 
    <string name="geofence_transition_unknown">Unknown transition</string>
    <string name="geofence_transition_entered">Entered</string>
    <string name="geofence_transition_exited">Exited</string>
    <string name="geofence_transition_dwell">Stop dwelling!</string>
    
    <string name="geofence_intent_service">Geofence Intent Service</string>
</resources>

We'll use a simple layout that uses a MapFragment:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.paulusworld.geofence.MainActivity" >
 
    <fragment
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="45"
        class="com.google.android.gms.maps.SupportMapFragment" />
 
</LinearLayout>

One of the reasons why Google changed the way geofences were handled in code was to make it easier for the developer. If you look at the old way, you will notice that there is a lot less code and logic that is required to make this work. Additionally, I moved the geofence logic into a different class called GeofenceStore. I did this to keep the main activity class as clean as possible.

package com.paulusworld.geofence;

import java.util.ArrayList;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.CircleOptions;
import com.google.android.gms.maps.model.LatLng;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity implements OnCameraChangeListener {
	
	/**
	 * Google Map object
	 */
	private GoogleMap mMap;

	/**
	 * Geofence Data
	 */

	/**
	 * Geofences Array
	 */
	ArrayList<Geofence> mGeofences;
	
	/**
	 * Geofence Coordinates
	 */
	ArrayList<LatLng> mGeofenceCoordinates;
	
	/**
	 * Geofence Radius'
	 */
	ArrayList<Integer> mGeofenceRadius;
	
	/**
	 * Geofence Store
	 */
	private GeofenceStore mGeofenceStore;


	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		// Initializing variables
		mGeofences = new ArrayList<Geofence>();
		mGeofenceCoordinates = new ArrayList<LatLng>();
		mGeofenceRadius = new ArrayList<Integer>();
		
		// Adding geofence coordinates to array.
		mGeofenceCoordinates.add(new LatLng(43.042861, -87.911559));
		mGeofenceCoordinates.add(new LatLng(43.042998, -87.909753));
		mGeofenceCoordinates.add(new LatLng(43.040732, -87.921364));
		mGeofenceCoordinates.add(new LatLng(43.039912, -87.897038));
		
		// Adding associated geofence radius' to array.
		mGeofenceRadius.add(100);
		mGeofenceRadius.add(50);
		mGeofenceRadius.add(160);
		mGeofenceRadius.add(160);
		
		// Bulding the geofences and adding them to the geofence array.
		
		// Performing Arts Center
		mGeofences.add(new Geofence.Builder()
				.setRequestId("Performing Arts Center")
				// The coordinates of the center of the geofence and the radius in meters.
				.setCircularRegion(mGeofenceCoordinates.get(0).latitude, mGeofenceCoordinates.get(0).longitude, mGeofenceRadius.get(0).intValue()) 
				.setExpirationDuration(Geofence.NEVER_EXPIRE)
				// Required when we use the transition type of GEOFENCE_TRANSITION_DWELL
				.setLoiteringDelay(30000) 
				.setTransitionTypes(
						Geofence.GEOFENCE_TRANSITION_ENTER
							| Geofence.GEOFENCE_TRANSITION_DWELL
							| Geofence.GEOFENCE_TRANSITION_EXIT).build());
		
		// Starbucks
		mGeofences.add(new Geofence.Builder()
				.setRequestId("Starbucks")
				// The coordinates of the center of the geofence and the radius in meters.
				.setCircularRegion(mGeofenceCoordinates.get(1).latitude, mGeofenceCoordinates.get(1).longitude, mGeofenceRadius.get(1).intValue()) 
				.setExpirationDuration(Geofence.NEVER_EXPIRE)
				// Required when we use the transition type of GEOFENCE_TRANSITION_DWELL
				.setLoiteringDelay(30000) 
				.setTransitionTypes(
						Geofence.GEOFENCE_TRANSITION_ENTER
							| Geofence.GEOFENCE_TRANSITION_DWELL
							| Geofence.GEOFENCE_TRANSITION_EXIT).build());
		
		// Milwaukee Public Museum
		mGeofences.add(new Geofence.Builder()
				.setRequestId("Milwaukee Public Museum")
				// The coordinates of the center of the geofence and the radius in meters.
				.setCircularRegion(mGeofenceCoordinates.get(2).latitude, mGeofenceCoordinates.get(2).longitude, mGeofenceRadius.get(2).intValue()) 
				.setExpirationDuration(Geofence.NEVER_EXPIRE)
				.setTransitionTypes(
						Geofence.GEOFENCE_TRANSITION_ENTER
							| Geofence.GEOFENCE_TRANSITION_EXIT).build());
		
		// Milwaukee Art Museum
		mGeofences.add(new Geofence.Builder()
				.setRequestId("Milwaukee Art Museum")
				// The coordinates of the center of the geofence and the radius in meters.
				.setCircularRegion(mGeofenceCoordinates.get(3).latitude, mGeofenceCoordinates.get(3).longitude, mGeofenceRadius.get(3).intValue()) 
				.setExpirationDuration(Geofence.NEVER_EXPIRE)
				.setTransitionTypes(
						Geofence.GEOFENCE_TRANSITION_ENTER
							| Geofence.GEOFENCE_TRANSITION_EXIT).build());
		
		// Add the geofences to the GeofenceStore object.
		mGeofenceStore = new GeofenceStore(this, mGeofences);

	}

	@Override
	protected void onStart() {
		super.onStart();
	}

	@Override
	protected void onStop() {
		mGeofenceStore.disconnect();
		super.onStop();
	}

	@Override
	protected void onResume() {
		super.onResume();
		if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
			setUpMapIfNeeded();
		} else {
			GooglePlayServicesUtil.getErrorDialog(
					GooglePlayServicesUtil.isGooglePlayServicesAvailable(this),
					this, 0);
		}
	}

	private void setUpMapIfNeeded() {
		// Do a null check to confirm that we have not already instantiated the
		// map.
		if (mMap == null) {
			// Try to obtain the map from the SupportMapFragment.
			mMap = ((SupportMapFragment) getSupportFragmentManager()
					.findFragmentById(R.id.map)).getMap();

			// Check if we were successful in obtaining the map.
			if (mMap != null) {
				setUpMap();
			}
		}
	}

	/**
	 * This is where we can add markers or lines, add listeners or move the
	 * camera. In this case, we just add a marker near Africa.
	 * <p/>
	 * This should only be called once and when we are sure that {@link #mMap}
	 * is not null.
	 */
	private void setUpMap() {
		// Centers the camera over the building and zooms int far enough to
		// show the floor picker.
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(
				43.039634, -87.908395), 14));
		
		// Hide labels.
		mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
		mMap.setIndoorEnabled(false);
		mMap.setMyLocationEnabled(true);
		
		mMap.setOnCameraChangeListener(this);

	}

	@Override
	public void onCameraChange(CameraPosition position) {
		// Makes sure the visuals remain when zoom changes.
		for(int i = 0; i < mGeofenceCoordinates.size(); i++) {
			mMap.addCircle(new CircleOptions().center(mGeofenceCoordinates.get(i))
					.radius(mGeofenceRadius.get(i).intValue())
					.fillColor(0x40ff0000)
					.strokeColor(Color.TRANSPARENT).strokeWidth(2));
		}
	}
}

 

package com.paulusworld.geofence;

import java.util.ArrayList;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
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.Geofence;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

public class GeofenceStore implements ConnectionCallbacks,
		OnConnectionFailedListener, ResultCallback<Status>, LocationListener {

	private final String TAG = this.getClass().getSimpleName();

	/**
	 * Context
	 */
	private Context mContext;
	
	/**
	 * Google API client object.
	 */
	private GoogleApiClient mGoogleApiClient;
	
	/**
	 * Geofencing PendingIntent
	 */
	private PendingIntent mPendingIntent;
	
	/**
	 * List of geofences to monitor.
	 */
	private ArrayList<Geofence> mGeofences;
	
	/**
	 * Geofence request.
	 */
	private GeofencingRequest mGeofencingRequest;
	
	/**
	 * Location Request object.
	 */
	private LocationRequest mLocationRequest;

	/**
	 * Constructs a new GeofenceStore. 
	 * 
	 * @param context The context to use.
	 * @param geofences List of geofences to monitor.
	 */
	public GeofenceStore(Context context, ArrayList<Geofence> geofences) {
		mContext = context;
		mGeofences = new ArrayList<Geofence>(geofences);
		mPendingIntent = null;

		// Build a new GoogleApiClient, specify that we want to use LocationServices
		// by adding the API to the client, specify the connection callbacks are in 
		// this class as well as the OnConnectionFailed method. 
		mGoogleApiClient = new GoogleApiClient.Builder(context)
				.addApi(LocationServices.API).addConnectionCallbacks(this)
				.addOnConnectionFailedListener(this).build();

		// This is purely optional and has nothing to do with geofencing. 
		// I added this as a way of debugging.
		// Define the LocationRequest.
		mLocationRequest = new LocationRequest();
		// We want a location update every 10 seconds.
		mLocationRequest.setInterval(10000);
		// We want the location to be as accurate as possible.
		mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
		
		mGoogleApiClient.connect();
	}

	@Override
	public void onResult(Status result) {
		if (result.isSuccess()) {
			Log.v(TAG, "Success!");
		} else if (result.hasResolution()) {
			// TODO Handle resolution
		} else if (result.isCanceled()) {
			Log.v(TAG, "Canceled");
		} else if (result.isInterrupted()) {
			Log.v(TAG, "Interrupted");
		} else {

		}

	}

	@Override
	public void onConnectionFailed(ConnectionResult connectionResult) {
		Log.v(TAG, "Connection failed.");
	}

	@Override
	public void onConnected(Bundle connectionHint) {
		// We're connected, now we need to create a GeofencingRequest with
		// the geofences we have stored.
		mGeofencingRequest = new GeofencingRequest.Builder().addGeofences(
				mGeofences).build();
		
		mPendingIntent = createRequestPendingIntent();
		
		// This is for debugging only and does not affect
		// geofencing.
		LocationServices.FusedLocationApi.requestLocationUpdates(
				mGoogleApiClient, mLocationRequest, this);
		
		// Submitting the request to monitor geofences.
		PendingResult<Status> pendingResult = LocationServices.GeofencingApi
				.addGeofences(mGoogleApiClient, mGeofencingRequest,
						mPendingIntent);
		
		// Set the result callbacks listener to this class.
		pendingResult.setResultCallback(this);
	}

	@Override
	public void onConnectionSuspended(int cause) {
		Log.v(TAG, "Connection suspended.");
	}

	/**
	 * This creates a PendingIntent that is to be fired when geofence transitions
	 * take place. In this instance, we are using an IntentService to handle the
	 * transitions.
	 * 
	 * @return A PendingIntent that will handle geofence transitions.
	 */
	private PendingIntent createRequestPendingIntent() {
		if (mPendingIntent == null) {
			Log.v(TAG, "Creating PendingIntent");
			Intent intent = new Intent(mContext, GeofenceIntentService.class);
			mPendingIntent = PendingIntent.getService(mContext, 0, intent,
					PendingIntent.FLAG_UPDATE_CURRENT);
		}

		return mPendingIntent;
	}

	@Override
	public void onLocationChanged(Location location) {

		Log.v(TAG, "Location Information\n" 
				+ "==========\n"
				+ "Provider:\t" + location.getProvider() + "\n"
				+ "Lat & Long:\t" + location.getLatitude() + ", "
				+ location.getLongitude() + "\n"
				+ "Altitude:\t" + location.getAltitude() + "\n"
				+ "Bearing:\t" + location.getBearing() + "\n"
				+ "Speed:\t\t" + location.getSpeed() + "\n"
				+ "Accuracy:\t" + location.getAccuracy() + "\n");
	}
}

The GeofenceIntentService is a basic class that handles Intent broadcasts from the LocationServices API. The sendNotification and getTriggeringGeofences are just additional logic code that is executed when a Geofence transition is broadcast.

package com.paulusworld.geofence;

import java.util.List;

import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;

public class GeofenceIntentService extends IntentService {

	private final String TAG = this.getClass().getCanonicalName();
	
	public GeofenceIntentService() {
		super("GeofenceIntentService");
		Log.v(TAG, "Constructor.");
	}

	public void onCreate() {
		super.onCreate();
		Log.v(TAG, "onCreate");
	}
	
	public void onDestroy() {
		super.onDestroy();
		Log.v(TAG, "onDestroy");
	}
	
	@Override
	protected void onHandleIntent(Intent intent) {
		GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
		Log.v(TAG, "onHandleIntent");
		if(!geofencingEvent.hasError()) {
			int transition = geofencingEvent.getGeofenceTransition();
			String notificationTitle;
			
			switch(transition) {
			case Geofence.GEOFENCE_TRANSITION_ENTER:
				notificationTitle = "Geofence Entered";
				Log.v(TAG, "Geofence Entered");
				break;
			case Geofence.GEOFENCE_TRANSITION_DWELL:
				notificationTitle = "Geofence Dwell";
				Log.v(TAG, "Dwelling in Geofence");
				break;
			case Geofence.GEOFENCE_TRANSITION_EXIT:
				notificationTitle = "Geofence Exit";
				Log.v(TAG, "Geofence Exited");
				break;
			default:
				notificationTitle = "Geofence Unknown";
			}
			
			sendNotification(this, getTriggeringGeofences(intent), notificationTitle);
		}
	}

	private void sendNotification(Context context, String notificationText,
			String notificationTitle) {

		PowerManager pm = (PowerManager) context
				.getSystemService(Context.POWER_SERVICE);
		PowerManager.WakeLock wakeLock = pm.newWakeLock(
				PowerManager.PARTIAL_WAKE_LOCK, "");
		wakeLock.acquire();

		NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
				context).setSmallIcon(R.drawable.ic_launcher)
				.setContentTitle(notificationTitle)
				.setContentText(notificationText)
				.setDefaults(Notification.DEFAULT_ALL).setAutoCancel(false);

		NotificationManager notificationManager = (NotificationManager) context
				.getSystemService(Context.NOTIFICATION_SERVICE);
		notificationManager.notify(0, notificationBuilder.build());

		wakeLock.release();
	}
	
	private String getTriggeringGeofences(Intent intent) {
		GeofencingEvent geofenceEvent = GeofencingEvent.fromIntent(intent);
		List<Geofence> geofences = geofenceEvent
				.getTriggeringGeofences();
		
		String[] geofenceIds = new String[geofences.size()];

		for (int i = 0; i < geofences.size(); i++) {
			geofenceIds[i] = geofences.get(i).getRequestId();
		}

		return TextUtils.join(", ", geofenceIds);
	}
}

In order to get Geofence information from the intent received in the IntentService, we create a GeofencingEvent by calling GeofencingEvent.fromIntent(Intent intent) on line 39. On line 41, we make sure there are no errors before getting the transition on line 42 by calling the getGeofenceTransition function. From that point, the rest of the function is self explanatory and the rest of the class is beyond the scope of Geofencing.

There is one important thing to remember when working with geofences, transition broadcasts won't be fired if the user is not completely in the geofence. Accuracy is also taken into account when determining if a user has entered, exitied, or is dwelling. For example, if a geofence has a diameter of 100 meters and your accuracy is 150 meteres even if you are shown as being smack in the center of the geofence, no transitions will be fired because Google isn't sure if you are in the geofence or not.  

Android Geofences

Google has deprecated and removed some of the classes used in this example in favor of their Fuse Location API. An updated example on how to use the new API is in the works.

Introduction

I'm aware that there already exists a geofence tutorial and example on developer.android.com. However, at the time of writing there are two issues that I have with them. The first being that the tutorial's code doesn't match with the example code, which is linked on the tutorial's page. Though the example is a wonderful resource and shows you what to do and how to do it, trying to understand what you need to do can be difficult figure out. 

Requirements

Since we will be using Google Maps and Location based objects, we will need to have our project reference the Google Play Services. See this tutorial on adding the Google Play Services to your project.

Application

The first thing we need to do is add the necessary entries to the manifest file for both the permissions and pieces of our application.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paulusworld.geofence"
    android:versionCode="1"
    android:versionName="1.0" >
    
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

    <uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
	<!--
            Requests address-level location access, which is usually
            necessary for Geofencing
    -->
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
	<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name="com.paulusworld.geofence.ReceiveTransitionsIntentService" android:exported="false"></service>
        
        <!-- 
        	Required for Google Maps
         -->
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="ANDROID_MAPS_API_KEY_GOES_HERE" />
    </application>

</manifest>

The android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are necessary for downloading Google Map data. The android.permission.ACCESS_FINE_LOCATION and com.google.android.providers.gsf.permission.READ_GSERVICES are needed for use with geofences.

We're specifying a service on line 36. By using a service, our app does not need to be running when we enter, exit, or dwell in a geofence.

I've defined some strings in the strings.xml that will be used in the example:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">Geofence</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>

    <string name="geofence_transition_notification_title">
        %1$s geofence(s) %2$s
    </string>
    <string name="geofence_transition_notification_text">
        Click notification to return to app
    </string>

    <string name="geofence_transition_unknown">Unknown transition</string>
    <string name="geofence_transition_entered">Entered</string>
    <string name="geofence_transition_exited">Exited</string>
    <string name="geofence_transition_dwell">Stop dwelling!</string>
</resources>

We'll use a simple layout that uses a MapFragment:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.paulusworld.geofence.MainActivity" >

    <fragment
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="45"
        class="com.google.android.gms.maps.SupportMapFragment" />

</LinearLayout>

Normally we won't be throwing so much of the location related code in the MainActivity, but for simplicity's sake we will do it this time.  

package com.paulusworld.geofence;

import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.widget.Toast;


import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationStatusCodes;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
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 java.util.ArrayList;

public class MainActivity extends FragmentActivity
        implements GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener,
        LocationClient.OnAddGeofencesResultListener {

    private final static String TAG = "MainActivity";
    /**
     * Google Map object
     */
    private GoogleMap mMap;

    /**
     * Geofence Data
     */

    /**
     * Coordinates for the Geofence.
     */
    private LatLng mGeofenceLatLng = new LatLng(YOUR_LATITUDE, YOUR_LONGITUDE);

    /**
     * Radius of the Geofence in meters.
     */
    private int mRadius = 80;

    /**
     * The Geofence object.
     */
    private Geofence mGeofence;

    /**
     * Entry point for Google's location related APIs.
     */
    private LocationClient mLocationClient;

    /**
     * Used to set the priority and intervals of the location requests.
     */
    private LocationRequest mLocationRequest;

    /**
     * Visuals
     */
    private CircleOptions mCircleOptions;
    private Circle mCircle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /** 
         * We create a new LocationClient which is used as an entry point for Google's location
         * related APIs. The first parameter is the context, the second is
         * GooglePlayServicesClient.ConnectionCallbacks, and the third is
         * GooglePlayServicesClient.OnConnectionFailedListener. Since we implemented both listeners
         * on the MainActivity class, we pass 'this' for the second and third parameters.
         */
        mLocationClient = new LocationClient(this, this, this);

        /**
         * With the LocationRequest, we can set the quality of service. For example, the priority
         * and intervals.
         */
        mLocationRequest = LocationRequest.create();
        mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
        mLocationRequest.setInterval(3600000);
        mLocationRequest.setFastestInterval(60000);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Connect to the location APIs.
        mLocationClient.connect();
    }

    protected void onStop() {
        // Disconnect from the location APIs.
        mLocationClient.disconnect();
        super.onStop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
            setUpMapIfNeeded();
        } else {
            GooglePlayServicesUtil.getErrorDialog(
                    GooglePlayServicesUtil.isGooglePlayServicesAvailable(this),
                    this, 0);
        }
    }

    private void setUpMapIfNeeded() {
        // Do a null check to confirm that we have not already instantiated the
        // map.
        if (mMap == null) {
            // Try to obtain the map from the SupportMapFragment.
            mMap = ((SupportMapFragment) getSupportFragmentManager()
                    .findFragmentById(R.id.map)).getMap();

            // Check if we were successful in obtaining the map.
            if (mMap != null) {
                setUpMap();
            }
        }
    }

    /**
     * This is where we can add markers or lines, add listeners or move the
     * camera. In this case, we just add a marker near Africa.
     * <p/>
     * This should only be called once and when we are sure that {@link #mMap}
     * is not null.
     */
    private void setUpMap() {
        // Centers the camera over the building and zooms int far enough to
        // show the floor picker.
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
                new LatLng(mGeofenceLatLng.latitude, mGeofenceLatLng.longitude), 18));
        // Hide labels.
        mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
        mMap.setIndoorEnabled(false);
        mMap.setMyLocationEnabled(true);

        // Adding visuals.
        mCircleOptions = new CircleOptions()
                .center(mGeofenceLatLng).radius(mRadius).fillColor(0x40ff0000)
                .strokeColor(Color.TRANSPARENT).strokeWidth(2);
        mCircle = mMap.addCircle(mCircleOptions);

    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Log.v("GEOFENCE", "Connection to LocationClient failed!");
    }

    @Override
    public void onConnected(Bundle arg0) {

        Log.v("GEOFENCE", "Connected to location services.");

        ArrayList<Geofence> geofences = new ArrayList<Geofence>();

        /**
         * The addGeofences function requires that the Geofences be in a List, so there can be
         * multiple geofences. For this example we will only need one.
         */
        mGeofence = new Geofence.Builder()
                .setRequestId("Geofence")
                // There are three types of Transitions. 
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
                // Set the geofence location and radius. 
                .setCircularRegion(mGeofenceLatLng.latitude, mGeofenceLatLng.longitude, mRadius)
                // How long the geofence will remain in place. 
                .setExpirationDuration((1000 * 60) * 60)
                // This is required if you specify GEOFENCE_TRANSITION_DWELL when setting the transition types.
                .setLoiteringDelay(1000)
                .build();

        /**
         * Adding the geofence to the ArrayList, which will be passed as the first parameter
         * to the LocationClient object's addGeofences function.
         */
        geofences.add(mGeofence);

        /**
         * We're creating a PendingIntent that references the ReceiveTransitionsIntentService class
         * in conjunction with the geofences.
         */
        Intent intent = new Intent(this, ReceiveTransitionsIntentService.class);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        /**
         * We want this (MainActivity) to handle location updates.(see onLocationChanged function)
         */
        mLocationClient.requestLocationUpdates(mLocationRequest, this);
        /**
         * Adding the Geofences and PendingIntent to the LocationClient and setting this
         * (MainActivity) to handle onAddGeofencesResult. The pending intent, which is the
         * ReceiveTransitionsIntentService, is what gets utilized when one of the transitions
         * that was specified in the geofence is fired.
         */
        mLocationClient.addGeofences(geofences, pendingIntent, this);

    }

    @Override
    public void onDisconnected() {
        Log.v("GEOFENCE", "Disconnected");
    }

    @Override
    public void onLocationChanged(Location location) {
        /**
         * Location data is passed back to this function.
         */
        Toast.makeText(this, "Location Changed: " + location.getLatitude() + ", " + location.getLongitude(), Toast.LENGTH_LONG).show();
    }

    @Override
    public void onAddGeofencesResult(int statusCode, String[] geofenceRequestIds) {

        switch(statusCode) {
            case LocationStatusCodes.SUCCESS:
                Log.v(TAG, "Successfully added Geofence.");
                break;
            case LocationStatusCodes.ERROR:
                Log.v(TAG, "Error adding Geofence.");
                break;
            case LocationStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
                Log.v(TAG, "Too many geofences.");
                break;
            case LocationStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
                Log.v(TAG, "Too many pending intents.");
                break;
        }
    }
}

The MainActivity class implements the the GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener, LocationListener, and LocatoinClient.OnAddGeofencesResultListener.

The GooglePlayServicesClient.ConnectionCallbacks requires the class to implement the following functions:

The GooglePlayServicesClient.OnConnectionFailedListener requires the class to implement the onConnectionFailed(ConnectionResult result) function.

The LocationListener has four functions that can be implemented, but only one of them is required:

Finally, the LocatoinClient.onAddGeofencesResultListener requires the onAddGeofencesResult function to be implemented. This function is called when the addGeofences(List, PendingIntent, OnAddGeofencesResultListener) operation completes, whether successfully or not.

On lines 51 and 56 you can specify your own latitude, longitude, and radius of the geofence. For the most part, if you've seen my previous blog entries regarding Google Maps, there is a lot of code that is reused. 

On line 91 we're creating a LocationClient object that is saying to "Hey, I want to use Google's location services."

On lines 97 to 100, we're creating a LocationRequest object that sets the various parameters that will be applied for requesting location updates. This LocationRequest object is passed in the LocationClient objects function requestLocationUpdates.

On line 160 is simply adding visuals to the map to make it easier to see where the geofence is.

Line 169 is overriding an abstract function that is defined by implementing the OnConnectionFailedListener.

Line 174 is overriding another abstract function that is defined in GooglePlayServicesClient.ConnectionCallbacks. In this function, we're creating the geofence that we want to monitor. On line 178 we're creating an ArrayList to hold the geofences, even though there is only one. We're doing this because when we call the function that adds the geofences, LocationClient.addGeofences it expects an ArrayList of Geofence objects. Geofence objects are built, not instantiated like most other objects. We do this on line 184 using the Builder class and setting all the parameters through other functions from lines 185 to 193 until finally returning a Geofence object by calling the build() function. The Geofence is added to the ArrayLsit on line 200. On line 206 - 207, we're creating a PendingIntent, which is tied to an IntentService and is fired when the conditions set on the Geofence have been met. In this case, enter, dwell, and exit. In order for the PendingIntents to be handled, we need to add the Geofences to the LocationClient.

Line 224 overrides the abstract function onDisconnect that is defined in GooglePlayServicesClient.ConnectionCallbacks.

Line 229 overrides the abstract function onLocationChanged that is defined in LocationSource.OnLocationChangedListener.

The onAddGeofencesResult function found on line 237 is defined in the LocationClient.OnAddGeofencesResultListener. In this function, we would do any work that needs to be done in the event that a specific statusCode is returned.

package com.paulusworld.geofence;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.LocationClient;

import java.util.List;

public class ReceiveTransitionsIntentService extends IntentService {

    private final static String TAG = ReceiveTransitionsIntentService.class.getPackage() + "." + ReceiveTransitionsIntentService.class.getSimpleName();

	public ReceiveTransitionsIntentService() {
		super("ReceiveTransitionsIntentService");
        Log.v(TAG, "Service Constructor");
	}

	@Override
	protected void onHandleIntent(Intent intent) {

        if(!LocationClient.hasError(intent)) {
            int transition = LocationClient.getGeofenceTransition(intent);
            Log.v(TAG, "Transition: " + transition);

            // Post a notification
            List<Geofence> geofences = LocationClient.getTriggeringGeofences(intent);
            String[] geofenceIds = new String[geofences.size()];
            for (int index = 0; index < geofences.size() ; index++) {
                geofenceIds[index] = geofences.get(index).getRequestId();
            }
            String ids = TextUtils.join(", ", geofenceIds);
            String transitionType = getTransitionString(transition);

            sendNotification(transitionType, ids);
        } else {
            Log.e(TAG, String.valueOf(LocationClient.getErrorCode(intent)));
        }
	}

    /**
     * Posts a notification in the notification bar when a transition is detected.
     * If the user clicks the notification, control goes to the main Activity.
     * @param transitionType The type of transition that occurred.
     *
     */
    private void sendNotification(String transitionType, String ids) {

        // Create an explicit content Intent that starts the main Activity
        Intent notificationIntent =
                new Intent(getApplicationContext(),MainActivity.class);

        // Construct a task stack
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);

        // Adds the main Activity to the task stack as the parent
        stackBuilder.addParentStack(MainActivity.class);

        // Push the content Intent onto the stack
        stackBuilder.addNextIntent(notificationIntent);

        // Get a PendingIntent containing the entire back stack
        PendingIntent notificationPendingIntent =
                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        // Get a notification builder that's compatible with platform versions >= 4
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

        // Set the notification contents
        builder.setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle(
                        getString(R.string.geofence_transition_notification_title,
                                transitionType, ids))
                .setContentText(getString(R.string.geofence_transition_notification_text))
                .setContentIntent(notificationPendingIntent);

        // Get an instance of the Notification manager
        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        // Issue the notification
        mNotificationManager.notify(0, builder.build());
    }

    private String getTransitionString(int transitionType) {
        switch (transitionType) {

            case Geofence.GEOFENCE_TRANSITION_ENTER:
                return getString(R.string.geofence_transition_entered);

            case Geofence.GEOFENCE_TRANSITION_EXIT:
                return getString(R.string.geofence_transition_exited);

            case Geofence.GEOFENCE_TRANSITION_DWELL:
                return getString(R.string.geofence_transition_dwell);

            default:
                return getString(R.string.geofence_transition_unknown);
        }
    }
}

We're using an IntentService because we want to still be able to handle transitions and dwelling Intents even when the application is closed. As long as the geofence has not expired, the LocationClient will continue to fire off Intents, which will be picked up and handled by the IntentService.onHandleIntent

On line 31 we're using the LocationClient to check to see if the Intent contains any errors and if it doesn't, we will proceed to handle the Intent. Once we know that there are no errors, we determine the transition type on line 32.

Line 36 retrieves the geofences that have been triggered. At this point, we have all the information we need to react accordingly. The rest of the function creates a notification reporting the type of transition and which geofence triggered it. 

Pages